Mkbitmap zu WebAssembly kompilieren

Unter Was ist WebAssembly und woher es kommt?, Ich habe erklärt, wie wir schließlich zur WebAssembly von heute gekommen sind. In diesem Artikel zeige ich Ihnen, wie ich ein bestehendes C-Programm (mkbitmap) zu WebAssembly kompilieren kann. Es ist komplexer als das Hello World-Beispiel, da es das Arbeiten mit Dateien, die Kommunikation zwischen WebAssembly und JavaScript sowie das Zeichnen auf einem Canvas umfasst. Es ist aber dennoch übersichtlich genug, um Sie nicht zu überfordern.

Der Artikel richtet sich an Webentwickler, die WebAssembly kennenlernen möchten, und zeigt Schritt für Schritt, wie du vorgehen kannst, wenn du etwas wie mkbitmap in WebAssembly kompilieren möchtest. Wir möchten Sie darauf hinweisen, dass es völlig normal ist, dass eine App oder Bibliothek bei der ersten Ausführung nicht kompiliert werden muss. Aus diesem Grund haben einige der unten beschriebenen Schritte nicht funktioniert. Daher musste ich den Vorgang rückgängig machen und es noch einmal versuchen. Der Artikel zeigt nicht den magischen abschließenden Kompilierungsbefehl, als ob er aus dem Himmel gefallen wäre, sondern beschreibt meinen tatsächlichen Fortschritt, einschließlich einiger Frustrationen.

Ungefähr mkbitmap

Das C-Programm mkbitmap liest ein Bild und wendet einen oder mehrere der folgenden Vorgänge in dieser Reihenfolge darauf an: Inversion, Hochpassfilter, Skalierung und Schwellenwert. Jeder Vorgang kann individuell gesteuert und aktiviert oder deaktiviert werden. mkbitmap wird hauptsächlich verwendet, um 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 ein Vorverarbeitungstool besonders nützlich, um gescannte Strichzeichnungen, z. B. Cartoons oder handschriftlichen Text, in hochauflösende Bilder auf zwei Ebenen umzuwandeln.

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

$ mkbitmap [options] [filename...]
Zeichentrickbild in Farbe.
Das Originalbild (Quelle).
Zeichentrickbild nach der Vorverarbeitung in Graustufen umgewandelt
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 es auf der Website des Projekts. Zum Zeitpunkt der Veröffentlichung dieses Dokuments 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 für das Verhalten zu bekommen. Die Datei INSTALL enthält die folgende Anleitung:

  1. cd in das Verzeichnis mit dem Quellcode des Pakets 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 gibt sie einige Meldungen aus, die darüber informieren, welche Funktionen geprüft werden.

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

  3. Sie können auch make check eingeben, um alle im Paket enthaltenen Selbsttests auszuführen. Verwenden Sie in der Regel die gerade erstellten deinstallierten Binärprogramme.

  4. Geben Sie make install ein, um die Programme sowie Datendateien und die Dokumentation zu installieren. Bei der Installation in einem Root-Präfix wird empfohlen, das Paket als regulärer 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 zwei ausführbare Dateien haben, potrace und mkbitmap. Letztere wird in diesem Artikel behandelt. Sie können prüfen, ob sie richtig funktioniert, indem Sie mkbitmap --version ausführen. Hier ist die Ausgabe aller vier Schritte meines Computers, der Einfachheit halber 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'.

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 erhalten, haben Sie mkbitmap erfolgreich kompiliert und installiert. Als Nächstes sorgen Sie dafür, 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 in Emscripten steht Folgendes:

Das Erstellen großer Projekte mit Emscripten ist sehr 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. Der Rest des aktuellen Build-Systems Ihres Projekts bleibt in den meisten Fällen unverändert.

Die Dokumentation geht dann weiter (der Kürze halber überarbeitet):

Betrachten Sie den Fall, in dem Sie normalerweise mit den folgenden Befehlen erstellen:

./configure
make

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

emconfigure ./configure
emmake make

Im Wesentlichen wird ./configure also zu emconfigure ./configure und make wird zu emmake make. Im Folgenden wird gezeigt, wie dies 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 im Verzeichnis nun .wasm-Dateien befinden. Sie können sie finden, indem Sie find . -name "*.wasm" ausführen:

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

Die beiden letzten sehen vielversprechend aus, sodass cd in das src/-Verzeichnis eingetragen wird. Außerdem gibt es zwei neue entsprechende Dateien, mkbitmap und potrace. Für diesen Artikel ist nur mkbitmap relevant. Dass sie nicht die Erweiterung .js haben, ist ein wenig 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 bzw. mv potrace potrace.js aufrufen. Jetzt wird beim ersten Test geprüft, ob er funktioniert hat. Dazu führen Sie 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 in WebAssembly kompiliert. Im nächsten Schritt müssen Sie dafür sorgen, dass sie im Browser funktioniert.

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-Standarddatei, 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. Es sollte eine Eingabeaufforderung angezeigt werden. Das ist zu erwarten, da laut der Manpage des Tools „[i]Wenn keine Argumente für den Dateinamen angegeben werden, dann fungiert mkbitmap als Filter und liest aus der Standardeingabe“, was für Emscripten standardmäßig ein prompt() ist.

Mkbitmap-App mit einer Eingabeaufforderung, über die eine Eingabe angefordert wird

Automatische Ausführung verhindern

Wenn Sie die Ausführung von mkbitmap sofort beenden und stattdessen auf Nutzereingaben warten lassen möchten, müssen Sie das Module-Objekt von Emscripten verstehen. Module ist ein globales JavaScript-Objekt mit Attributen, die von Emscripten generierter Code an verschiedenen Stellen während der Ausführung aufgerufen wird. Du kannst eine Implementierung von Module angeben, um die Ausführung von Code zu steuern. Wenn eine Emscripten-Anwendung gestartet wird, prüft sie die Werte im Module-Objekt und wendet sie an.

Legen Sie bei mkbitmap für Module.noInitialRun true fest, um die anfängliche Ausführung zu verhindern, die zum Anzeigen der Eingabeaufforderung 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 in script.js den folgenden Code ein. Wenn Sie die App jetzt aktualisieren, sollte die Aufforderung nicht mehr angezeigt werden.

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

Modularen Build mit mehr Build-Flags erstellen

Zur Eingabe von Eingaben in die App können Sie die Dateisystemunterstützung von Emscripten in Module.FS verwenden. Der Abschnitt Einschließlich Unterstützung für das Dateisystem der Dokumentation besagt:

Emscripten entscheidet, ob die Dateisystemunterstützung automatisch einbezogen wird. Viele Programme benötigen keine Dateien und die Unterstützung des Dateisystems ist nicht zu vernachlässigen. Daher vermeidet Emscripten es, es aufzunehmen, wenn es keinen Grund dafür gibt. Wenn also Ihr C-/C++-Code nicht auf Dateien zugreift, sind das FS-Objekt und andere Dateisystem-APIs nicht in der Ausgabe enthalten. Wenn Ihr C-/C++-Code jedoch 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. Sie müssen dies also explizit mitteilen. Sie müssen also die oben beschriebenen Schritte emconfigure und emmake ausführen und einige weitere Flags über das Argument CFLAGS festlegen. 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 Skript configure 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'

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 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 und die Eingabeaufforderung wird nicht mehr angezeigt, da die main()-Funktion von mkbitmap zu Beginn nicht mehr aufgerufen wird.

Mkbitmap-App mit weißem Bildschirm, auf dem das in der Entwicklertools-Konsole protokollierte Modulobjekt angezeigt wird

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 mit Argumenten verwendet, die genau mit dem übereinstimmen, was Sie in der Befehlszeile übergeben würden. Wenn Sie in der Befehlszeile mkbitmap -v ausführen würden, rufen Sie Module.callMain(['-v']) im Browser auf. Dadurch wird die Versionsnummer von mkbitmap 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();

Mkbitmap-App mit weißem Bildschirm, auf dem die in der Entwicklertools-Konsole protokollierte mkbitmap-Versionsnummer angezeigt wird

Standardausgabe weiterleiten

Die Standardausgabe (stdout) ist standardmäßig die Konsole. Sie können sie jedoch an etwas anderes weiterleiten, z. B. an eine Funktion, mit der die Ausgabe in einer Variablen gespeichert wird. Das bedeutet, dass Sie die Ausgabe in den HTML-Code einfügen können, indem Sie die Module.print-Eigenschaft 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();

Mkbitmap-Anwendung mit der mkbitmap-Versionsnummer

Eingabedatei in das Speicherdateisystem abrufen

Um die Eingabedatei in das Speicherdateisystem zu erhalten, müssen Sie das Äquivalent von mkbitmap filename in der Befehlszeile benötigen. Damit ich besser verstehen kann, wie ich dabei angehe, habe ich einige Hintergrundinformationen dazu, wie mkbitmap die Eingabe erwartet und die 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 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 lautet der Name der Ausgabedatei beispielsweise example.pbm.

Emscripten bietet ein virtuelles Dateisystem, das das lokale Dateisystem simuliert, sodass nativer Code, der synchrone Datei-APIs verwendet, kompiliert und mit geringen oder gar keiner Änderung 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 (häufig 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.

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

Hinweis: Der vorherige Aufruf von Module.callMain(['-v']) zum Drucken der Versionsnummer wurde entfernt. Das liegt daran, dass die Funktion Module.callMain() normalerweise 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();

Mkbitmap-App mit einem Array von Dateien im Speicherdateisystem, einschließlich „example.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 MEMFS-Ordners '/'. Die neu erstellte Ausgabedatei example.pbm 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();

Mkbitmap-App mit einer Reihe von Dateien im Arbeitsspeicherdateisystem, darunter „example.bmp“ und „example.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 im Allgemeinen nicht für die direkte Anzeige im Browser unterstützen. (Es gibt elegantere Möglichkeiten, eine Datei zu speichern. Am häufigsten wird jedoch eine dynamisch erstellte <a download> verwendet.) Nachdem Sie die Datei gespeichert haben, 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

In diesem Fall ist die Eingabedatei hartcodiert und mkbitmap wird mit Standardparametern ausgeführt. Im letzten Schritt muss der Nutzer eine Eingabedatei dynamisch 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, sodass Sie mit einigem JavaScript-Code sogar eine Vorschau des Ausgabebildes anzeigen können. Der Quellcode der eingebetteten Demo unten zeigt eine Möglichkeit.

Fazit

Herzlichen Glückwunsch! Sie haben mkbitmap erfolgreich in WebAssembly kompiliert und die Anwendung im Browser funktioniert. Es gab einige Sackgassen und Sie mussten das Tool mehrmals kompilieren, bis es funktionierte, aber wie ich oben geschrieben habe, ist das Teil der Erfahrung. Denken Sie auch an das webassembly-Tag von Stack Overflow, falls Sie nicht weiterkommen. Viel Spaß beim Zusammenstellen!

Danksagungen

Dieser Artikel wurde von Sam Clegg und Rachel Andrew verfasst.