🤝JavaScript ↔ WebAssembly

Ein Wasm-Modul ist allein machtlos: Alles, was es mit der Außenwelt tun darf, bekommt es als Import. Zurück gibt es Funktionen, Speicher, Tabellen und Globals als Exporte. Alle Beispiele laufen hier live mit der Engine deines Browsers.

🧩Modul, Instanz, Importe, Exporte

💡 WebAssembly.Module
Das kompilierte, zustandslose Modul. Kann zwischengespeichert und mehrfach instanziiert werden.
💡 WebAssembly.Instance
Modul + eigener Zustand: Speicher, Tabellen, Globals – verbunden mit den übergebenen Importen.
✅ Memory, Table, Global
Lassen sich auch in JavaScript anlegen (new WebAssembly.Memory({ initial: 1 })) und als Import mit einem oder mehreren Modulen teilen.

1 · Laden und instanziieren

const bytes = await fetch("demo.wasm").then(r => r.arrayBuffer());

let memory;
const imports = {
  env: {
    // wird aus Wasm aufgerufen: call $log
    log: (ptr, len) => console.log(
      new TextDecoder().decode(new Uint8Array(memory.buffer, ptr, len)))
  }
};

const { instance } = await WebAssembly.instantiate(bytes, imports);
memory = instance.exports.memory;

instance.exports.add(20, 22);      // 42
instance.exports.hallo();          // log(16, 12) → "Hallo, Wasm!"

Im Web empfiehlt sich WebAssembly.instantiateStreaming(fetch(url), imports) – kompiliert schon während des Downloads (Server muss Content-Type: application/wasm senden).

Live im Browser: Demo-Modul (341 Byte)

Funktionen aufrufen …

Tipp: factorial(13) liefert 1932053504 – i32 rechnet modulo 2³², 13! = 6227020800 passt nicht hinein.

2 · Strings über den linearen Speicher

Wasm-Funktionen kennen nur Zahlen. Ein String wird von JavaScript als UTF-8 in den Speicher geschrieben; übergeben werden Zeiger und Länge. Danach liest JavaScript die veränderten Bytes wieder aus.

const bytes = new TextEncoder().encode(text);      // UTF-8
new Uint8Array(memory.buffer).set(bytes, 256);   // in den Speicher
instance.exports.grossschreiben(256, bytes.length);
const out = new Uint8Array(memory.buffer, 256, bytes.length);
new TextDecoder().decode(out);

3 · Tabellen und call_indirect

Funktionszeiger gibt es in Wasm nicht als Speicheradresse, sondern als Index in eine Tabelle mit Funktionsreferenzen.call_indirect prüft zur Laufzeit, ob die Signatur passt – ein Sprung auf beliebigen Code ist unmöglich.

(table (export "tabelle") 3 funcref)
(elem (i32.const 0) $add $sub $mul)
…
local.get $a
local.get $b
local.get $op
call_indirect (type $binop)

// JavaScript sieht dieselbe Tabelle:
instance.exports.tabelle.get(2)(6, 7);  // 42

Tabellengröße: … · Index außerhalb → WebAssembly.RuntimeError (Trap).

4 · Performance: gleiche Funktion in JS und Wasm

Beide Varianten nutzen denselben Algorithmus (siehe benchmark.wat). Gemessen wird hier in deinem Browser: Median aus 7 Läufen nach einem Aufwärmlauf.

⚠️ Wie aussagekräftig ist das?
  • Moderne JS-Engines optimieren solche Ganzzahl-Schleifen per JIT sehr gut – oft liegen JS und Wasm nahe beieinander, manchmal ist JS schneller.
  • Browser vergröbern performance.now() absichtlich (Schutz vor Timing-Angriffen); sehr kurze Messungen sind ungenau.
  • Stärken von Wasm liegen eher bei vorhersagbarer Leistung ohne Aufwärmphase, portiertem C/C++/Rust-Code, SIMD, Threads und kompakten Datenlayouts im linearen Speicher.
  • Jeder Aufruf über die JS↔Wasm-Grenze kostet etwas – viele winzige Aufrufe können den Vorteil aufzehren.

Noch nicht gemessen. Die Messung blockiert den Browser kurz.