ADR-130: A WebAssembly Library Is the Same Entry Point on Another Target — There Is No extern "wasm", and the Host Takes Its Buffers From the Module

Status: Superseded by ADR-284 — its D1-D4 are D22-D25 there Date: September 19, 2026 Target Version: Nikaia 0.0.9 Related: ADR-125 (the library boundary), ADR-037 (target), ADR-062 (a library build is a target), ADR-119 (a target without an operating system), ADR-124 (extern is reserved with its construct)

1. Context

ADR-125 wrote that the string after extern names the physical calling convention and reserved "wasm" for a WebAssembly export. Read again, that was a second word for the same thing: a WebAssembly module’s exports are C-convention functions — that is what the emitted Rust’s extern "C" produces on wasm32, and what every toolchain that targets the web does. What differs on the web is not the convention; it is the memory and the thread. The host cannot hand the module a pointer into its own heap, because the module has one linear memory and sees nothing else; and the host has one thread that must not be blocked, because it is the page’s.

Part III 15.3 says that user_parallelism = no and wasm32-unknown fit; what it lacked was the library shape on that target.

2. Decisions

D1 — extern "C" is the only string, and the target says WebAssembly.

[build]
target = "wasm32-unknown"
artifact = "c-library"

produces <package>.wasm, a generated <package>.js (an ES module the browser and Node load alike) and <package>.d.ts, from the same pub extern "C" fn declarations, the same ledger and the same rules as the header: the JavaScript file is the header for a host that has no headers. There is no extern "wasm"; a declaration that writes it is refused with the message the convention is "C" on every target; the target names WebAssembly.

Everything ADR-125 decides holds — status codes, out-parameters, Untrusted at entry, init and shutdown, the poison after a panic, the handle lock, the naming — except where memory and thread differ, which is D2 and D3.

D2 — The host obtains its buffers from the module.

The caller still owns the memory (ADR-125 D3) — but the only memory the module can read is its own, so the caller obtains it there:

<package>_alloc(size: i32, align: i32) -> i32     // an offset into linear memory
<package>_free(ptr: i32, size: i32, align: i32)

The host asks for a buffer, writes its text into it, calls the entry point with offset and length, reads the result out of the result buffer it also asked for, and frees both. The generated .js does all of that: a JavaScript caller writes hello.render("name") and receives a string, and the offsets never show. set_allocator does not exist on this target, because there is one memory and the module already manages it.

A handle is an offset the host keeps, freed through <package>_<Type>_free as everywhere; the .js wraps it in a class with a free() method and, where the host offers FinalizationRegistry, registers it so that a forgotten handle is freed when the wrapper is collected — a convenience the generated file says is not a guarantee.

D3 — A function that may pause has only the callback form, and the .js makes it a Promise.

The host’s thread may not block, so the blocking form of ADR-125 D6 is not exported on this target. A pausing entry point is exported once, with the completion callback and the ticket of ADR-129 — and the module’s executor is driven from the host’s event loop: the generated .js registers the module’s wakers with the host’s timers and I/O, so that a pause on a fetch or a timer resumes when the host resolves it. The .js exposes each such function as one returning a Promise, with cancel reachable through an AbortSignal the caller may pass, which maps to <package>_cancel.

A sync entry point is exported plainly and the .js calls it plainly.

D4 — What a WebAssembly library may do is what its target’s std offers.

Part III 17.2’s availability table decides per target already: on wasm32-unknown there is no filesystem, no thread and no socket of the module’s own, and a program that calls fs::read_to_string is refused at compile time as it is today. What the host has — fetch, timers, storage — reaches the module through the same mechanism the target already uses for its I/O worker (Part I 8.4’s sidecar as a Web Worker), and the entry points change nothing about it.

3. Consequences

4. What this does not decide

5. What is built

Nothing. The order of work, after ADR-125 §5:

  1. the target check: a library artifact on wasm32-unknown with the two allocation exports and without set_allocator and the blocking forms;
  2. the .js and .d.ts generator from the ledger, buffers and handles hidden;
  3. the executor bridge to the host’s event loop, and the Promise form;
  4. an example: ADR-125’s library built for the web and called from a page, and the test that runs it in Node.

#111 carries it.

This site is open source. Improve this page.