demo · v141
JS Prototype Bridge
Before custom descriptors, calling a method on a Wasm GC object from JavaScript meant importing the function explicitly and passing the object as the first argument. Custom descriptors let the Wasm module attach a real JavaScript prototype to its types — wasmObj.method(arg) works natively.
chrome://flags/#enable-experimental-webassembly-features). The probe below feature-detects; the comparison snippets work regardless.
before — bare wasm reference
const { instance } = await WebAssembly.instantiate(wasmBytes, {
env: {},
});
const player = instance.exports.makePlayer("Lara", 100);
// no method calls — only exported functions
const damaged = instance.exports.takeDamage(player, 20);
const score = instance.exports.getScore(damaged);
console.log("score:", score);
after — custom descriptor + JS prototype
// wasm module declares Player has a JS prototype with `takeDamage` + `score`
const { instance } = await WebAssembly.instantiate(wasmBytes);
const player = instance.exports.makePlayer("Lara", 100);
// methods on the wasm object — same syntax as a JS class
player.takeDamage(20);
console.log("score:", player.score);
the wasm side (simplified)
;; module declares the Player type with a JS prototype
(type $Player (struct (field $name (mut stringref))
(field $hp (mut i32))))
(custom-descriptor $Player
(js-prototype "Player"
(methods
(method "takeDamage" (param $self (ref $Player)) (param $dmg i32) ...)
(getter "score" (param $self (ref $Player)) ...))))
why this angle
Wasm GC (shipped in 119) gave Wasm modules first-class objects with fields. But the JS interop story remained awkward: every method had to be exported as a separate function, called as module.method(obj, args). Custom descriptors close the loop — Wasm modules can present themselves to JS as if they were ergonomic JS classes. For library authors (Wasm-compiled image codecs, AI runtimes, game engines), this is the difference between "you have to write a JS wrapper" and "your wasm just works."