FFI: Calling Out, and Being Called Into

Talking to other languages is two different problems. Calling out to a C function is one line — `extern fn time: unit -> int;` — and it is the honest boundary where the language's guarantees stop. Being called into, from JavaScript in a browser, is harder: the host holds objects Mere can't represent, and must be able to invoke Mere closures. Both directions turn out to reduce to the same trick — trading indices into a table the other side owns.

mereffiwebassemblyinteroplanguage-design

Four backends can run any Mere program, held to identical output. What none of them can do yet is talk to code written in another language — call a C library function, or let a browser hand an event to a Mere closure. That is what a foreign function interface is for, and it is really two separate problems pointing in opposite directions: calling out to foreign code, and being called into by it. Mere solves each, and the two solutions rhyme in a way worth noticing.

Calling out: extern fn

Until this point, everything Mere could do to the outside world was a built-in — around ninety of them, each hardcoded into the compiler across all four backends. Adding one meant editing the compiler and shipping a new release. That is fine for a closed language and untenable for a practical one: you cannot ask everyone who wants time() or getenv() or sqrt() to patch the compiler.

The fix is a way to declare an external function from ordinary user code:

extern fn time:   unit -> int;
extern fn getenv: str -> str;

let now = time () in ...

One line names the function and its type, and it becomes callable. The syntax chosen is the plain one — not a Rust-style extern "C" block, not an effect-annotated form — on the principle, familiar by now, of shipping the minimal honest version and adding ceremony only when real use demands it. Each backend already had the machinery to make the call: C emits an extern declaration and a direct call, and Wasm reuses the exact host-import mechanism from the previous episode — an extern fn on Wasm is just a named (import "env" ...), the same gate print already went through, now opened to arbitrary functions the host provides.

What matters most about extern fn is what it admits. The whole language is built to maximize what a compiler can verify — explicit types, explicit effects, byte-identical backends. An external C function is outside all of that: the compiler can’t check what it does, whether it frees a pointer you still hold, whether it matches the type you declared. FFI is a genuine hole in the guarantees. The language’s response is not to pretend otherwise but to name the hole: the extern keyword marks exactly where verifiability stops, so a reader can see which calls leave the safe world. This is the same honesty the project applies everywhere — the coverage ledger that won’t call a feature done, the refusal to let “close enough” pass for identical. Where the guarantee ends, the language says so out loud, rather than letting the boundary blur.

Being called into: the browser problem

Calling out is the easy direction, because Mere is in control — it marshals the arguments and trusts the result. The hard direction is the reverse: JavaScript, in a browser, calling into a running Mere program, and passing around things Mere was never built to hold. Mere already compiles to Wasm and runs in a page; to be useful on a frontend it has to touch the DOM and respond to events, and that surfaces two problems a scalar extern fn can’t solve.

The first is objects Mere can’t represent. A DOM element is a JavaScript object; every Mere value is a 4-byte word. You cannot put a <div> into an i32. The answer is an opaque handle: a new extern type JsRef that is an i32 on the wire but opaque inside Mere — you can hold it and pass it back, but never inspect it. The trick lives on the host side: JavaScript keeps a table of real objects and hands out indices into it. dom_get_by_id stores the element and returns its index; dom_set_text takes an index back and looks the element up to act on it. Mere carries around a number that is meaningless to Mere and meaningful to the host.

The second is letting JavaScript call a Mere function — an event handler has to run Mere code when a click happens. And here something built two episodes ago pays off. The Wasm backend already represents every closure as an index into a function table, because Wasm has no function pointers; internally, calling a closure is a call_indirect through that table. A callback into Mere is just that same index, exported across the boundary: Mere hands JavaScript the closure’s table index, and when the event fires, JavaScript calls back in through the function table with it. The machinery that existed only for Mere’s own closures becomes the mechanism by which the outside world calls Mere.

One trick, twice

Set the two side by side and they are the same idea. Mere’s linear memory and JavaScript’s object heap are two worlds that cannot share pointers — neither can hold a raw address into the other. So both directions cross the boundary the same way: by trading indices into a table the other side owns. A DOM element is an index into the host’s handle table. A Mere callback is an index into Mere’s function table. In each case the value that crosses is a meaningless number to the side receiving it and a lookup key to the side that issued it. Two worlds that can’t share memory can still share integers, and an integer plus a table on each end is enough to pass anything.

That symmetry is why the frontend interface came together from parts already on hand: the handle table mirrored on the JavaScript side, the function table already built inside the Wasm backend, and extern fn’s import mechanism generalized to carry them. The concrete result is a small DOM binding and a counter that runs in a browser — a Mere closure serving as a live click handler — reached, in keeping with how the whole project moves, as a minimal MVP with the rough edges named rather than hidden (handles leak for the page’s lifetime; the safety story is deferred until use demands it).

The language can now run four ways and speak to the code on either side of it. The natural next question is whether it can carry real weight — not a counter, but a program substantial enough to prove the language is usable for something you’d actually want to build. The project’s answer was to build its own documentation site in Mere. Next, the Part IV closer: writing the docs site in the language itself, and what dogfooding at that scale exposed.

← Back to Mere: Building a Language