Linear Memory, ABI, and Host Interop
Core WebAssembly does not know that a byte range is a string, struct, image, or tensor. It exposes linear memory and typed scalar calls; the module and host must agree on every richer representation. That agreement is an ABI even when it is informal, local to one application, or hidden behind generated bindings.
Introduction and Mental Model
Treat linear memory as a resizable, zero-based byte array owned by a WebAssembly.Memory object. A Wasm “pointer” is an integer offset into that array. A JavaScript typed array is a view over the memory's current buffer, not the memory itself. Shape, length, alignment, encoding, lifetime, mutability, and ownership remain external facts that both sides must preserve.
Crossing the boundary is therefore a protocol: allocate a region, encode values at agreed offsets, pass scalars, execute, observe results, and release or reuse storage. Memory growth can make cached views stale; callbacks can re-enter code while invariants are temporarily broken; and a claimed zero-copy path may merely move the copy to an earlier conversion. The useful question is not “is it zero-copy?” but “which bytes move, when, why, and who owns both representations?”
This branch applies byte layout and allocator fundamentals from the Low-Level Atlas specifically to the Wasm/host boundary. The later AI branches provide model and operator semantics; here a tensor is first a precise memory contract.
Why It Matters
For browser inference, boundary mistakes can dominate small kernels and corrupt large ones. JSON expands numeric data and loses layout. Repeated allocation increases latency variance. A stale view after memory.grow can read the wrong buffer. Ambiguous ownership causes leaks or use-after-free behavior at the application level. A clear ABI makes correctness testable and copy costs measurable before a runtime adds more abstraction.
Questions This Branch Answers
- How are pointers, lengths, alignments, and typed-array indices related without confusing elements and bytes?
- How should dense tensors, strings, arrays, and structs be laid out and versioned across the boundary?
- What happens to JavaScript buffer objects and cached views when Wasm memory grows?
- Which side allocates, initializes, mutates, retains, and frees each region?
- When should the host use imports, exports, callbacks, handles, or shared state?
- Where do copies occur when data starts in an
ArrayBuffer, decoded media object, GPU buffer, or external library tensor? - What can direct views eliminate, and which ownership, growth, isolation, or device-boundary constraints prevent zero-copy?
- How does an application-specific core-module ABI differ from the evolving Component Model Canonical ABI?
Scope
- Linear-memory addressing, pages, bounds, alignment, growth, and typed loads/stores.
ArrayBuffer,SharedArrayBufferwhere applicable,DataView, and typed-array views over Wasm memory.- Explicit layouts for UTF-8 strings, fixed and variable arrays, structs, and dense tensor buffers.
- Allocator choices, arenas, reuse, fragmentation, ownership, lifetime, and failure reporting.
- Scalar imports and exports, callbacks, re-entrancy, handles, and error/status conventions.
- Copy accounting for host-to-Wasm, Wasm-to-host, and device transfer boundaries.
- The Canonical ABI as a contrasting standardized lifting/lowering model, with its current evolving status stated explicitly.
Out of Scope
- Rebuilding a general-purpose allocator course or a complete foreign-function interface generator.
- Claiming that every browser, runtime, memory mode, or binding tool has identical growth behavior.
- Calling a path zero-copy without tracing the original producer and final consumer of the bytes.
- Full Component Model composition, resource types, WASI services, or WASI-NN.
- GPU buffer interop, WebNN tensors, and runtime-specific I/O binding details covered by later branches.
- Serialization formats for durable model storage.
Expected Outcomes
After this branch, the reader should be able to:
- Specify a byte-level ABI with offsets, sizes, alignments, encodings, ownership, and failure behavior.
- Safely create and refresh JavaScript views over Wasm memory.
- Move dense
f32tensor data without JSON and identify every remaining copy. - Select a simple allocation and reuse strategy appropriate for known inference lifetimes.
- Prevent stale pointers, double release, accidental retention, and unsafe callback re-entrancy in the host contract.
- Explain when generated bindings or the Canonical ABI add valuable structure and when a narrow scalar ABI is easier to audit.
Candidate Note Roadmap
linear-memory-pages-offsets-and-views— relate pages, byte addresses, element indices, endianness, alignment, bounds, and JavaScript view construction.memory-growth-and-stale-host-views— observe growth in unshared and shared configurations, refresh views defensively, and test allocation failure paths.versioned-layouts-for-strings-structs-and-tensors— define UTF-8 slices, padded and tagged structs, plus tensor dtype, rank, dimensions, strides, byte ranges, validation invariants, and compatibility rules without relying on host object layout.allocators-arenas-and-inference-lifetimes— compare bump allocation, free lists, scratch arenas, and planned buffer reuse for predictable workloads.ownership-borrowing-and-release-protocols— make host-owned, module-owned, borrowed, transferred, and retained data explicit in APIs and tests.imports-exports-callbacks-and-reentrancy— design narrow scalar calls, opaque handles, status codes, and callbacks that cannot observe half-mutated state.copy-ledgers-and-zero-copy-limits— instrument copies from producers through Wasm and onward to CPU or device consumers instead of inferring them from API names.canonical-abi-as-a-lifting-and-lowering-model— study Component Model values and Canonical ABI operations as an evolving standard, then contrast them with the workbench's core-module ABI.
Future Runnable Artifact
WasmAI Workbench v0 will gain a versioned f32 tensor bridge between TypeScript and a Wasm module. The module will export alloc(bytes, alignment), free(ptr, bytes), affine_relu(input_ptr, output_ptr, length, scale, bias), and memory. Version 1 of the contract will require little-endian contiguous FP32 data, 16-byte-aligned regions, non-overlapping input/output slices, checked ptr + length * 4 bounds, and negative status codes for allocation or descriptor errors.
The TypeScript side will implement two explicit paths. A copy-in path will accept an existing Float32Array, allocate Wasm-owned storage, copy with .set, run the kernel, and expose a borrowed output view. A direct-fill path will allocate first and let the producer write into the Wasm-backed view, avoiding that particular staging copy. Neither path will be labeled universally zero-copy: the report will count source-to-linear-memory bytes and explain later CPU/GPU or runtime transfers separately.
The artifact will force one controlled memory.grow, prove that the view cache is refreshed, reuse an arena across repeated runs, and display a live allocation/copy ledger. No JSON, per-element host calls, or hidden generated bindings will carry tensor payloads.
How to Verify and Measure
- Define the ABI in a machine-readable manifest and assert its version, exported signatures, memory ownership, alignments, and status codes before use.
- Test zero-length, odd-length, maximum in-range, misaligned, overlapping, wrapped-offset, out-of-bounds, allocation-failure, and growth cases.
- Place canary bytes around every region and verify them after calls; combine this with Wasm bounds traps rather than treating either check as sufficient alone.
- Differentially compare results with a TypeScript reference and verify that input buffers declared immutable remain byte-identical.
- Cache a view, trigger growth, and assert that the host detects and replaces it before the next access; run this test per supported memory mode and runtime.
- Instrument allocated bytes, peak pages, growth count, reused bytes, host-to-Wasm bytes, Wasm-to-host bytes, and calls crossing the boundary.
- Benchmark bulk operations against an intentionally bad per-element-call baseline, while keeping correctness, warm-up, input sizes, and engine versions fixed.
- Run callback/re-entrancy tests that attempt nested calls and release operations at forbidden points, and require deterministic rejection rather than corruption.
Primary Sources
- WebAssembly Core Specification — normative memory types, instructions, bounds behavior, and execution semantics. Release 3.0; checked 2026-07-16.
- WebAssembly JavaScript Interface — normative
WebAssembly.Memory, buffer, growth, import/export, and JavaScript conversion behavior. Living Editor's Draft dated 2026-07-10; checked 2026-07-16. - WebAssembly Component Model — primary specification repository containing the Canonical ABI design. Evolving proposal/specification work, not assumed universally available; checked 2026-07-16.
wasm-bindgenreference — primary documentation for generated Rust/JavaScript interoperation and supported conversions. Active project documentation; checked 2026-07-16.- Emscripten: Interacting with code — primary C/C++ and JavaScript memory/call-boundary documentation. Development documentation changes with releases; checked 2026-07-16.
Connects to: WebAssembly Execution Model · Toolchains and Language Targets · Tensor Kernels and Inference from Scratch · Preprocessing, Postprocessing, and Media Pipelines · WASI, Components, and WASI-NN