Mkbitmap zu WebAssembly kompilieren

Im Artikel Was ist WebAssembly und woher kommt es? Ich habe erklärt, wie das heutige WebAssembly entstanden ist. In diesem Artikel zeige ich Ihnen meinen Ansatz, ein bestehendes C-Programm, mkbitmap, in WebAssembly zu kompilieren. Es ist komplexer als das Hello World-Beispiel, da es um die Arbeit mit Dateien, die Kommunikation zwischen WebAssembly und JavaScript sowie das Zeichnen in einem Canvas geht. Aber es ist dennoch so handlich, dass Sie nicht überfordert werden.

Der Artikel richtet sich an Webentwickler, die WebAssembly kennenlernen möchten, und eine detaillierte Anleitung, wie Sie vorgehen können, wenn Sie etwas wie mkbitmap in WebAssembly kompilieren möchten. Es ist völlig normal, dass eine App oder Bibliothek bei der ersten Ausführung nicht kompiliert wird. Aus diesem Grund haben einige der unten beschriebenen Schritte nicht funktioniert. Daher musste ich den Schritt zurückverfolgen und es noch einmal versuchen. Der Artikel zeigt den magischen Befehl zur endgültigen Kompilierung nicht so, als wäre er vom Himmel gefallen. Vielmehr beschreibt er meinen tatsächlichen Fortschritt und auch einige Frustrationen.

Ungefähr mkbitmap

Das C-Programm mkbitmap liest ein Bild und wendet in dieser Reihenfolge einen oder mehrere der folgenden Vorgänge darauf an: Inversion, Hochpassfilterung, Skalierung und Schwellenwertverarbeitung. Jeder Vorgang kann individuell gesteuert und aktiviert oder deaktiviert werden. mkbitmap wird hauptsächlich dazu verwendet, Farb- oder Graustufenbilder in ein Format zu konvertieren, das als Eingabe für andere Programme geeignet ist, insbesondere das Tracing-Programm potrace, das die Grundlage von SVGcode bildet. mkbitmap ist als Vorverarbeitungstool besonders nützlich, um gescannte Strichzeichnungen wie Cartoons oder handschriftlichen Text in hochauflösende Bilder auf zwei Ebenen umzuwandeln.

Sie verwenden mkbitmap, indem Sie ihr mehrere Optionen und einen oder mehrere Dateinamen übergeben. Alle Einzelheiten finden Sie auf der Manpage des Tools:

$ mkbitmap [options] [filename...]
Zeichentrickbild in Farbe.
Das Originalbild (Quelle).
Zeichentrickbild nach Vorverarbeitung in Graustufen konvertiert.
Erste Skalierung, dann Grenzwert: mkbitmap -f 2 -s 2 -t 0.48 (Quelle).

Code abrufen

Der erste Schritt besteht darin, den Quellcode von mkbitmap abzurufen. Sie finden sie auf der Website des Projekts. Als diese Nachricht verfasst wurde, war potrace-1.16.tar.gz die neueste Version.

Kompilieren und lokal installieren

Der nächste Schritt besteht darin, das Tool lokal zu kompilieren und zu installieren, um ein Gefühl dafür zu bekommen, wie es sich verhält. Die Datei INSTALL enthält die folgenden Anweisungen:

  1. cd in das Verzeichnis, das den Quellcode des Pakets enthält, und geben ./configure ein, um das Paket für Ihr System zu konfigurieren.

    Das Ausführen von configure kann eine Weile dauern. Während der Ausführung werden einige Meldungen ausgegeben, die angeben, welche Features geprüft werden.

  2. Geben Sie make ein, um das Paket zu kompilieren.

  3. Geben Sie optional make check ein, um die im Paket enthaltenen Selbsttests auszuführen. In der Regel werden dafür die soeben erstellten, deinstallierten Binärdateien verwendet.

  4. Geben Sie make install ein, um die Programme und alle Datendateien und die Dokumentation zu installieren. Bei der Installation in einem Präfix, das dem Root-Konto gehört, sollte das Paket als regulärer Nutzer konfiguriert und erstellt werden. Nur die Phase make install sollte mit Root-Berechtigungen ausgeführt werden.

Wenn Sie diese Schritte ausführen, sollten Sie zwei ausführbare Dateien haben: potrace und mkbitmap. In diesem Artikel geht es um letztere. Sie können prüfen, ob er ordnungsgemäß funktioniert hat, indem Sie mkbitmap --version ausführen. Hier ist die Ausgabe aller vier Schritte meines Computers, die der Kürze halber gekürzt wurde:

Schritt 1, ./configure:

 $ ./configure
checking for a BSD-compatible install... /usr/bin/install -c
checking whether build environment is sane... yes
checking for a thread-safe mkdir -p... ./install-sh -c -d
checking for gawk... no
checking for mawk... no
checking for nawk... no
checking for awk... awk
checking whether make sets $(MAKE)... yes
[…]
config.status: executing libtool commands

Schritt 2, make:

$ make
/Applications/Xcode.app/Contents/Developer/usr/bin/make  all-recursive
Making all in src
clang -DHAVE_CONFIG_H -I. -I..     -g -O2 -MT main.o -MD -MP -MF .deps/main.Tpo -c -o main.o main.c
mv -f .deps/main.Tpo .deps/main.Po
[…]
make[2]: Nothing to be done for `all-am'.

Schritt 3, make check:

$ make check
Making check in src
make[1]: Nothing to be done for `check'.
Making check in doc
make[1]: Nothing to be done for `check'.
[…]
============================================================================
Testsuite summary for potrace 1.16
============================================================================
# TOTAL: 8
# PASS:  8
# SKIP:  0
# XFAIL: 0
# FAIL:  0
# XPASS: 0
# ERROR: 0
============================================================================
make[1]: Nothing to be done for `check-am'.

Schritt 4, sudo make install:

$ sudo make install
Password:
Making install in src
 .././install-sh -c -d '/usr/local/bin'
  /bin/sh ../libtool   --mode=install /usr/bin/install -c potrace mkbitmap '/usr/local/bin'
[…]
make[2]: Nothing to be done for `install-data-am'.

Um zu prüfen, ob es funktioniert hat, führen Sie mkbitmap --version aus:

$ mkbitmap --version
mkbitmap 1.16. Copyright (C) 2001-2019 Peter Selinger.

Wenn Sie die Versionsdetails abrufen, haben Sie mkbitmap erfolgreich kompiliert und installiert. Stellen Sie als Nächstes sicher, dass das Äquivalent dieser Schritte mit WebAssembly funktioniert.

mkbitmap in WebAssembly kompilieren

Emscripten ist ein Tool zum Kompilieren von C/C++-Programmen in WebAssembly. In der Dokumentation zu Gebäudeprojekten von Emscripten steht Folgendes:

Das Erstellen großer Projekte mit Emscripten ist denkbar einfach. Emscripten stellt zwei einfache Skripts bereit, mit denen Sie Ihre Makefiles so konfigurieren, dass emcc als Drop-in-Ersatz für gcc verwendet wird. In den meisten Fällen bleibt der Rest des aktuellen Build-Systems Ihres Projekts unverändert.

Weiter geht es mit der Dokumentation (zur Kürze halber überarbeitet):

Stellen Sie sich vor, Sie erstellen Builds normalerweise mit den folgenden Befehlen:

./configure
make

Zum Erstellen mit Emscripten würden Sie stattdessen die folgenden Befehle verwenden:

emconfigure ./configure
emmake make

Aus ./configure wird also emconfigure ./configure und aus make wird emmake make. Im Folgenden wird gezeigt, wie dies mit mkbitmap geht.

Schritt 0, make clean:

$ make clean
Making clean in src
 rm -f potrace mkbitmap
test -z "" || rm -f
rm -rf .libs _libs
[…]
rm -f *.lo

Schritt 1, emconfigure ./configure:

$ emconfigure ./configure
configure: ./configure
checking for a BSD-compatible install... /usr/bin/install -c
checking whether build environment is sane... yes
checking for a thread-safe mkdir -p... ./install-sh -c -d
checking for gawk... no
checking for mawk... no
checking for nawk... no
checking for awk... awk
[…]
config.status: executing libtool commands

Schritt 2, emmake make:

$ emmake make
make: make
/Applications/Xcode.app/Contents/Developer/usr/bin/make  all-recursive
Making all in src
/opt/homebrew/Cellar/emscripten/3.1.36/libexec/emcc -DHAVE_CONFIG_H -I. -I..     -g -O2 -MT main.o -MD -MP -MF .deps/main.Tpo -c -o main.o main.c
mv -f .deps/main.Tpo .deps/main.Po
[…]
make[2]: Nothing to be done for `all'.

Wenn alles gut gelaufen ist, sollten sich im Verzeichnis jetzt .wasm-Dateien befinden. Sie können sie mit dem Befehl find . -name "*.wasm" abrufen:

$ find . -name "*.wasm"
./a.wasm
./src/mkbitmap.wasm
./src/potrace.wasm

Die beiden letzten Vorschläge sehen vielversprechend aus, also cd in das src/-Verzeichnis. Außerdem gibt es jetzt zwei neue entsprechende Dateien: mkbitmap und potrace. Für diesen Artikel ist nur mkbitmap relevant. Die Tatsache, dass sie nicht die Erweiterung .js haben, ist etwas verwirrend, aber es handelt sich tatsächlich um JavaScript-Dateien, die mit einem kurzen head-Aufruf überprüft werden können:

$ cd src/
$ head -n 20 mkbitmap
// include: shell.js
// The Module object: Our interface to the outside world. We import
// and export values on it. There are various ways Module can be used:
// 1. Not defined. We create it here
// 2. A function parameter, function(Module) { ..generated code.. }
// 3. pre-run appended it, var Module = {}; ..generated code..
// 4. External script tag defines var Module.
// We need to check if Module already exists (e.g. case 3 above).
// Substitution will be replaced with actual code on later stage of the build,
// this way Closure Compiler will not mangle it (e.g. case 4. above).
// Note that if you want to run closure, and also to use Module
// after the generated code, you will need to define   var Module = {};
// before the code. Then that object will be used in the code, and you
// can continue to use Module afterwards as well.
var Module = typeof Module != 'undefined' ? Module : {};

// --pre-jses are emitted after the Module integration code, so that they can
// refer to Module (if they choose; they can also define Module)

Benennen Sie die JavaScript-Datei in mkbitmap.js um, indem Sie mv mkbitmap mkbitmap.js aufrufen (bzw. mv potrace potrace.js). Jetzt können Sie mit dem ersten Test herausfinden, ob alles funktioniert hat. Führen Sie dazu die Datei mit Node.js in der Befehlszeile aus, indem Sie node mkbitmap.js --version ausführen:

$ node mkbitmap.js --version
mkbitmap 1.16. Copyright (C) 2001-2019 Peter Selinger.

Sie haben mkbitmap erfolgreich in WebAssembly kompiliert. Im nächsten Schritt muss es im Browser funktionieren.

mkbitmap mit WebAssembly im Browser

Kopieren Sie die Dateien mkbitmap.js und mkbitmap.wasm in ein neues Verzeichnis mit dem Namen mkbitmap und erstellen Sie eine index.html-HTML-Boilerplate-Datei, die die JavaScript-Datei mkbitmap.js lädt.

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <title>mkbitmap</title>
  </head>
  <body>
    <script src="mkbitmap.js"></script>
  </body>
</html>

Starten Sie einen lokalen Server, der das Verzeichnis mkbitmap bereitstellt, und öffnen Sie ihn in Ihrem Browser. Nun sollte eine Aufforderung zur Eingabe angezeigt werden. Das ist erwartungsgemäß, da laut der Manpage des Tools „[i]Wenn keine Dateinamenargumente angegeben werden, dann mkbitmap als Filter fungiert und aus der Standardeingabe liest“, was für Emscripten standardmäßig ein prompt() ist.

Die mkbitmap-App mit einer Aufforderung zur Eingabe

Automatische Ausführung verhindern

Um die sofortige Ausführung von mkbitmap zu stoppen und stattdessen auf eine Nutzereingabe zu warten, musst du das Module-Objekt von Emscripten verstehen. Module ist ein globales JavaScript-Objekt mit Attributen, die von Emscripten an verschiedenen Punkten der Ausführung aufgerufen werden. Sie können eine Implementierung von Module bereitstellen, um die Codeausführung zu steuern. Beim Start einer Emscripten-Anwendung werden die Werte im Module-Objekt geprüft und angewendet.

Setzen Sie im Fall von mkbitmap Module.noInitialRun auf true, um die erstmalige Ausführung zu verhindern, die zum Erscheinen der Aufforderung geführt hat. Erstellen Sie ein Skript mit dem Namen script.js, fügen Sie es vor <script src="mkbitmap.js"></script> in index.html ein und fügen Sie den folgenden Code in script.js ein. Wenn Sie jetzt die App neu laden, sollte die Aufforderung verschwinden.

var Module = {
  // Don't run main() at page load
  noInitialRun: true,
};

Modularen Build mit mehr Build-Flags erstellen

Für Eingaben in die App können Sie die Dateisystemunterstützung von Emscripten in Module.FS verwenden. Im Abschnitt Mit Dateisystemunterstützung der Dokumentation wird Folgendes beschrieben:

Emscripten entscheidet, ob die Dateisystemunterstützung automatisch eingebunden wird. Viele Programme benötigen keine Dateien und die Dateisystemunterstützung ist in der Größe nicht zu vernachlässigen. Emscripten vermeidet daher, diese aufzunehmen, wenn es keinen Grund dafür gibt. Wenn dein C/C++-Code also nicht auf Dateien zugreift, werden das FS-Objekt und andere Dateisystem-APIs nicht in die Ausgabe aufgenommen. Wenn Ihr C/C++ Code hingegen Dateien verwendet, wird das Dateisystem automatisch unterstützt.

Leider ist mkbitmap einer der Fälle, in denen Emscripten die Dateisystemunterstützung nicht automatisch unterstützt. Daher müssen Sie es explizit dazu auffordern. Das bedeutet, dass Sie die oben beschriebenen Schritte für emconfigure und emmake ausführen müssen und einige weitere Flags über ein CFLAGS-Argument festgelegt werden. Die folgenden Flags können auch für andere Projekte nützlich sein.

In diesem speziellen Fall müssen Sie außerdem das Flag --host auf wasm32 setzen, um dem configure-Skript mitzuteilen, dass Sie für WebAssembly kompilieren.

Der letzte emconfigure-Befehl sieht so aus:

$ emconfigure ./configure --host=wasm32 CFLAGS='-sFILESYSTEM=1 -sEXPORTED_RUNTIME_METHODS=FS,callMain -sMODULARIZE=1 -sEXPORT_ES6 -sINVOKE_RUN=0'

Vergessen Sie nicht, emmake make noch einmal auszuführen und die neu erstellten Dateien in den Ordner mkbitmap zu kopieren.

Ändern Sie index.html so, dass nur das ES-Modul script.js geladen wird, aus dem Sie dann das mkbitmap.js-Modul importieren.

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <title>mkbitmap</title>
  </head>
  <body>
    <!-- No longer load `mkbitmap.js` here -->
    <script src="script.js" type="module"></script>
  </body>
</html>
// This is `script.js`.
import loadWASM from './mkbitmap.js';

const run = async () => {
  const Module = await loadWASM();
  console.log(Module);
};

run();

Wenn du die App jetzt im Browser öffnest, sollte das Module-Objekt in der Entwicklertools-Konsole protokolliert werden. Die Aufforderung wird nicht mehr angezeigt, da die main()-Funktion von mkbitmap zu Beginn nicht mehr aufgerufen wird.

Die mkbitmap-App mit einem weißen Bildschirm, auf dem das in der Entwicklertools-Konsole protokollierte Modulobjekt zu sehen ist.

Hauptfunktion manuell ausführen

Im nächsten Schritt rufen Sie die main()-Funktion von mkbitmap manuell auf, indem Sie Module.callMain() ausführen. Für die Funktion callMain() wird ein Array von Argumenten verwendet, die nacheinander entsprechen, was Sie in der Befehlszeile übergeben würden. Wenn Sie in der Befehlszeile mkbitmap -v ausführen, rufen Sie Module.callMain(['-v']) im Browser auf. Dadurch wird die mkbitmap-Versionsnummer in der Entwicklertools-Konsole protokolliert.

// This is `script.js`.
import loadWASM from './mkbitmap.js';

const run = async () => {
  const Module = await loadWASM();
  Module.callMain(['-v']);
};

run();

Die mkbitmap-App mit einem weißen Bildschirm, auf dem die in der Entwicklertools-Konsole protokollierte mkbitmap-Versionsnummer zu sehen ist.

Standardausgabe weiterleiten

Die Standardausgabe (stdout) ist standardmäßig die Konsole. Sie können sie jedoch an etwas anderes weiterleiten, z. B. eine Funktion, die die Ausgabe in einer Variablen speichert. Das bedeutet, dass du die Ausgabe in den HTML-Code einfügen kannst, indem du das Attribut Module.print festlegst.

// This is `script.js`.
import loadWASM from './mkbitmap.js';

const run = async () => {
  let consoleOutput = 'Powered by ';
  const Module = await loadWASM({
    print: (text) => (consoleOutput += text),
  });
  Module.callMain(['-v']);
  document.body.textContent = consoleOutput;
};

run();

Die mkbitmap-App mit der mkbitmap-Versionsnummer

Eingabedatei in das Speicherdateisystem laden

Um die Eingabedatei in das Speicherdateisystem zu übertragen, benötigen Sie das Äquivalent zu mkbitmap filename in der Befehlszeile. Um zu verstehen, wie ich an dieses Thema herangeht, möchte ich zuerst ein paar Hintergrundinformationen darüber erhalten, wie mkbitmap die Eingabe erwartet und die Ausgabe erstellt.

Unterstützte Eingabeformate von mkbitmap sind PNM (PBM, PGM, PPM) und BMP. Die Ausgabeformate sind PBM für Bitmaps und PGM für Graymaps. Wenn ein filename-Argument angegeben wird, erstellt mkbitmap standardmäßig eine Ausgabedatei, deren Name aus dem Namen der Eingabedatei abgerufen wird, indem das Suffix in .pbm geändert wird. Für den Namen der Eingabedatei example.bmp wäre der Name der Ausgabedatei beispielsweise example.pbm.

Emscripten bietet ein virtuelles Dateisystem, das das lokale Dateisystem simuliert, sodass nativer Code mit synchronen Datei-APIs mit wenig oder gar keiner Änderung kompiliert und ausgeführt werden kann. Damit mkbitmap eine Eingabedatei so lesen kann, als wäre sie als filename-Befehlszeilenargument übergeben worden, müssen Sie das von Emscripten bereitgestellte FS-Objekt verwenden.

Das FS-Objekt wird von einem speicherinternen Dateisystem (allgemein als MEMFS bezeichnet) unterstützt und hat eine writeFile()-Funktion, mit der Sie Dateien in das virtuelle Dateisystem schreiben. Sie verwenden writeFile(), wie im folgenden Codebeispiel gezeigt.

Prüfen Sie, ob der Dateischreibvorgang funktioniert hat, indem Sie die Funktion readdir() des FS-Objekts mit dem Parameter '/' ausführen. Sie sehen example.bmp und eine Reihe von Standarddateien, die immer automatisch erstellt werden.

Beachten Sie, dass der vorherige Aufruf von Module.callMain(['-v']) zum Drucken der Versionsnummer entfernt wurde. Das liegt daran, dass Module.callMain() eine Funktion ist, die normalerweise erwartet, dass sie nur einmal ausgeführt wird.

// This is `script.js`.
import loadWASM from './mkbitmap.js';

const run = async () => {
  const Module = await loadWASM();
  const buffer = await fetch('https://example.com/example.bmp').then((res) => res.arrayBuffer());
  Module.FS.writeFile('example.bmp', new Uint8Array(buffer));
  console.log(Module.FS.readdir('/'));
};

run();

Die Anwendung „mkbitmap“ mit einem Array von Dateien im Dateisystem des Arbeitsspeichers, einschließlich „beispiel.bmp“.

Erste tatsächliche Ausführung

Wenn alles eingerichtet ist, führen Sie mkbitmap mit Module.callMain(['example.bmp']) aus. Protokollieren Sie den Inhalt des Ordners '/' im MEMFS-Ordner. Die neu erstellte example.pbm-Ausgabedatei sollte neben der Eingabedatei example.bmp angezeigt werden.

// This is `script.js`.
import loadWASM from './mkbitmap.js';

const run = async () => {
  const Module = await loadWASM();
  const buffer = await fetch('https://example.com/example.bmp').then((res) => res.arrayBuffer());
  Module.FS.writeFile('example.bmp', new Uint8Array(buffer));
  Module.callMain(['example.bmp']);
  console.log(Module.FS.readdir('/'));
};

run();

Die App „mkbitmap“ mit einem Array von Dateien im Dateisystem des Arbeitsspeichers, einschließlich „beispiel.bmp“ und „beispiel.pbm“.

Ausgabedatei aus dem Speicherdateisystem holen

Mit der Funktion readFile() des FS-Objekts kann die im letzten Schritt erstellte example.pbm aus dem Speicherdateisystem abgerufen werden. Die Funktion gibt ein Uint8Array zurück, das Sie in ein File-Objekt konvertieren und auf dem Laufwerk speichern, da Browser PBM-Dateien für die direkte Anzeige im Browser nicht unterstützen. Es gibt elegantere Möglichkeiten, eine Datei zu speichern, die am weitesten verbreitete Methode ist jedoch die Verwendung eines dynamisch erstellten <a download>. Sobald die Datei gespeichert ist, können Sie sie in Ihrem bevorzugten Bildanzeigeprogramm öffnen.

// This is `script.js`.
import loadWASM from './mkbitmap.js';

const run = async () => {
  const Module = await loadWASM();
  const buffer = await fetch('https://example.com/example.bmp').then((res) => res.arrayBuffer());
  Module.FS.writeFile('example.bmp', new Uint8Array(buffer));
  Module.callMain(['example.bmp']);
  const output = Module.FS.readFile('example.pbm', { encoding: 'binary' });
  const file = new File([output], 'example.pbm', {
    type: 'image/x-portable-bitmap',
  });
  const a = document.createElement('a');
  a.href = URL.createObjectURL(file);
  a.download = file.name;
  a.click();
};

run();

macOS Finder mit einer Vorschau der BMP-Eingabedatei und der PBM-Ausgabedatei.

Interaktive UI hinzufügen

Bis jetzt ist die Eingabedatei hartcodiert und mkbitmap wird mit Standardparametern ausgeführt. Im letzten Schritt kann der Nutzer eine Eingabedatei dynamisch auswählen, die mkbitmap-Parameter optimieren und dann das Tool mit den ausgewählten Optionen ausführen.

// Corresponds to `mkbitmap -o output.pbm input.bmp -s 8 -3 -f 4 -t 0.45`.
Module.callMain(['-o', 'output.pbm', 'input.bmp', '-s', '8', '-3', '-f', '4', '-t', '0.45']);

Das PBM-Bildformat ist nicht besonders schwer zu parsen, sodass Sie mit JavaScript-Code sogar eine Vorschau des Ausgabebilds anzeigen können. Eine Möglichkeit dazu finden Sie im Quellcode der eingebetteten Demo unten.

Fazit

Herzlichen Glückwunsch! Sie haben mkbitmap erfolgreich in WebAssembly kompiliert und dies im Browser funktioniert. Es gab einige Sackgassen und Sie mussten das Tool mehrmals kompilieren, bis es funktionierte, aber wie ich bereits geschrieben habe, gehört das zum Erlebnis. Merken Sie sich auch das webassembly-Tag von StackOverflow, wenn Sie nicht weiterkommen. Viel Spaß beim Zusammenstellen!

Danksagungen

Dieser Artikel wurde von Sam Clegg und Rachel Andrew gelesen.