Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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 zero unsafe anywhere in this codebase today.
  • The one prior time a "need a native capability" problem came up — korrin-pkg needing to talk to git — the project explicitly rejected an unsafe-FFI dependency (git2) on identity grounds and shelled out to a subprocess instead (ADR 0033): "a dependency whose entire purpose is unsafe FFI 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 unsafe anywhere in this feature. The entire mechanism is ordinary Cargo dependency resolution and rustc compilation — nothing here needs an exception to unsafe_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 korrin runtime it links against are compiled together, in one rustc invocation, 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 korrin binary — the generic one on $PATH cannot 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 "git on PATH" assumption.

Alternatives considered

  • Dynamic loading (libloading/dlopen/LoadLibrary, a cdylib built 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 an unsafe exception for at least one loader crate — directly reversing the identity ADR 0033 already stated in rejecting git2 for the same reason — and needs a defined, hand-rolled extern "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, no unsafe needed 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 of minifb would 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 pkg needs to do) — deciding it first, in its own ADR, is what let ADR 0036-0038 be written without re-litigating it.