Die „alternative“ virtuelle Maschine WebAssembly ist auf dem Vormarsch

WebAssembly: Die neue Hoffnung
1 Kommentar

WebAssembly soll sowohl die Ladezeiten als auch die Ausführung von Webanwendungen gegenüber JavaScript beschleunigen. Die großen Browseranbieter treiben das zukunftsträchtige Projekt bei der Entwicklung voran. Zeit, sich WebAssembly mit seinen Stärken und Schwächen einmal genauer anzuschauen.

Die Entwicklung des Web war in vielerlei Hinsicht eine Sache von „Bedarf treibt Standard“. Hätten Brendan Eich und seine Mitstreiter vorausgesehen, dass JavaScript einmal eine so große Verbreitung erreichen würde, hätten sie sich sicherlich für einen saubereren Sprachstandard samt härterer Syntax entschieden. Wie dem auch sei: JavaScript ist heute nicht nur eine Programmiersprache für so ziemlich alles, sondern dient auch als Kompilierziel für andere Sprachen. CoffeeScript, TypeScript und Co. werden in JavaScript umgewandelt, bevor sie in der Runtime des Browsers zur Ausführung gelangen. Da die Auseinandersetzungen zwischen Opera, Mozilla und Microsoft zu einer immer größeren Performancesteigerung führten, sahen sich Entwickler dazu genötigt, eine virtuelle Maschine für den Browser zu konstruieren. Mit einem als Emscripten bezeichneten Compiler konnte man C-Programme transpilieren und danach in der gleichnamigen virtuellen Maschine im Browser ausführen.

Emscripten mag mit kleineren Programmen problemlos funktionieren, stößt in der Praxis über kurz oder lang jedoch an Performancegrenzen. Das liegt daran, dass der Aufbau von JavaScript eher schlecht als recht für virtuelle Maschinen geeignet ist: Die Sprache muss geparst werden und vieles mehr. WebAssembly versucht dieses Problem durch Erweiterungen des Browsers zu umgehen. Es handelt sich dabei im Grunde genommen um eine alternative virtuelle Maschine, die im Browser neben der JavaScript-Umgebung ausgeführt wird und mit ihr interagiert. WebAssembly-Programme sind dann „High-Performance-Inseln“, die aus dem gewöhnlichen JavaScript-Code heraus aufgerufen werden können, wenn man die maximale Leistungsfähigkeit der zugrunde liegenden Hardware benötigt. Ähnlichkeiten mit dem in Palm OS5 implementierten ARMlet-System sind rein zufällig, die Idee ist jedoch dieselbe.

Immer weitere Verbreitung

Als erste Versionen von Emscripten erschienen, betrachteten viele Entwickler das Produkt als Spielzeug. Im Fall von WebAssembly sieht die Situation jedoch anders aus – wer CanIUse nach Unterstützung für den Standard befragt, bekommt das in Abbildung 1 gezeigte und sehr befriedigende Ergebnis.

Abb. 1: Die Unterstützung von WebAssembly darf durchaus als breit bezeichnet werden (Quelle: [1])

Abb. 1: Die Unterstützung von WebAssembly darf durchaus als breit bezeichnet werden

Analog zur Entwicklung von Programmen für Microcontroller gilt auch in der Welt des WebAssembly, dass man Assemblercode nicht direkt von Hand erzeugt. Stattdessen kommen niedere Hochsprachen zum Einsatz, als Klassiker in besonderer Weise C.

Tipp: Wer bereits erfahren in Sachen Assemblerprogrammierung ist, findet auf der Webassembly-Homepage eine Einführung in die Assemblersprache der VM. Sie ist im Großen und Ganzen einfach und von der Handhabung her wesentlich komfortabler als X86- oder ARM-Assembly.

Die eigentliche Entstehung des Codes erfolgt über LLVM, der mit einem speziellen Pro l für die WebAssembly-VM ausgestattet ist. Wir wollen in den folgenden Schritten unter Ubuntu für 14.04 LTS experimentieren – die meisten Linux-Versionen funktionieren analog, für macOS und Windows finden Sie im Internet alternative Einstiegsanweisungen. Die erste Amtshandlung besteht jedenfalls darin, den Emscripten-Compiler herunterzuladen und im zweiten Schritt bereitzustellen:

tamhan@tamhan-thinkpad:~/webasmspace$ git clone https://github.com/juj/emsdk.git
. . .
tamhan@tamhan-thinkpad:~/webasmspace$ cd emsdk
tamhan@tamhan-thinkpad:~/webasmspace/emsdk$ ./emsdk install latest
. . .
Done installing tool 'emscripten-1.37.28'.
Done installing SDK 'sdk-1.37.28-64bit'.

Wundern Sie sich nicht darüber, dass im Rahmen der Abarbeitung dieses Kommandos einige 100 MB an Informationen heruntergeladen werden: Emscripten ist und bleibt nun einmal eine vollwertige C-Programmierumgebung. Im nächsten Schritt müssen Sie den Compiler aktivieren:

tamhan@tamhan-thinkpad:~/webasmspace$ ./emsdk activate latest
Abb. 2: Der Konfigurator legt diverse relevante Attribute fest

Abb. 2: Der Konfigurator legt diverse relevante Attribute fest

Von besonderem Interesse ist hier nur die in Abbildung 2 gezeigte Ausgabe von Konfigurationsinformationen. emsdk ist in der Lage, zwischen verschiedenen Emscripten-Installationen zu makeln.

Wer – wie der Autor – die Path-Variable seiner Workstation nicht gerne anpasst, kann dies umgehen. Im Rahmen der Ausführung von activate legt das Werkzeug nämlich ein Skript an, das ein Shellfenster mit den notwendigen Umgebungsvariablen dotiert. Seine Aktivierung muss natürlich nach jedem Öffnen des Konsolenfensters erfolgen und sieht folgendermaßen aus:

tamhan@tamhan-thinkpad:~/webasmspace/emsdk$ source ./emsdk_env.sh

Im nächsten Schritt konstruieren wir ein kleines Hello-World-Beispiel, das jedem C-Programmierer bestens bekannt vorkommt:

#include <stdio.h>
int main(int argc, char ** argv) {
  printf("Hello, world!\n");
}

Die Kompilation erfolgt im Großen und Ganzen so, wie man es von GCC erwarten würde:

tamhan@tamhan-thinkpad:~/webasmspace/project1$ emcc helloworld.c -s WASM=1 -o hello.html

Wichtig ist das Übergeben des Flags -s WASM=1 – fehlt es, so generiert Emscripten stattdessen Code für das weniger performante Format Asm.JS. In manchen Fällen kommt es während der Kompilation zu einer Gruppe von Fehlern, die auf ein Problem in der LLVM-Umgebung hinweisen:

CRITICAL:root:Could not verify LLVM version: Command '['/home/tamhan/webasmspace/emsdk/clang/e1.37.28_64bit/clang++', '-v']' returned non-zero exit status 1

Das Ubuntu-Team ist dafür berüchtigt, Pakete langsam zu aktualisieren. Zur Behebung des Problems müssen wir im ersten Schritt cmake herunterladen – es handelt sich dabei um ein Kompilationswerkzeug, das die Erstellung von C-Code automatisiert:

sudo apt-get remove cmake
wget http://www.cmake.org/files/v3.4/cmake-3.4.3.tar.gz 
tar -xvzf cmake-3.4.3.tar.gz 
cd cmake-3.4.3/ 
./configure 
make  -j 4
sudo make install

Danach folgt eine modifizierte Version des Downloadprozesses:

sudo ./emsdk install --build=Release sdk-incoming-64bit binaryen-master-64bit

Beachten Sie, dass diese Version des Befehls Herunterladen und Kompilieren nicht sequenziell ausführt – die Internetverbindung muss während des gesamten Deploymentprozesses weiterbestehen. Die eigentliche Kompilationsarbeit ist dabei nicht von schlechten Eltern – das zweikernige ThinkPad des Autors war gute zwei Stunden mit sich selbst beschäftigt. An dieser Stelle müssen wir den Aktivierungsprozess abermals ausführen. Diesmal übergeben wir andere Flags, um emcc anzuweisen, die soeben heruntergeladene brandaktuelle Version der Toolchain zu aktivieren. Beachten Sie, dass die Abarbeitung von ./ emsdk activate das Sourcen des generierten Files nicht erledigt – die Eingabe von Source muss von Hand erfolgen:

./emsdk activate --build=Release sdk-incoming-64bit binaryen-master-64bit

Rufen Sie den Compiler durch Eingabe von emcc helloworld. c -s WASM=1 -o hello.html auf, um einen weiteren Versuch zur Kompilation unseres Projektbeispiels anzuwerfen. Das Übergeben von -o befiehlt das Erzeugen einer Webseite, die die virtuelle Maschine und andere Nettigkeiten lädt. Öffnen Sie die Datei Hello. html sodann in einem Browser Ihrer Wahl, um sich an einer schwarzen Konsole und unserer kleinen Begrüßung zu erfreuen.

Modularisiere mich!

Ein Blick auf die bereitstehende Wunschliste des WebAssembly-Entwicklerteams verrät, dass man daran arbeitet, das Laden der hauseigenen .wasm-Module im Browser über ein nach dem Schema <script> aufgebautes Tag zu erleichtern. Im Moment ist dies allerdings noch nicht möglich – wer WebAssembly im Browser ausführen möchte, muss entweder auf die von Emscripten generierte „Shellseite“ setzen oder von Hand einen eigenen JavaScript-Lader realisieren. Wir wollen im folgenden Schritt die zweite Möglichkeit nutzen. Emscripten erweist sich hierbei insofern als kritisch, als die diversen in der Standardbibliothek enthaltenen Kommandos eine Unterstützungsinfrastruktur auf Seiten der Runtime voraussetzen. Aus diesem Grund ist es vernünftig, die Generierung des Wrappers zumindest anfangs an Emscripten zu delegieren. Zur Realisierung der Funktionen benötigen wir eine neue Version der .c-Datei, die nun eine Gruppe von Funktionen bereitstellen muss, mit denen unser JavaScript-Code interagieren soll:

#include <stdio.h>
#include <emscripten/emscripten.h>

Aus technischer Sicht finden sich hier zwei Unterschiede. Erstens müssen wir nun den Header <emscripten/emscripten.h> laden, der die C-Ausführungsumgebung um eine Gruppe von Emscripten-spezifischen Elementen erweitert. Zweitens muss eigentlich der zu exportierende Code wie in Listing 1 aufgebaut sein.

#ifdef __cplusplus
extern "C" {
#endif

void EMSCRIPTEN_KEEPALIVE myFunction(int argc, char ** argv) {
  printf("MyFunction Called\n");
}

#ifdef __cplusplus
}
#endif

Emscripten implementiert im Interesse der Performance eine Vielzahl von Optimierungsalgorithmen, die nicht benötigte Funktionen während der Kompilation eliminieren. Das Einschreiben des Parameters EMSCRIPTEN_KEEPALIVE informiert den Compiler darüber, dass die betreffende Funktion unbedingt erforderlich ist und auf keinen Fall wegoptimiert werden darf. C++-Puristen wundern sich mitunter darüber, warum die aufzurufende Funktion als C-Funktion deklariert wird. Die Ursache dafür ist, dass C++-Compiler den von ihnen bearbeiteten Code „mangeln“ und die Namen der Methoden auf diese Art und Weise verändern. Für uns wäre dies allerdings kontraproduktiv – der Name der Methode soll konstant bleiben, da wir ihn in der JavaScript-Datei benötigen. Die Kompilation erfolgt sodann durch eine Abart des von vorher bekannten emcc-Aufrufs:

tamhan@tamhan-thinkpad:~/webasmspace/project2$ emcc worker.c -s WASM=1  -s NO_EXIT_RUNTIME=1  -s EXTRA_EXPORTED_RUNTIME_METHODS='["ccall"]'

NO_EXIT_RUNTIME weist die Emscripten Runtime dazu an, weiterzulaufen, nachdem die Ausführung der main-Funktion beendet wurde. Das ist nicht nur aus API-technischer Sicht wichtig, sondern stellt sicher, dass Standardbibliothek und Co. weiter problemlos funktionieren. Parameter Numero zwei sorgt dafür, dass die Bibliotheksfunktion ccall auch im Kompilat zur Verfügung steht.

Das Resultat des Kompilationsprozesses sind eine .js-Datei und ein wasm-Modul. Beachten Sie, dass das Modul nicht alleinstehend ist. Weitere Informationen, warum das der Fall ist, finden Sie auf GitHub. Wir werden uns mit diesem Problem im Laufe des Artikels weiter auseinandersetzen. Für einen ersten Test reicht derzeit folgender Korpus:

<html>
  <head>
    <script src="a.out.js"></script>
  </head>
  <body></body>
</html>

Achten Sie darauf, dass die Bereitstellung der Datei nun zwangsweise über einen Server zu erfolgen hat. Unter Linux arbeitende Entwickler setzen auf den in der Python-Distribution bereitliegenden SimpleHTTPServer, unter Windows und macOS finden sich in Google diverse Alternativen. Öffnen Sie sodann die Browserkonsole, um sich an der in Abbildung 3 gezeigten Ausgabe unseres WebAssembly-Moduls zu erfreuen.

Abb. 3: Auch die abgespeckte Version des WebAssembly-Moduls ruft „main()“ auf, wenn sie geladen wird

Abb. 3: Auch die abgespeckte Version des WebAssembly-Moduls ruft „main()“ auf, wenn sie geladen wird

Im nächsten Akt müssen wir den Korpus um einen Button erweitern, der das Auslösen der Funktion ermöglichen soll. Erzeugen Sie den Button auf Ihre liebste Art und Weise – nutzen Sie ruhig jQuery, wenn Sie dazu in der Laune sind. Wichtig ist nur, dass Sie im Korpus der Event-Handler-Funktion folgendes Code-Snippet ablegen:

function onClick()
{
  var result = Module.ccall('myFunction', // name of C function
                                    null, // return type
                                    null, // argument types
                                   null); // arguments
}

Das WebAssembly-API des Browsers steht normalerweise über das globale Objekt Module zur Verfügung. Bei der Arbeit mit Emskripten nistet sich in ebendiesem eine Funktion namens ccall ein, die das Aufrufen von C-Methoden ermöglicht. In unserem Beispiel ist ob des Fehlens von Parametern außer dem Namen nur das Übergeben von drei Nullwerten erforderlich. Wer das Programm ausführt und den Button anklickt, findet in der Kommandozeile eine Gruppe von Ausgaben. Bedenken Sie, dass moderne Browser mehrere identische Zeilen gerne zusammenfassen und nur einmal anzeigen.

TypeScript mit WebAssembly

Tipp: Falls Sie sich mit JavaScript nicht anfreunden können und ohne Emscripten Compiler Frontend (emcc) arbeiten möchten, sei ihnen die TypeScript-Arbeitsumgebung ans Herz gelegt. Hier findet sich eine detaillierte Beschreibung des Workflows, der mit dem hier besprochenen bis auf den Namen nichts gemein hat.

Nachdem wir hier sowieso Emscripten-spezifischen Code erzeugen, wollen wir den Korpus von myFunction folgendermaßen anpassen:

void EMSCRIPTEN_KEEPALIVE myFunction(int argc, char ** argv) {
  emscripten_run_script("alert('hi')");
}

Die Funktion emscripten_run_script hat die Aufgabe, den an sie übergebenen String direkt an die eval-Methode der JavaScript Runtime zu übergeben. Auf diese Art und Weise kann Ihr C-Code JavaScript-Elemente zusammenbauen, die sodann zur Laufzeit ausgeführt werden. Beachten Sie bei der Nutzung dieses Features allerdings die Sicherheitsrichtlinien, denn kann ein Angreifer bösartig Code platzieren, kann er nach Belieben Chaos verursachen. Zudem erlauben viele App-Stores die Nutzung von eval und Co. nicht, weil man auf diese Art und Weise das QASystem problemlos umgehen könnte (Kasten: „TypeScript mit WebAssembly“).

Und jetzt in schlank

Als nächste Aufgabe wollen wir die Emscripten-Unterstützungsbibliothek aus dem Spiel nehmen und nur die in WebAssembly vorhandenen Möglichkeiten nutzen. Dies ist im Moment eher akademischer Natur, da die weiter oben erwähnten Bibliotheken das Vorhandensein einer gewissen Unterstützungsinfrastruktur voraussetzen. Zudem ist der JavaScript-Wrapper von Emscripten mit wenigen Kilobytes nicht sonderlich groß. (Kasten: „Keine Feigheit“) Da der JavaScript-WebAssembly-Lader strenge Kontrollen über das geladene Modul laufen lässt, müssen wir den Inhalt der C-Datei im ersten Schritt abspecken. Es soll anfangs eine Funktion ausreichen, die einen konstanten Integer-Wert zurückliefert:

int myFunction() {
  return 42;
}

Bei der Kompilation müssen wir eine Gruppe von zusätzlichen Flags übergeben, um emcc auf die neue Betriebssituation vorzubereiten:

tamhan@tamhan-thinkpad:~/webasmspace/project3$ emcc worker.c -O1 -s WASM=1 -s SIDE_MODULE=1 -o worker.wasm

Das Setzen von -s SIDE_MODULE=1 informiert den Compiler darüber, dass wir eine WebAssembly-Klassenbibliothek generieren wollen und die erzeugten Module dementsprechend alleinstehend aufzubauen sind. Das Aktivieren des Optimierers ist ebenfalls unbedingt erforderlich, da emcc sonst Teile der Runtime in das WebAssembly-Modul packt. Diese sind ihrerseits wiederum von der in JavaScript gehaltenen Unterstützungsinfrastruktur abhängig.

An dieser Stelle müssen wir das Importobjekt anlegen. Es handelt sich dabei um eine Art Platzhalter, der die JavaScript-Runtime darüber informiert, welche Bedürfnisse das WebAssembly-Modul an seine Umgebung stellen wird. Bei der Arbeit mit emcc-generiertem Code empfiehlt es sich, den folgenden Code zu nehmen:

<script>
var importObject = {
  env: {
    memoryBase :  0,
    tableBase :  0,
    memory : new WebAssembly.Memory({ initial: 256 }),
    table : new WebAssembly.Table({ initial: 0, element: 'anyfunc' })
  }
};

Keine Feigheit!

Tipp: Alleinstehende WebAssembly-Module leiden unter extremen Einschränkungen – es ist zum Beispiel nicht möglich, mittels malloc() Speicher anzufordern. Es spricht in der Praxis nichts dagegen, die Spiele aufzugeben und den nicht sonderlich großen JavaScript Wrapper ins Projekt einzubinden.

Da wir unseren Code über einen Webserver bereitstellen, erfolgt das Laden über eine gewöhnliche RESTOperation. Kritisch ist hier nur der Dateiname – wer emcc keinen -o-Parameter übergibt, bekommt ein File namens a.o.wasm:

fetch('worker.wasm').then(response => response.arrayBuffer())
.then(bytes => WebAssembly.instantiate(bytes, importObject))
.then(instance =>  {
  console.log("Kompilation erfolgreich!");
});

Der Code ist nicht kompliziert, aufgrund der Nutzung von Promises allerdings vergleichsweise lang. Fetch liefert uns einen Bytestrom, der im nächsten Schritt an WebAssembly.instantiate weitergereicht wird. Die Funktion nutzt den Datenstrom und das Importobjekt zur Bereitstellung einer Instanz – fürs Erste reicht es aus, bei erfolgreichem Durchlaufen die Meldung über die erfolgreiche Kompilation auszugeben.

Als Nächstes wollen wir die exportierte Funktion aufrufen. Im von Mozilla vorgegebenen Code gibt es hierbei eine kleine Doppeldeutigkeit – das zurückgelieferte Objekt hört auf den Namen instance, exponiert seinerseits aber ein gleichnamiges Unterobjekt. Dieses stellt den eigentlichen WebAssembly-Wrapper bereit. In der Theorie würde ein Aufruf von myFunction folgendermaßen aussehen:

fetch('worker.wasm').then(response => response.arrayBuffer())
.then(bytes => WebAssembly.instantiate(bytes, importObject))
.then(instance =>  {
  console.log(instance.instance.exports.myFunction());
  console.log("Kompilation erfolgreich!");
});

Leider ist in der Praxis nichts so, wie es zu sein scheint. Wer das vorliegende Programm in einem Browser seiner Wahl ausführt, bekommt eine Fehlermeldung, dass die Funktion nicht existiert. Zur tiefergehenden Analyse dieses Problems sollten wir das .wasm-File im ersten Schritt in eine menschenlesbare Form verwandeln. Hierzu reicht es aus, emcc nach dem Schema emcc worker.c -O1 -g -s WASM=1 -s SIDE_MODULE=1 -o worker. wasm, also unter Übergeben des Parameters -g aufzurufen.

Nach getaner Arbeit finden Sie neben der .wasm-Datei auch eine .wast-Datei. Die Dateiendung steht, Normen ist Omen, für WebAssembly Text, das File lässt sich in einem beliebigen Texteditor öffnen. Relevant ist dort folgende Passage:

(export "_myFunction" (func $_myFunction))
. . .
(func $_myFunction (; 0 😉 (result i32)
  ;;@ worker.c:3:0
    (i32.const 42)
)

Aus für den Autor unerfindlichen Gründen fügt der Compiler exportierten Funktionen ein _ an. myFunction hört also auf den Namen _myFunction. Wir wollen an dieser Stelle nicht über die Ursachen sinnieren, sondern ändern den Aufrufcode einfach folgendermaßen ab:

.then(instance =>  {
  console.log(instance.instance.exports._myFunction());
  console.log("Kompilation erfolgreich!");
});

Wer den vorliegenden Code ausführt, sieht in der Konsole die Ausgabe der Zahl 42 – es ist bewiesen, dass unser WebAssembly-Modul auch ohne die JavaScript-Umgebung lebensfähig bleibt. An dieser Stelle könnte man als Entwickler versucht sein, mit Parametern zu experimentieren. Als ersten Versuch passen wir my-Function insofern an, als wir nun einen weiteren Wert anliefern:

int myFunction(int what) {
  return what*42;
}

Nach der Kompilation öffnen Sie abermals die .wast-Datei, in der die Deklaration der Funktion nun etwas komplexer ausfällt:

(func $_myFunction (; 0 😉 (param $0 i32) (result i32)

Entwickler von WebAssembly leiden darunter, dass die Typwelt von JavaScript mit der einer realen, also in Hardware implementierten, Maschine nur wenig gemein hat. Zur Lösung dieses Problems führt man die vier in Tabelle 1 eingeführten Typenwerte ein, die zwischen den beiden Welten hin und her geschoben werden können.

Value Description
i32 Repräsentiert eine 32-bittige Ganzzahl
i64 Repräsentiert eine 64-bittige Ganzzahl
f32 Repräsentiert eine Fließkommazahl, zu deren Speicherung 32 Bit verwendet werden
f64 Repräsentiert eine Fließkommazahl, zu deren Nutzung 64 Bit verwendet werden

Tabelle 1: Typenwerte

Zur Laufzeit können wir den Aufruf jedenfalls folgendermaßen ausführen:

.then(instance =>  {
  console.log(instance.instance.exports._myFunction(2));
  console.log("Kompilation erfolgreich!");
});

Wer das vorliegende Programm ausführt, sieht den erhöhten Wert in der Kommandozeile. Fraglich ist an dieser Stelle nur, wie man Strings und andere komplexe Elemente übertragen kann – eine Aufgabe, der wir uns nun zuwenden wollen (Kasten: „Disassemblage ist intendiert“).

Disassemblage ist intendiert!

Tipp: WebAssembly taugt nicht bzw. nur
leidlich zum Schutz ihres geistigen Eigentums. Eines der Ziele des Sprachstandards ist eine möglichst bequeme Disassemblage, um die Analyse des generierten Codes zu erleichtern. Dass wasm schwerer zu lesen ist als C- oder JavaScript-Code, sei allerdings zugegeben.

Linearspeicher und mehr

WebAssembly arbeitet mit Linearspeicherobjekten. Stellen Sie sich diese wie einen zusammenhängenden Speicherbereich vor, der vom Browser mittels malloc bereitgestellt und dann zwischen der JavaScript- und der WebAssembly-Umgebung hin- und hergereicht wird. Neben Sicherheit gegen fehlgeleitete Zugriffe gewinnen Sie so auch die Sicherheit, dass es nicht zu Speicherlecks kommen kann. Instanzen von WebAssembly.Memory leben ja im Java-Script-Teil des Programms und unterliegen somit der in JavaScript integrierten Speicherverwaltung. Wie dem auch sei, müssen wir im ersten Schritt das Importobjekt anpassen – die neue Version ist in Listing 2 zu sehen.

var myMem= new WebAssembly.Memory({ initial: 256 });
var importObject = {
  env: {
    memoryBase :  0,
    tableBase :  0,
  memory : myMem,
  table : new WebAssembly.Table({ initial: 0, element: 'anyfunc' })
    }
};

Die Änderung ist im Großen und Ganzen eine Komfortangelegenheit: Anstatt die Memory-Instanz wie bisher als Teil des Objekts anzulegen, exponieren wir sie nun im Interesse des einfacheren Zugriffs über eine globale Variable. WebAssembly-Speicher lässt sich übrigens nicht beliebig dimensionieren. Stattdessen dient eine 64 Kilobyte große Speicherseite als Basis, die an den Konstruktor übergebenen Integer-Werte geben an, wie viele Speicherseiten das anzulegende Objekt umfassen soll. Es ist übrigens auch möglich, durch das Schema WebAssembly.Memory({initial:10, maximum:100}) sowohl die minimale als auch die maximale Größe anzugeben – WebAssembly.Memory-Objekte werden von der Runtime zur Laufzeit nach Belieben vergrößert. Bei der Arbeit mit im Fluss befindlichen systemtechnischen Prozessen ist es empfehlenswert, den Code als eine Art Spielzeug zu betrachten. Als ersten Versuch passen wir den Korpus von myFunction deshalb an, um ein gewöhnliches Integer und ein Array zu übergeben:

int myFunction(int* what, int howMany) {
  return 42;
}

Nach der Kompilation können wir in der .wast-Datei folgende Änderung feststellen:

(func $_myFunction (; 0 😉 (param $0 i32) (param $1 i32) (result i32)
  ;;@ worker.c:3:0
  (i32.const 42)

Ein Array ist auf Seiten der WebAssembly-Programmierumgebung also nur ein weiterer Parameter vom Typ i32. Es handelt sich dabei um eine lokale Adresse, die in Bezug auf das zum jeweiligen WebAssembly-Modul gehörende lineare Speicherobjekt zu betrachten ist. Im nächsten Schritt können wir eine grundlegende Array-Summierungsfunktion anlegen, die die Aufgabe hat, alle überlieferten Parameter zusammenzuziehen:

int myFunction(int* what, int howMany) {
int accu;
for(int i=0;i<howMany;i++) {
accu+=what[i];
}
return accu;
}

Von Interesse ist hier eigentlich nur das Übergeben eines zweiten Parameters. Er informiert unseren Parser darüber, wie tief er in das jeweilige Feld vordringen darf. Als Nächstes passen wir, wie in Listing 3 zu sehen, den Aufruf der Methode ein wenig an.

fetch('worker.wasm').then(response => response.arrayBuffer())
.then(bytes => WebAssembly.instantiate(bytes, importObject))
.then(instance =>  {

var i32 = new Uint32Array(myMem.buffer);
  for (var i = 0; i < 10; i++) {
    i32[i] = i;
  }
    console.log(instance.instance.exports._myFunction(i32, 3));
    . . .

JavaScript Runtimes sind – wohl im Interesse besserer Zusammenarbeit mit nativem Code – seit längerer Zeit mit einer Gruppe von Feldklassen ausgestattet, die das Realisieren von typgebundenen Feldern erlauben. Spezifischerweise handelt es sich dabei um die in Listing 4 zu sehenden Feldklassen.

Int8Array();
Uint8Array();
Uint8ClampedArray();
Int16Array();
Uint16Array();
Int32Array();
Uint32Array();
Float32Array();
Float64Array();

Wir nutzen den Konstruktor Uint32Array, dem wir eine Instanz des Puffers übergeben. Auf diese Art und Weise wissen Compiler und Runtime, wo der für das Feld benötigte Speicher herkommen soll. Nach der Bevölkerung des Felds schicken wir es in Richtung der Funktion – hier gibt es eigentlich nichts, was Sie nicht schon kennen.

Fazit

WebAssembly macht JavaScript mit Sicherheit nicht arbeitslos – dafür sorgt schon das Fehlen von DOM-APIs. Zudem ist die Programmierung von JavaScript unter dem Strich bequemer – das Einrichten von Importobjekt und Co. artet in Arbeit aus.

WebAssembly kann seine Stärken immer dann ausspielen, wenn es darum geht, einen vergleichsweise kleinen Teil des Programms mit sehr hoher Geschwindigkeit auszuführen. Realisieren Sie beispielsweise einen komplexen Finanzmathematischen Algorithmus, ist er ein geradezu idealer Kandidat für eine Optimierung. Behalten Sie dabei allerdings immer das Pareto-Prinzip im Hinterkopf: verschwendete Lebens- bzw. Arbeitszeit bekommen Sie auf keinen Fall zurück.

PHP Magazin

Entwickler MagazinDieser Artikel ist im PHP Magazin erschienen. Das PHP Magazin deckt ein breites Spektrum an Themen ab, die für die erfolgreiche Webentwicklung unerlässlich sind.

Natürlich können Sie das PHP Magazin über den entwickler.kiosk auch digital im Browser oder auf Ihren Android- und iOS-Devices lesen. In unserem Shop ist das Entwickler Magazin ferner im Abonnement oder als Einzelheft erhältlich.

Unsere Redaktion empfiehlt:

Relevante Beiträge

Hinterlasse einen Kommentar

1 Kommentar auf "WebAssembly: Die neue Hoffnung"

avatar
400
  Subscribe  
Benachrichtige mich zu:
Michael
Gast

Wie wärs denn mit dem FlashPlayer als WASM?

X
- Gib Deinen Standort ein -
- or -