Mkbitmap zu WebAssembly kompilieren

Im Abschnitt Was ist WebAssembly und woher kommt es? Ich habe erklärt, wie wir zu dem heutigen WebAssembly gekommen sind. In diesem Artikel zeige ich Ihnen, wie ich ein vorhandenes C-Programm, mkbitmap, in WebAssembly kompiliere. Es ist komplexer als das Hello World-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 genug, um Sie nicht zu überfordern.

Dieser Artikel richtet sich an Webentwickler, die WebAssembly kennenlernen möchten. Er zeigt Schritt für Schritt, wie Sie vorgehen können, wenn Sie etwas wie mkbitmap in WebAssembly kompilieren möchten. Es ist ganz normal, wenn eine App oder Bibliothek beim ersten Versuch nicht kompiliert wird. Deshalb haben einige der unten beschriebenen Schritte nicht funktioniert. Ich musste also einen Schritt zurückgehen und es noch einmal anders versuchen. Im Artikel wird der magische endgültige Kompilierungsbefehl nicht so dargestellt, als wäre er vom Himmel gefallen, sondern es wird mein tatsächlicher Fortschritt beschrieben, einschließlich einiger Frustrationen.

Über mkbitmap

Das C-Programm mkbitmap liest ein Bild und wendet in dieser Reihenfolge einen oder mehrere der folgenden Vorgänge darauf an: Invertieren, Hochpassfilterung, Skalierung und Schwellenwertbildung. Jeder Vorgang kann einzeln gesteuert und aktiviert oder deaktiviert werden. Die Hauptanwendung von mkbitmap besteht darin, Farb- oder Graustufenbilder in ein Format zu konvertieren, das als Eingabe für andere Programme geeignet ist, insbesondere für das Tracing-Programm potrace, das die Grundlage von SVGcode bildet. Als Vorverarbeitungstool eignet sich mkbitmap besonders gut, um gescannte Strichzeichnungen wie Cartoons oder handschriftlichen Text in hochauflösende zweifarbige Bilder zu konvertieren.

Sie verwenden mkbitmap, indem Sie eine Reihe von Optionen und einen oder mehrere Dateinamen übergeben. Alle Details finden Sie auf der Manpage des Tools:

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

Code abrufen

Der erste Schritt besteht darin, den Quellcode von mkbitmap zu beziehen. Sie finden sie auf der Website des Projekts. Zum Zeitpunkt der Erstellung dieses Dokuments ist potrace-1.16.tar.gz die aktuelle Version.

Lokal kompilieren und installieren

Als Nächstes kompilieren und installieren Sie das Tool lokal, 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 Sie ./configure ein, um das Paket für Ihr System zu konfigurieren.

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

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

  3. Geben Sie optional make check ein, um alle Selbsttests auszuführen, die im Paket enthalten sind. Dabei werden in der Regel die gerade erstellten, nicht installierten Binärdateien verwendet.

  4. Geben Sie make install ein, um die Programme sowie alle Datendateien und die Dokumentation zu installieren. Wenn Sie in einem Präfix installieren, das dem Root-Nutzer gehört, empfiehlt es sich, das Paket als regulärer Nutzer zu konfigurieren und zu erstellen und nur die make install-Phase mit Root-Berechtigungen auszuführen.

Wenn Sie diese Schritte ausführen, sollten Sie zwei ausführbare Dateien erhalten: potrace und mkbitmap. Letztere ist das Thema dieses Artikels. Sie können prüfen, ob alles richtig funktioniert hat, indem Sie mkbitmap --version ausführen. Hier ist die Ausgabe aller vier Schritte von meinem Computer, stark gekürzt, um sie kurz zu halten:

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 es funktioniert hat:

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

Wenn Sie die Versionsdetails erhalten, haben Sie mkbitmap erfolgreich kompiliert und installiert. Als Nächstes müssen Sie dafür sorgen, dass diese Schritte mit WebAssembly funktionieren.

mkbitmap in WebAssembly kompilieren

Emscripten ist ein Tool zum Kompilieren von C/C++-Programmen in WebAssembly. In der Emscripten-Dokumentation zum Erstellen von Projekten wird Folgendes angegeben:

Mit Emscripten lassen sich große Projekte ganz einfach erstellen. Emscripten bietet zwei einfache Skripts, mit denen Sie Ihre Makefiles so konfigurieren können, dass emcc als Ersatz für gcc verwendet wird. In den meisten Fällen bleibt das aktuelle Build-System Ihres Projekts unverändert.

In der Dokumentation heißt es dann (etwas gekürzt):

Angenommen, Sie erstellen normalerweise mit den folgenden Befehlen:

./configure
make

Um mit Emscripten zu erstellen, verwenden Sie stattdessen die folgenden Befehle:

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 gut 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 letzten beiden sehen vielversprechend aus. Wechseln Sie also mit 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. Dass sie nicht die Erweiterung .js 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) aufrufen. Führen Sie die Datei mit Node.js in der Befehlszeile aus, indem Sie node mkbitmap.js --version eingeben, um zu prüfen, ob alles funktioniert:

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

Sie haben mkbitmap erfolgreich in WebAssembly kompiliert. Als Nächstes muss die Funktion im Browser funktionieren.

mkbitmap mit WebAssembly im Browser

Kopieren Sie die Dateien mkbitmap.js und mkbitmap.wasm in ein neues Verzeichnis namens mkbitmap und erstellen Sie eine index.html-HTML-Boilerplate-Datei, in der die JavaScript-Datei mkbitmap.js geladen wird.

<!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 zur Eingabe sehen. Das ist zu erwarten, da laut der Manpage des Tools „[i]f no filename arguments are given, then mkbitmap acts as a filter, reading from standard input“ (wenn keine Dateinamenargumente angegeben werden, fungiert mkbitmap als Filter und liest aus der Standardeingabe), was für Emscripten standardmäßig ein prompt() ist.

Die mkbitmap-App mit einer Aufforderung zur Eingabe.

Automatische Ausführung verhindern

Damit mkbitmap nicht sofort ausgeführt wird, sondern auf die Eingabe des Nutzers wartet, müssen Sie das Module-Objekt von Emscripten verstehen. Module ist ein globales JavaScript-Objekt mit Attributen, die vom Emscripten-generierten Code an verschiedenen Stellen während der Ausführung aufgerufen werden. Sie können eine Implementierung von Module bereitstellen, um die Ausführung von Code 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 den ersten Lauf zu verhindern, der dazu geführt hat, dass der Prompt angezeigt wurde. 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 den folgenden Code in script.js ein. 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 weiteren Build-Flags erstellen

Um der App Eingaben zu geben, können Sie die Dateisystemunterstützung von Emscripten in Module.FS verwenden. Im Abschnitt Unterstützung von Dateisystemen der Dokumentation heißt es:

Emscripten entscheidet, ob die Dateisystemunterstützung automatisch einbezogen wird. Viele Programme benötigen keine Dateien. Die Unterstützung des Dateisystems ist jedoch nicht unerheblich. Daher wird sie von Emscripten nicht einbezogen, wenn es keinen Grund dafür gibt. 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 für das Dateisystem automatisch eingeschlossen.

Leider ist mkbitmap einer der Fälle, in denen Emscripten nicht automatisch Unterstützung für das Dateisystem enthält. Sie müssen es also explizit anweisen, dies zu tun. Das bedeutet, dass Sie die zuvor beschriebenen Schritte emconfigure und emmake ausführen müssen, wobei einige weitere Flags über ein CFLAGS-Argument festgelegt werden. Die folgenden Flags können auch für andere Projekte nützlich sein.

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

Der endgültige 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 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. Die Aufforderung wird nicht mehr angezeigt, da die main()-Funktion von mkbitmap nicht mehr am Anfang aufgerufen wird.

Die mkbitmap-App mit einem weißen Bildschirm, auf dem das Modulobjekt in der Entwicklertools-Konsole protokolliert wird.

Hauptfunktion manuell ausführen

Der nächste Schritt besteht darin, die main()-Funktion von mkbitmap manuell aufzurufen, indem Sie Module.callMain() ausführen. Die Funktion callMain() verwendet ein Array von Argumenten, die eins zu eins den Argumenten entsprechen, die 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 Versionsnummer mkbitmap 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 mkbitmap-Versionsnummer in der DevTools-Konsole protokolliert wird.

Standardausgabe umleiten

Die Standardausgabe (stdout) ist standardmäßig die Konsole. Sie können die Ausgabe jedoch an etwas anderes weiterleiten, z. B. an eine Funktion, die die Ausgabe in einer Variablen speichert. Das bedeutet, dass Sie die Ausgabe dem HTML hinzufügen können, indem Sie die Property 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 mkbitmap-Versionsnummer.

Eingabedatei in das In-Memory-Dateisystem übertragen

Damit die Eingabedatei in das Speicherdateisystem gelangt, benötigen Sie das Äquivalent von mkbitmap filename in der Befehlszeile. Um zu verstehen, wie ich vorgehe, ist es hilfreich, sich erst einmal anzusehen, 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 Graustufenkarten. 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 ist, lautet der Ausgabedateiname example.pbm.

Emscripten bietet ein virtuelles Dateisystem, das das lokale Dateisystem simuliert, sodass nativer Code, der synchrone Datei-APIs verwendet, mit wenig 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 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 können. 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 Ausgeben 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 mkbitmap-App zeigt ein Array von Dateien im Arbeitsspeicher-Dateisystem an, einschließlich „example.bmp“.

Erste tatsächliche Ausführung

Führen Sie mkbitmap aus, indem Sie Module.callMain(['example.bmp']) ausführen. Protokollieren Sie den Inhalt des MEMFS-Ordners '/'. Neben der Eingabedatei example.bmp sollte die neu erstellte Ausgabedatei example.pbm 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 mkbitmap-App zeigt ein Array von Dateien im Arbeitsspeicher-Dateisystem an, darunter „example.bmp“ und „example.pbm“.

Ausgabedatei aus dem Arbeitsspeicher-Dateisystem abrufen

Mit der Funktion readFile() des FS-Objekts kann die im letzten Schritt erstellte example.pbm aus dem In-Memory-Dateisystem abgerufen werden. Die Funktion gibt ein Uint8Array zurück, das Sie in ein File-Objekt umwandeln und auf der Festplatte speichern. Browser unterstützen PBM-Dateien in der Regel nicht für die direkte Anzeige im Browser. Es gibt elegantere Möglichkeiten, eine Datei zu speichern, aber die Verwendung eines dynamisch erstellten <a download> ist die am weitesten unterstützte. Nachdem die Datei gespeichert wurde, können Sie sie in Ihrem bevorzugten Bildbetrachtungsprogramm ö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 fest codiert und mkbitmap wird mit Standardparametern ausgeführt. Im letzten Schritt kann der Nutzer dynamisch eine Eingabedatei auswählen, die mkbitmap-Parameter anpassen und das Tool dann 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. Mit etwas JavaScript-Code können Sie sogar eine Vorschau des Ausgabebilds anzeigen. Eine Möglichkeit hierzu finden Sie im Quellcode der eingebetteten Demo unten.

Fazit

Sie haben mkbitmap erfolgreich in WebAssembly kompiliert und im Browser zum Laufen gebracht. Es gab einige Sackgassen und ich musste das Tool mehrmals kompilieren, bis es funktionierte, aber wie oben beschrieben, gehört das dazu. Denken Sie auch an das StackOverflow-Tag webassembly, falls Sie nicht weiterkommen. Viel Spaß beim Erstellen!

Danksagungen

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