Live-alias WebAssembly.Memory.buffer to WAMR linear memory (browser-faithful detach-on-grow)#19
Live-alias WebAssembly.Memory.buffer to WAMR linear memory (browser-faithful detach-on-grow)#19e-fu wants to merge 9 commits into
Conversation
Go's GOOS=js GOARCH=wasm compiler (and TinyGo, Rust wasm-bindgen) emit bulk-memory opcodes unconditionally. WASM_ENABLE_BULK_MEMORY and WASM_ENABLE_REF_TYPES were already set via -D in native.ex, but the _OPT sub-feature gating memory.copy (fc 0a) / memory.fill (fc 0b) was not, so any such module failed to instantiate with 'unsupported opcode fc 0a'. Add -DWASM_ENABLE_BULK_MEMORY_OPT=1 beside the existing flags (the build's single source of truth; the vendored config.h #ifndef defaults are overridden by these -D defines). Regression test runs a module exercising all four bulk-memory opcodes (memory.fill/copy/init + data.drop) via both the native WASM API and the JS WebAssembly API, asserting run() == 43998.
The precompile workflow only uploaded to a GitHub release on v* tags, so a workflow_dispatch run on a feature branch built the NIF but left it unreachable. Add an actions/upload-artifact step (always) so a manual dispatch on any branch yields a downloadable per-target build (e.g. aarch64-macos-none) without cutting a release.
…w, regression tests
…ithful grow/zero-page detach
There was a problem hiding this comment.
Pull request overview
This PR updates QuickBEAM’s WebAssembly polyfill/runtime integration to make WebAssembly.Memory.prototype.buffer a live alias of WAMR’s linear memory (browser-faithful), including detach-on-grow semantics and safety when memory.grow happens mid-call before host re-entry. It also enables WAMR bulk-memory opcode support and extends TextDecoder.decode to accept DataView, with new regression tests.
Changes:
- Live-alias
Memory.bufferonto WAMR linear memory with stable identity, detach-on-grow, and host-import-boundary invalidation. - Enable WAMR bulk-memory “_OPT” opcodes and add fixtures/tests to ensure they compile and run.
- Accept
DataViewinTextDecoder.decode(with bounds checking) and add workflow artifact uploads for easier branch testing.
Reviewed changes
Copilot reviewed 11 out of 11 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
test/wasm_test.exs |
Adds regression fixtures and tests for bulk-memory opcodes plus Memory.buffer aliasing/detach semantics and TextDecoder DataView decoding. |
priv/ts/webassembly.ts |
Switches WasmMemory.buffer to a native-backed live buffer accessor instead of returning a copied buffer. |
priv/c_src/wamr_bridge.h |
Exposes new C bridge APIs to retrieve linear-memory base/size and the underlying module instance pointer. |
priv/c_src/wamr_bridge.c |
Fixes memory.grow semantics for WAMR and implements linear-memory base/size and module-inst accessors. |
lib/quickbeam/wasm_js.zig |
Implements cached, live-aliased ArrayBuffer creation/detachment, plus invalidation on grow and after calls. |
lib/quickbeam/wasm_host_imports.zig |
Adds a host-import boundary hook to invalidate/detach stale memory aliases before JS callbacks. |
lib/quickbeam/wamr.zig |
Enables WASM_ENABLE_BULK_MEMORY_OPT in the WAMR cImport configuration. |
lib/quickbeam/text_encoding.zig |
Extends TextDecoder.decode to accept DataView inputs correctly and safely. |
lib/quickbeam/quickbeam.zig |
Adds a root-module C-ABI export seam for alias invalidation reachable from host-import code. |
lib/quickbeam/native.ex |
Enables WASM_ENABLE_BULK_MEMORY_OPT in WAMR C flags and adds an opt-out for UBSan flags via env var. |
.github/workflows/precompile.yml |
Uploads build artifacts for all runs (not only tags) to support feature-branch testing. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| if (base == null or size == 0) { | ||
| // Zero-page linear memory: the browser returns a 0-length ArrayBuffer, | ||
| // not an error (`new WebAssembly.Memory({initial: 0}).buffer`). There is | ||
| // nothing to alias, and wasm memory cannot shrink, so no cached buffer | ||
| // exists to reuse — mint a fresh empty owned (detachable) buffer. | ||
| var empty: [1]u8 = .{0}; | ||
| return qjs.JS_NewArrayBufferCopy(ctx, &empty, 0); | ||
| } |
| test "zero-page memory exposes a 0-length buffer instead of throwing", %{rt: rt} do | ||
| assert {:ok, 0} = | ||
| QuickBEAM.eval(rt, """ | ||
| const bytes = #{@zeropage_bytes}; | ||
| const { instance } = await WebAssembly.instantiate(bytes); | ||
| instance.exports.mem.buffer.byteLength; | ||
| """) | ||
| end |
Known limitation: guest-opcode
|
Problem
masterreturnsWebAssembly.Memory.prototype.bufferas a copy of WAMR'slinear memory (
.slice().buffer). That breaks the browser contract thatmem.bufferaliases the live backing store: a host that writes through aDataView/TypedArrayovermem.buffercan't make those writes visible to theguest, and a guest write isn't visible to a previously-handed-out view. The
concrete casualty is Go's
wasm_exec.js, which caches aDataViewovermem.bufferand relies on host↔guest writes propagating through it.Change
Live-alias
Memory.bufferdirectly onto WAMR's linear memory, with browser-faithful semantics:.bufferaccess returns the sameArrayBufferwhile the backing store hasn't moved/resized.
WebAssembly.Memory.growdetaches the previouslyhanded-out buffer on every call (including
grow(0)and in-place growth),matching browsers; a fresh
.bufferreflects the grown size.memory.growopcode inside a call movesthe backing store; the cached alias is now invalidated at the JS host-import
re-entry boundary (via a C-ABI seam exported from the root NIF module that
reverse-maps the host
exec_env's module instance to the owning instance), soa host callback never reads through a stale/dangling alias.
(memory 0)now exposes a 0-lengthArrayBuffer(
new WebAssembly.Memory({initial: 0}).buffer) instead of throwing.TextDecoder.decodeaccepts aDataView(incl. non-zerobyteOffset),with bounds-checking.
Tests
test/wasm_test.exscovers: host writes through amem.bufferDataViewarevisible to the guest; stable buffer identity until grow; detach-on-grow and
grow(0)detach; a buffer captured before an in-call grow is detached at thenext host re-entry; zero-page 0-length buffer;
TextDecoder.decodeof aDataView.Builds under the CI toolchain pin (OTP 27.0 / Elixir 1.18 / Zig 0.15.2); CI
runs the full suite.