Emscripten und npm

Wie binden Sie WebAssembly in dieses Setup ein? In diesem Artikel verwenden wir C/C++ und Emscripten als Beispiel dafür.

WebAssembly (wasm) wird oft die als Leistungsprimitiv oder zur Ausführung Ihrer vorhandenen C++- Codebasis im Web. Mit squoosh.app möchten wir zeigen, dass es für Wasm zumindest eine dritte Perspektive gibt: die Nutzung der riesigen Ökosysteme anderer Programmiersprachen. Mit Emscripten können Sie C/C++ Code Rost verfügt über integrierte Wasm-Unterstützung und die Go- auch daran arbeitet. Ich bin werden viele weitere Sprachen folgen.

In diesen Szenarien ist Wasm nicht das Herzstück Ihrer App, sondern ein Rätsel. noch ein weiteres Modul. Ihre App enthält bereits JavaScript-, CSS-, Bild-Assets, ein webbasiertes Build-System und vielleicht sogar ein Framework wie React. Wie geht es Ihnen? WebAssembly in dieses Setup zu integrieren? In diesem Artikel geht es darum, mit C/C++ und Emscripten als Beispiel.

Docker

Ich habe festgestellt, dass Docker bei der Arbeit mit Emscripten von unschätzbarem Wert ist. C/C++ Bibliotheken werden oft so geschrieben, dass sie mit dem Betriebssystem funktionieren, auf dem sie basieren. Eine einheitliche Umgebung ist unglaublich hilfreich. Docker bietet ein virtuelles Linux-System, das bereits für Emscripten eingerichtet ist und alle installierten Tools und Abhängigkeiten. Wenn etwas fehlt, können Sie einfach können Sie sie installieren, ohne sich Gedanken darüber machen zu müssen, wie sich dies auf Ihren Computer andere Projekte. Wenn etwas schiefgeht, entsorgen Sie den Behälter und beginnen Sie vorbei. Wenn es einmal funktioniert, können Sie sicher sein, dass es auch weiterhin funktioniert und zu identischen Ergebnissen führen.

Die Docker Registry enthält eine Emscripten- Bild von Trzeci, die ich oft benutze.

Integration mit npm

In den meisten Fällen ist der Einstiegspunkt für ein Webprojekt das package.json Konventionsgemäß können die meisten Projekte mit npm install && npm run build erstellt werden.

Im Allgemeinen werden die von Emscripten erzeugten Build-Artefakte (ein .js und ein .wasm) -Datei) sollte als weiteres JavaScript-Modul und nur als ein weiteres Die JavaScript-Datei kann von einem Bundler wie einem Webpack oder Rollup verarbeitet werden, und die Wasm-Datei sollte wie jedes andere größere binäre Asset behandelt werden, Bilder.

Daher müssen die Emscripten-Build-Artefakte vor den normalen der Build-Prozess beginnt:

{
    "name": "my-worldchanging-project",
    "scripts": {
    "build:emscripten": "docker run --rm -v $(pwd):/src trzeci/emscripten
./build.sh",
    "build:app": "<the old build command>",
    "build": "npm run build:emscripten && npm run build:app",
    // ...
    },
    // ...
}

Die neue Aufgabe build:emscripten könnte Emscripten direkt aufrufen, aber da empfehle ich die Verwendung von Docker, um sicherzustellen, dass die Build-Umgebung einheitlich sind.

docker run ... trzeci/emscripten ./build.sh weist Docker an, ein neues Container mit dem trzeci/emscripten-Image und führen Sie den Befehl ./build.sh aus. build.sh ist ein Shell-Skript, das Sie als Nächstes schreiben. --rm teilt Ihnen mit Docker, um den Container nach der Ausführung zu löschen. Auf diese Weise erstellen Sie eine Sammlung veralteter Maschinen-Images im Laufe der Zeit erstellen. -v $(pwd):/src bedeutet Folgendes: soll Docker „gespiegelt“ das aktuelle Verzeichnis ($(pwd)) in /src darin Container. Alle Änderungen, die Sie an Dateien im Verzeichnis /src innerhalb des wird der Container in Ihr eigentliches Projekt gespiegelt. Diese gespiegelten Verzeichnisse werden als „bind mounts“ bezeichnet.

Werfen wir einen Blick auf build.sh:

#!/bin/bash

set -e

export OPTIMIZE="-Os"
export LDFLAGS="${OPTIMIZE}"
export CFLAGS="${OPTIMIZE}"
export CXXFLAGS="${OPTIMIZE}"

echo "============================================="
echo "Compiling wasm bindings"
echo "============================================="
(
    # Compile C/C++ code
    emcc \
    ${OPTIMIZE} \
    --bind \
    -s STRICT=1 \
    -s ALLOW_MEMORY_GROWTH=1 \
    -s MALLOC=emmalloc \
    -s MODULARIZE=1 \
    -s EXPORT_ES6=1 \
    -o ./my-module.js \
    src/my-module.cpp

    # Create output folder
    mkdir -p dist
    # Move artifacts
    mv my-module.{js,wasm} dist
)
echo "============================================="
echo "Compiling wasm bindings done"
echo "============================================="

Hier gibt es viel zu zerlegen!

set -e bringt die Shell in den Modus „Fail Fast“ . Wenn Befehle im Skript einen Fehler zurückgeben, wird das gesamte Skript sofort abgebrochen. Dabei kann es sich um unglaublich hilfreich, denn die letzte Ausgabe des Skripts wird immer ein Erfolg sein. oder den Fehler, der zum Fehlschlagen des Builds geführt hat.

Mit den export-Anweisungen definieren Sie die Werte einiger Variablen. Sie ermöglichen die Übergabe zusätzlicher Befehlszeilenparameter an das Compiler (CFLAGS), der C++-Compiler (CXXFLAGS) und der Linker (LDFLAGS). Sie alle erhalten die Optimierungseinstellungen über OPTIMIZE, um sicherzustellen, wird alles auf die gleiche Weise optimiert. Es gibt einige mögliche Werte, für die Variable OPTIMIZE:

  • -O0: Es wird keine Optimierung durchgeführt. Es wird kein toter Code beseitigt und Emscripten wird auch der ausgegebene JavaScript-Code nicht komprimiert. Gut zum Debugging.
  • -O3: Optimieren Sie die Leistung intensiv.
  • -Os: Als sekundäre Kampagne eine aggressive Optimierung im Hinblick auf Leistung und Größe vornehmen Kriterium.
  • -Oz: Führen Sie eine aggressive Optimierung im Hinblick auf die Größe durch und verringern Sie bei Bedarf die Leistung.

Für das Web empfehle ich am besten -Os.

Der Befehl emcc bietet eine Vielzahl von Optionen. emcc ist ein „Drop-in-Ersatz“ für Compiler wie GCC oder Clang sein. Alle Meldungen, die Sie vielleicht von GCC kennen, werden höchstwahrscheinlich als gut. Das Flag -s ist insofern eine Besonderheit, als es uns ermöglicht, Emscripten zu konfigurieren. spezifisch sind. Alle verfügbaren Optionen finden Sie in Emscriptens settings.js, aber diese Datei kann ziemlich überwältigend sein. Hier ist eine Liste der Emscripten-Flags, die für Webentwickler am wichtigsten sind:

  • --bind aktiviert embind übergeben.
  • -s STRICT=1 stellt die Unterstützung für alle eingestellten Build-Optionen ein. Dadurch wird sichergestellt, dass Ihr Code auf vorwärtskompatible Weise erstellt wird.
  • -s ALLOW_MEMORY_GROWTH=1 ermöglicht das automatische Vergrößern des Arbeitsspeichers, wenn notwendig ist. Zum Zeitpunkt der Erstellung dieses Dokuments weist Emscripten 16 MB Speicher zu. anfänglich. Da Ihr Code Speicherblöcke zuweist, bestimmt diese Option, Diese Vorgänge führen dazu, dass das gesamte Wasm-Modul ausfällt, wenn der Arbeitsspeicher erschöpft ist oder der Glue Code den Gesamtarbeitsspeicher für die Zuweisung berücksichtigt werden.
  • -s MALLOC=... wählt aus, welche malloc()-Implementierung verwendet werden soll. emmalloc ist eine kleine und schnelle malloc()-Implementierung speziell für Emscripten. Die Alternative ist dlmalloc, eine vollwertige malloc()-Implementierung. Nur für Sie müssen Sie zu dlmalloc wechseln, wenn Sie viele kleine Objekte zuweisen oder wenn Sie Threading verwenden möchten.
  • -s EXPORT_ES6=1 wandelt den JavaScript-Code in ein ES6-Modul mit einem der mit jedem Bundler funktioniert. Erfordert außerdem -s MODULARIZE=1, um festgelegt werden.

Die folgenden Flags sind nicht immer erforderlich oder nur für die Fehlerbehebung hilfreich Zwecke:

  • -s FILESYSTEM=0 ist ein Flag, das sich auf Emscripten bezieht und die Fähigkeit, emulieren Sie ein Dateisystem für Sie, wenn Ihr C/C++ Code Dateisystemvorgänge verwendet. Es führt einige Analysen des kompilierten Codes durch, um zu entscheiden, ob der oder nicht. Manchmal wird dies jedoch kann die Analyse falsch sein und du bezahlst ziemlich satte 70 KB an zusätzlichem Klebstoff Code für eine Dateisystememulation, die Sie vielleicht gar nicht benötigen. Mit -s FILESYSTEM=0 können Sie erzwingen, dass Emscripten diesen Code nicht eingibt.
  • -g4 sorgt dafür, dass Emscripten Debugging-Informationen in .wasm und gibt auch eine Quellzuordnungsdatei für das Wasm-Modul aus. Weitere Informationen dazu finden Sie mit Emscripten beim Debugging .

Das war schon alles. Um diese Konfiguration zu testen, erstellen wir eine winzige my-module.cpp:

    #include <emscripten/bind.h>

    using namespace emscripten;

    int say_hello() {
      printf("Hello from your wasm module\n");
      return 0;
    }

    EMSCRIPTEN_BINDINGS(my_module) {
      function("sayHello", &say_hello);
    }

Und ein index.html:

    <!doctype html>
    <title>Emscripten + npm example</title>
    Open the console to see the output from the wasm module.
    <script type="module">
    import wasmModule from "./my-module.js";

    const instance = wasmModule({
      onRuntimeInitialized() {
        instance.sayHello();
      }
    });
    </script>

(Hier ist eine gist das alle Dateien enthält.)

Führen Sie den folgenden Befehl aus, um alles zu erstellen:

$ npm install
$ npm run build
$ npm run serve

Wenn Sie zu localhost:8080 gehen, sollten Sie die folgende Ausgabe im Entwicklertools-Konsole:

DevTools mit einer über C++ und Emscripten gedruckten Nachricht

C/C++-Code als Abhängigkeit hinzufügen

Wenn Sie eine C/C++-Bibliothek für Ihre Webanwendung erstellen möchten, muss deren Code Teil Ihres Projekts sind. Sie können den Code manuell in das Repository Ihres Projekts einfügen oder Sie können diese Art von Abhängigkeiten mit npm verwalten. Nehmen wir an, ich libvpx in meiner Webanwendung verwenden. libvpx ist eine C++-Bibliothek zum Codieren von Bildern mit VP8, dem Codec, der in .webm-Dateien verwendet wird. libvpx ist jedoch nicht auf npm und hat keine package.json, daher kann ich direkt mit npm installieren.

Um dieses Rätsel zu lösen, Napa. Mit napa können Sie jedes Git installieren Repository-URL als Abhängigkeit in den Ordner node_modules verschieben.

Installieren Sie napa als Abhängigkeit:

$ npm install --save napa

und führen Sie napa als Installationsskript aus:

{
// ...
"scripts": {
    "install": "napa",
    // ...
},
"napa": {
    "libvpx": "git+https://github.com/webmproject/libvpx"
}
// ...
}

Wenn du npm install ausführst, klont napa das libvpx-GitHub unter dem Namen libvpx in die node_modules ein.

Sie können Ihr Build-Skript jetzt für die Erstellung von libvpx erweitern. libvpx verwendet configure und make noch zu erstellen. Glücklicherweise kann Emscripten dazu beitragen, dass configure und make verwenden den Compiler von Emscripten. Zu diesem Zweck gibt es den Wrapper Befehle emconfigure und emmake:

# ... above is unchanged ...
echo "============================================="
echo "Compiling libvpx"
echo "============================================="
(
    rm -rf build-vpx || true
    mkdir build-vpx
    cd build-vpx
    emconfigure ../node_modules/libvpx/configure \
    --target=generic-gnu
    emmake make
)
echo "============================================="
echo "Compiling libvpx done"
echo "============================================="

echo "============================================="
echo "Compiling wasm bindings"
echo "============================================="
# ... below is unchanged ...

Eine C/C++-Bibliothek besteht aus zwei Teilen: den Headern (traditionell .h oder .hpp-Dateien), die die Datenstrukturen, Klassen, Konstanten usw. definieren, die ein Bibliothek und die eigentliche Bibliothek bereitgestellt werden (traditionelle Dateien vom Typ .so oder .a). Bis die VPX_CODEC_ABI_VERSION-Konstante der Bibliothek in Ihrem Code verwenden, um die Headerdateien der Bibliothek mithilfe einer #include-Anweisung hinzuzufügen:

#include "vpxenc.h"
#include <emscripten/bind.h>

int say_hello() {
    printf("Hello from your wasm module with libvpx %d\n", VPX_CODEC_ABI_VERSION);
    return 0;
}

Das Problem besteht darin, dass der Compiler nicht weiß, wo nach vpxenc.h gesucht werden soll. Dazu wird das Flag -I verwendet. Er teilt dem Compiler mit, welche Verzeichnisse nach Header-Dateien. Außerdem müssen Sie dem Compiler die Bibliotheksdatei:

# ... above is unchanged ...
echo "============================================="
echo "Compiling wasm bindings"
echo "============================================="
(
    # Compile C/C++ code
    emcc \
    ${OPTIMIZE} \
    --bind \
    -s STRICT=1 \
    -s ALLOW_MEMORY_GROWTH=1 \
    -s ASSERTIONS=0 \
    -s MALLOC=emmalloc \
    -s MODULARIZE=1 \
    -s EXPORT_ES6=1 \
    -o ./my-module.js \
    -I ./node_modules/libvpx \
    src/my-module.cpp \
    build-vpx/libvpx.a

# ... below is unchanged ...

Wenn Sie npm run build jetzt ausführen, werden Sie feststellen, dass der Prozess eine neue .js erstellt. .wasm-Datei erstellen und dass die Demoseite tatsächlich die folgende Konstante ausgibt:

DevTools
zeigt eine über emscripten gedruckte ABI-Version von libvpx.

Sie werden auch feststellen, dass der Build-Prozess sehr lange dauert. Der Grund für die lange Build-Dauer kann variieren. Im Fall von libvpx dauert es sehr lange, Es kompiliert bei jeder Ausführung einen Encoder und einen Decoder für VP8 und VP9. den Build-Befehl verwenden, auch wenn sich die Quelldateien nicht geändert haben. Selbst eine kleine an Ihrem my-module.cpp zu erstellen, wird viel Zeit in Anspruch nehmen. Das wäre sehr die Build-Artefakte von libvpx immer zur Hand zu haben, die Sie beim ersten Mal entwickelt haben.

Eine Möglichkeit, dies zu erreichen, sind Umgebungsvariablen.

# ... above is unchanged ...
eval $@

echo "============================================="
echo "Compiling libvpx"
echo "============================================="
test -n "$SKIP_LIBVPX" || (
    rm -rf build-vpx || true
    mkdir build-vpx
    cd build-vpx
    emconfigure ../node_modules/libvpx/configure \
    --target=generic-gnu
    emmake make
)
echo "============================================="
echo "Compiling libvpx done"
echo "============================================="
# ... below is unchanged ...

(Hier ein Überblick das alle Dateien enthält.)

Mit dem Befehl eval können Umgebungsvariablen durch Übergabe von Parametern festgelegt werden in das Build-Skript ein. Der Befehl test überspringt das Erstellen von libvpx, wenn $SKIP_LIBVPX ist auf einen beliebigen Wert festgelegt.

Jetzt können Sie das Modul kompilieren, aber die Neuerstellung von libvpx überspringen:

$ npm run build:emscripten -- SKIP_LIBVPX=1

Build-Umgebung anpassen

Manchmal sind für die Erstellung von Bibliotheken zusätzliche Tools erforderlich. Wenn diese Abhängigkeiten in der vom Docker-Image bereitgestellten Build-Umgebung fehlen, müssen Sie selbst hinzufügen. Nehmen wir als Beispiel an, Sie möchten auch die Dokumentation von libvpx mithilfe von doxygen Doxygen ist nicht im Docker-Container verfügbar, aber Sie können es mit apt installieren.

In diesem Fall musst du die App in deinem build.sh noch einmal herunterladen und neu installieren wenn ihr eure Bibliothek erstellen möchtet. Das wäre nicht nur ist verschwendet, aber es würde Sie auch davon abhalten, offline an Ihrem Projekt zu arbeiten.

Hier ist es sinnvoll, ein eigenes Docker-Image zu erstellen. Docker-Images werden von Schreiben eines Dockerfile, der die Build-Schritte beschreibt. Dockerfiles sind ziemlich leistungsstark und mit vielen Befehle, aber die meisten Zeit, die Sie mit FROM, RUN und ADD wegbekommen können. In diesem Fall gilt:

FROM trzeci/emscripten

RUN apt-get update && \
    apt-get install -qqy doxygen

Mit FROM können Sie deklarieren, welches Docker-Image Sie als Start-Image verwenden möchten. Punkt. Als Grundlage habe ich trzeci/emscripten ausgewählt – das Bild, das Sie verwendet haben die ganze Zeit über. Mit RUN weisen Sie Docker an, Shell-Befehle im Container. Alle Änderungen, die diese Befehle am Container vornehmen, das Docker-Image. So stellen Sie sicher, dass Ihr Docker-Image erstellt wurde und verfügbar, bevor Sie build.sh ausführen, müssen Sie package.json anpassen, Bit:

{
    // ...
    "scripts": {
    "build:dockerimage": "docker image inspect -f '.' mydockerimage || docker build -t mydockerimage .",
    "build:emscripten": "docker run --rm -v $(pwd):/src mydockerimage ./build.sh",
    "build": "npm run build:dockerimage && npm run build:emscripten && npm run build:app",
    // ...
    },
    // ...
}

(Hier ein Überblick das alle Dateien enthält.)

Dadurch wird Ihr Docker-Image erstellt, allerdings nur, wenn es noch nicht erstellt wurde. Dann alles wird wie vorher ausgeführt, aber jetzt hat die Build-Umgebung das doxygen verfügbar, wodurch die Dokumentation von libvpx als gut.

Fazit

Es ist nicht verwunderlich, dass C/C++ Code und npm nicht für Sie geeignet sind. Sie können mit einigen zusätzlichen Tools und der Isolierung von Docker bereitgestellt. Diese Einrichtung funktioniert nicht für jedes Projekt, ist aber einen guten Ausgangspunkt bieten, den Sie an Ihre Bedürfnisse anpassen können. Wenn Sie Verbesserungsvorschläge.

Anhang: Docker-Image-Ebenen verwenden

Eine alternative Lösung besteht darin, weitere dieser Probleme mit Docker zu kapseln Der intelligente Ansatz von Docker für das Caching. Docker führt Dockerfiles Schritt für Schritt aus weist dem Ergebnis jedes Schritts ein eigenes Bild zu. Diese Zwischenbilder werden oft als „Ebenen“ bezeichnet. Wenn sich ein Befehl in einem Dockerfile nicht geändert hat, wird dieser Schritt nicht noch einmal ausgeführt, wenn Sie das Dockerfile neu erstellen. Stattdessen Es wird die Ebene aus der letzten Image-Erstellung wiederverwendet.

Bisher mussten Sie libvpx nicht jedes Mal neu erstellen, Sie Ihre App entwickeln. Stattdessen können Sie die Erstellungsanleitung für libvpx verschieben aus Ihrem build.sh in den Dockerfile, um das Docker-Caching zu nutzen Mechanismus:

FROM trzeci/emscripten

RUN apt-get update && \
    apt-get install -qqy doxygen git && \
    mkdir -p /opt/libvpx/build && \
    git clone https://github.com/webmproject/libvpx /opt/libvpx/src
RUN cd /opt/libvpx/build && \
    emconfigure ../src/configure --target=generic-gnu && \
    emmake make

(Hier ein Überblick das alle Dateien enthält.)

Beachten Sie, dass Sie Git manuell installieren und libvpx klonen müssen, da Sie Binden Sie Bereitstellungen, wenn Sie docker build ausführen. Als Nebeneffekt sind keine nicht mehr wegzudenken.