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, wie ich ein vorhandenes C-Programm, mkbitmap, in WebAssembly kompiliere. Es ist komplexer als das Hallo-Welt-Beispiel, da es die Arbeit mit Dateien, die Kommunikation zwischen WebAssembly und JavaScript sowie das Zeichnen auf einem Canvas umfasst. Es ist aber immer noch überschaubar, damit Sie nicht überfordert werden.

Der Artikel richtet sich an Webentwickler, die WebAssembly kennenlernen möchten, und eine detaillierte Anleitung, wie du vorgehen kannst, wenn du etwas wie mkbitmap in WebAssembly kompilieren möchtest. Zur Information: Es ist völlig normal, dass eine App oder Bibliothek beim ersten Ausführen nicht kompiliert werden kann. Aus diesem Grund funktionierten einige der unten beschriebenen Schritte nicht. Ich musste also zurückgehen und es auf eine andere Weise versuchen. Im Artikel wird nicht der magische Befehl für die endgültige Kompilierung beschrieben, als wäre er vom Himmel gefallen, sondern es werden meine tatsächlichen Fortschritte beschrieben, einschließlich einiger 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 Schwellenwertbestimmung. Jeder Vorgang kann einzeln gesteuert und aktiviert oder deaktiviert werden. mkbitmap wird hauptsächlich verwendet, um Farb- oder Graustufenbilder in ein Format umzuwandeln, das als Eingabe für andere Programme geeignet ist, insbesondere für das Tracing-Programm potrace, das die Grundlage für 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 ihm eine Reihe von Optionen und einen oder mehrere Dateinamen übergeben. Weitere Informationen finden Sie in der Manpage des Tools:

$ mkbitmap [options] [filename...]
Zeichentrickbild in Farbe.
Das Originalbild (Quelle).
Cartoonbild, das nach der Vorverarbeitung in Graustufen umgewandelt wurde.
Zuerst skaliert, dann Grenzwert angewendet: mkbitmap -f 2 -s 2 -t 0.48 (Quelle).

Code abrufen

Im ersten Schritt müssen Sie den Quellcode von mkbitmap abrufen. Sie finden sie auf der Website des Projekts. Zum Zeitpunkt der Erstellung dieses Artikels ist potrace-1.16.tar.gz die neueste Version.

Kompilieren und lokal installieren

Im nächsten Schritt kompilieren und installieren Sie das Tool lokal, um ein Gefühl für sein Verhalten zu bekommen. 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.

    Die Ausführung von configure kann einige Zeit 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. Wenn Sie in einem Prefix installieren, das zu „root“ gehört, wird empfohlen, das Paket als normaler Nutzer zu konfigurieren und zu erstellen und nur die Phase make install mit Root-Berechtigungen auszuführen.

Wenn Sie diese Schritte ausführen, sollten Sie am Ende zwei ausführbare Dateien haben, potrace und mkbitmap. Letztere steht im Mittelpunkt dieses Artikels. Sie können prüfen, ob er ordnungsgemäß funktioniert hat, indem Sie mkbitmap --version ausführen. Hier ist die Ausgabe aller vier Schritte auf meinem Computer, stark gekürzt:

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'.

Führen Sie mkbitmap --version aus, um zu prüfen, ob die Änderung übernommen wurde:

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

Wenn Sie die Versionsdetails sehen, wurde 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 zum Erstellen von Projekten von Emscripten wird Folgendes angegeben:

Das Erstellen großer Projekte mit Emscripten ist sehr einfach. Emscripten bietet zwei einfache Scripts, mit denen Ihre Makefiles so konfiguriert werden, dass emcc als Drop-in-Ersatz für gcc verwendet wird. In den meisten Fällen bleibt das restliche Buildsystem Ihres Projekts unverändert.

Die Dokumentation geht dann so weiter (etwas gekürzt):

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

./configure wird also zu emconfigure ./configure und make zu emmake make. Im Folgenden wird gezeigt, wie das mit mkbitmap funktioniert.

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 richtig gelaufen ist, sollten sich jetzt irgendwo im Verzeichnis .wasm-Dateien befinden. Sie können sie durch Ausführen von find . -name "*.wasm" finden:

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

Die beiden letzten sehen vielversprechend aus. Daher cd in das Verzeichnis src/. 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 .js-Erweiterung haben, ist etwas verwirrend, aber es handelt sich tatsächlich um JavaScript-Dateien, was sich mit einem kurzen head-Aufruf überprüfen lässt:

$ 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 (und mv potrace potrace.js, falls gewünscht) aufrufen. Jetzt können Sie mit dem ersten Test herausfinden, ob es 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 sorgen wir dafür, dass es im Browser funktioniert.

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 es in Ihrem Browser. Sie sollten eine Aufforderung sehen, in der Sie um Eingabe gebeten werden. Dies 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

Wenn Sie verhindern möchten, dass mkbitmap sofort ausgeführt wird, sondern stattdessen auf Nutzereingaben warten soll, müssen Sie das Module-Objekt von Emscripten kennen. Module ist ein globales JavaScript-Objekt mit Attributen, die von Emscripten-generiertem Code an verschiedenen Stellen während der Ausführung aufgerufen werden. Sie können eine Implementierung von Module bereitstellen, um die Codeausführung zu steuern. Wenn eine Emscripten-Anwendung gestartet wird, 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 Script mit dem Namen script.js, fügen Sie es vor dem <script src="mkbitmap.js"></script> in index.html ein und fügen Sie script.js den folgenden Code hinzu. Wenn Sie die App jetzt neu laden, sollte die Aufforderung nicht mehr angezeigt werden.

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

Modularen Build mit einigen weiteren Build-Flags erstellen

Sie können die Dateisystemunterstützung von Emscripten in Module.FS verwenden, um Eingaben für die App bereitzustellen. Im Abschnitt Unterstützung für Dateisysteme einbinden der Dokumentation wird Folgendes angegeben:

Emscripten entscheidet automatisch, ob die Dateisystemunterstützung eingeschlossen werden soll. Viele Programme benötigen keine Dateien und die Unterstützung des Dateisystems ist nicht zu vernachlässigen. Daher wird sie von Emscripten nicht eingeschlossen, wenn kein Grund dafür besteht. Wenn Ihr 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 die Unterstützung des Dateisystems automatisch eingebunden.

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 Fall müssen Sie außerdem das Flag --host auf wasm32 setzen, um dem configure-Script mitzuteilen, dass Sie für WebAssembly kompilieren.

Der vollständige emconfigure-Befehl sieht so aus:

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

Denken Sie daran, 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 Modul mkbitmap.js 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 Sie die App jetzt im Browser öffnen, sollte das Module-Objekt in der DevTools-Konsole protokolliert werden und die Aufforderung nicht mehr angezeigt werden, da die main()-Funktion von mkbitmap nicht mehr zu Beginn aufgerufen wird.

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

Hauptfunktion manuell ausführen

Im nächsten Schritt wird die main()-Funktion von mkbitmap manuell aufgerufen, indem Module.callMain() ausgeführt wird. 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 würden, würden Sie im Browser Module.callMain(['-v']) aufrufen. Dadurch wird die mkbitmap-Versionsnummer in der DevTools-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 DevTools-Konsole protokollierte Versionsnummer von mkbitmap zu sehen ist

Standardausgabe umleiten

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

// 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 Versionsnummer von mkbitmap

Eingabedatei in das In-Memory-Dateisystem aufnehmen

Um die Eingabedatei in das Speicherdateisystem zu übertragen, benötigen Sie das Äquivalent von mkbitmap filename in der Befehlszeile. Damit Sie verstehen, wie ich vorgehen werde, möchte ich Ihnen zuerst erklären, wie mkbitmap seine Eingabe erwartet und seine Ausgabe erstellt.

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

Emscripten bietet ein virtuelles Dateisystem, das das lokale Dateisystem simuliert, sodass nativer Code mithilfe von synchronen Datei-APIs mit nur wenigen oder gar keinen Änderungen kompiliert und ausgeführt werden kann. Damit mkbitmap eine Eingabedatei so liest, als wäre sie als filename-Befehlszeilenargument übergeben worden, müssen Sie das von Emscripten bereitgestellte Objekt FS 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.

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

Der vorherige Aufruf von Module.callMain(['-v']) zum Drucken der Versionsnummer wurde entfernt. Das liegt daran, dass Module.callMain() eine Funktion ist, die in der Regel nur einmal ausgeführt werden soll.

// 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. Wenn Sie den Inhalt des '/'-Ordners des MEMFS protokollieren, sollte die neu erstellte example.pbm-Ausgabedatei neben der example.bmp-Eingabedatei 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“ zeigt eine Reihe von Dateien im Speicherdateisystem an, darunter „beispiel.bmp“ und „beispiel.pbm“.

Ausgabedatei aus dem Speicherdateisystem abrufen

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 in der Regel nicht für die direkte Anzeige im Browser 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>. Nachdem Sie die Datei gespeichert haben, können Sie sie in Ihrer bevorzugten Bildanzeige ö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 Eingabe-BMP-Datei und der Ausgabe-PBM-Datei

Interaktive Benutzeroberfläche hinzufügen

Bisher ist die Eingabedatei hartcodiert und mkbitmap wird mit den Standardparametern ausgeführt. Im letzten Schritt können Sie dem Nutzer ermöglichen, eine Eingabedatei dynamisch auszuwählen, die mkbitmap-Parameter anzupassen und das Tool dann mit den ausgewählten Optionen auszufü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. Mit ein wenig JavaScript-Code können Sie sogar eine Vorschau des Ausgabebilds anzeigen. Eine Möglichkeit dazu finden Sie im Quellcode der eingebetteten Demo unten.

Fazit

Herzlichen Glückwunsch! Sie haben mkbitmap erfolgreich in WebAssembly kompiliert und im Browser ausgeführt. Es gab einige Sackgassen und Sie mussten das Tool mehrmals kompilieren, bis es funktionierte. Wie ich oben schon schrieb, gehört das aber dazu. Denken Sie auch an das webassembly-Tag von StackOverflow, wenn Sie nicht weiterkommen. Viel Spaß beim Erstellen!

Danksagungen

Dieser Artikel wurde von Sam Clegg und Rachel Andrew geprüft.