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, welchemalloc()
-Implementierung verwendet werden soll.emmalloc
ist eine kleine und schnellemalloc()
-Implementierung speziell für Emscripten. Die Alternative istdlmalloc
, eine vollwertigemalloc()
-Implementierung. Nur für Sie müssen Sie zudlmalloc
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:
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
Bibliotheksdarstellungen und die eigentliche Bibliothek (traditionelle .so
- oder .a
-Dateien) bereitgestellt werden. Bis
die VPX_CODEC_ABI_VERSION
-Konstante der Bibliothek in Ihrem Code verwenden,
, um die Headerdateien der Bibliothek mithilfe einer #include
-Anweisung einzuschließen:
#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:
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.