35. Native modules load by compile-time linking, not dynamic loading or WASM
- Status: Accepted
- Date: 2026-09-05
Context
math and io are wired into the module loader by hand today — a closed,
compile-time match in crates/korrin/src/stdlib/mod.rs, editable only by
changing this crate's own source. Milestone 6 opens that up: a third party
should be able to write a Korrin module in Rust and have a Korrin program
import it, without editing crates/korrin itself. Milestone 7 (GameKorr,
a Pygame-style graphics library) is the concrete, already-planned first
consumer — a Window object wrapping a real windowing crate (minifb),
making real OS syscalls.
The question this ADR settles: once a native module exists as compiled Rust
code somewhere, how does it actually get into the running korrin process?
Three real candidates, and constraints already on record in this project that bear directly on the choice:
- The workspace enforces
unsafe_code = "forbid"on every crate (ADR 0013), and there is zerounsafeanywhere in this codebase today. - The one prior time a "need a native capability" problem came up —
korrin-pkgneeding to talk to git — the project explicitly rejected anunsafe-FFI dependency (git2) on identity grounds and shelled out to a subprocess instead (ADR 0033): "a dependency whose entire purpose isunsafeFFI to a C library sits against that identity, even though the lint itself only fires on this workspace's own code." - Milestone 7's own plan names a specific real dependency (
minifb) making real syscalls — windowing cannot be faked or sandboxed away without changing what GameKorr is.
Decision
A native module is a Rust crate. Using one means rebuilding the korrin
binary from source with that crate compiled in. There is no dlopen/
LoadLibrary, no cdylib plugin loading, and no WASM sandbox — the
mechanism is a real cargo build, mechanically detailed in
ADR 0037.
This has real, named precedent outside Rust: nginx's own --add-module
recompilation model for third-party modules is exactly this trade — no
runtime plugin loading, a rebuild instead.
Consequences
- Zero
unsafeanywhere in this feature. The entire mechanism is ordinary Cargo dependency resolution andrustccompilation — nothing here needs an exception tounsafe_code = "forbid", and no new ADR is ever needed to relax it for this milestone. - No Rust ABI-instability problem. A native module and the
korrinruntime it links against are compiled together, in onerustcinvocation, by construction — there is no cross-compiler-version or cross-crate-version ABI to get wrong, which a dynamic-loading design would have needed to solve from scratch (Rust has no stable ABI). - "Installing" a native module costs a rebuild, not a drop-in binary
swap. A project depending on one needs its own project-specific
korrinbinary — the generic one on$PATHcannot run a program that imports it (see ADR 0037, ADR 0038). This is the real, accepted price of this decision, named plainly rather than glossed over. - A project with any native-module dependency now also needs a working
Rust toolchain to
korrin pkg install/build, on top of M5's existing "gitonPATH" assumption.
Alternatives considered
- Dynamic loading (
libloading/dlopen/LoadLibrary, acdylibbuilt separately and loaded at runtime). The conventional sense of "plugin," and what "distributed like any other package" might suggest at first read. Rejected: needs a new ADR carving out anunsafeexception for at least one loader crate — directly reversing the identity ADR 0033 already stated in rejectinggit2for the same reason — and needs a defined, hand-rolledextern "C"ABI boundary to sidestep Rust's own unstable ABI across compiler versions, real engineering weight this milestone does not need to take on for a first cut. - WASM sandboxing (compile native modules to
wasm32-unknown-unknown, run via a pure-Rust runtime). Fully safe, real portability, nounsafeneeded with a suitable runtime (e.g.wasmi). Rejected specifically because it does not fit the already-stated plan for GameKorr: a WASM guest cannot make real OS syscalls, so windowing would have to move into Korrin's own runtime and be exposed to the guest as a curated host import — GameKorr's own choice ofminifbwould then be Korrin's choice, not GameKorr's. That is a materially more restrictive vision than "third parties bring their own native dependencies," and forecloses the actual next milestone this one exists to unblock. - Do nothing until M7 forces the question. Rejected on the same
reasoning every prior milestone plan used: the loading model shapes
nearly everything else in this milestone's design (the object model, the
manifest shape, what
korrin pkgneeds to do) — deciding it first, in its own ADR, is what let ADR 0036-0038 be written without re-litigating it.