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.
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.