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
|
|
Thanks for the PR. I rebased and tested this locally, but the new live-buffer tests still hit the WAMR Debug/UBSan alignment panic and abort the BEAM. Could you rework the tests/implementation so this path fails safely or avoids that WAMR panic? Also, please split the unrelated CI artifact and sanitizer-env changes out of this PR. |
|
Sorry for the very late reply — this dropped off my radar for far too long. I'm closing this one. Three of the commits here have since landed on Rather than let it rot, one parting note that may be worth more than the PR itself: I think I found the root cause of the "pre-existing WAMR Debug/UBSan alignment trap" you worked around in
frame_csp = frame->csp_bottom = (WASMBranchBlock *)frame->sp_boundary;
That makes it deterministic per guest — a function of the callee's cell parity — rather than "on some builds", which would explain why only certain fixtures trigger it while The fix should be to round Worth flagging separately: the live Thanks for the reviews here and on the earlier PRs. |
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.