13. Native modules
A native module is a Korrin module implemented as a Rust crate and linked
into the korrin binary at compile time, rather than a src/lib.kor file
(ADR 0035). It is a distinct
kind of dependency from a package (§12) — same manifest
table, same git/tag/branch/rev fetching, but a different native = true flag, a different validity check, and a different consequence: using
one means the project's own korrin binary must be rebuilt from source with
that crate compiled in.
There is no dynamic loading and no sandboxed bytecode format (WASM) —
korrin's own workspace forbids unsafe code everywhere
(ADR 0035), and a native
module is expected to make real, direct calls into the operating system (open
a window, read a device) that a sandboxed guest could not.
13.1 Declaring a native dependency
[dependencies]
gamekorr = { git = "https://github.com/korrin-lang/gamekorr", tag = "v0.1.0", native = true }
Everything §12.1 says about a
[dependencies] entry — the local name is what you import it by,
independent of the upstream crate's own name; exactly one of tag/branch/
rev; a name collision with a built-in module is rejected — applies
unchanged. The only difference native = true makes is what a fetched
checkout is validated against: a Cargo.toml at its root (the one thing
checkable without invoking rustc) rather than a src/lib.kor entry file.
The leaf-only rule (§12.4) still
applies if the fetched crate happens to carry its own korrin.toml.
import "gamekorr" resolves exactly like a plain package's bare specifier —
same merged namespace, same E0314 if the name is absent —
except that what actually satisfies the import is a Rust Environment a
linked-in crate builds, not a compiled .kor file
(ADR 0036).
13.2 Writing one: the NativeModule trait
A native-module crate depends on korrin (via the same git-rev pin
described in §13.4) and implements one trait:
pub trait NativeModule {
fn build(&self) -> Environment;
}
build is called once per run, the first time a program imports the module —
the same "build once, cache forever" contract a built-in module like math
already has. It returns an Environment populated the same way math's and
io's own Rust source populates one: ordinary functions via Value::Native,
and — where a module needs its own stateful object type, like an open
window — values built with Value::native_object (§13.3).
The crate exposes exactly one well-known free function at its root, the
thing a generated project's main.rs calls:
pub fn native_module() -> Box<dyn korrin::NativeModule>
This mirrors the existing math::module() / io::module() convention a
built-in module already follows — Rust has no reflection to discover "what
to construct" any other way.
A native module's own Rust dependencies (a windowing crate, for example)
talk to the operating system directly. They never go through
Host — Host exists only to reach Korrin's own
engine state (printing, script arguments, calling back into a Korrin
function), which a native module needs only if it wants to invoke a Korrin
callback; nothing about opening a window or reading a file requires it.
13.3 The native-object value
A native function may return an opaque, stateful object with its own
methods — Korrin's twelfth value kind (§3.1).
From a Korrin program's side, it behaves exactly like a built-in str/
list/map value with methods: . on it dispatches to a method table, and
nothing about the call syntax distinguishes it from any other method call.
win = gamekorr.open(640, 480, "hello")
win.draw_rect(10, 10, 100, 100, 16763904)
type(win)
(Illustrative only — gamekorr is a hypothetical native module, not one
linked into the binary that runs this specification's examples, so this
block is checked for valid syntax but not executed.)
type() reports whatever name the module chose when it built the object —
gamekorr's own Window, not a generic "native" label — the same way
type() on an instance reports its class's name rather than a fixed
"instance" string. Two objects returned by two separate calls (two windows,
say) never compare equal to each other by value; a native object's == is
identity-only, the same stance already taken for function/class/
instance values (§3.3).
There is exactly one method table per native object type, fixed by the
module that built it — not a general, extensible, multi-type registry
(ADR 0036). This is enough for a
module like GameKorr's Window; a module needing more than one distinct
native object type builds each with its own method table, the same way it
would define more than one Korrin class.
13.4 korrin pkg build
Because there is no dynamic loading, using a native module means compiling a
project-specific korrin binary that has it linked in. korrin pkg build
does this:
- Regenerates
.korrin/native-runner/— a small, gitignored,@generated — do not edit by handCargo project (the same conventionkorrin.lockuses) that depends onkorrin-cliplus every native dependencykorrin.lockcurrently records. - Runs
cargo build --releaseinside it. - On success, writes a wrapper script at the project root —
./korrinon Unix,./korrin.cmdon Windows — forwarding to the binary that build produced.
korrin-cli's own dependency in the generated project is pinned to the
exact commit and git remote the currently-running korrin binary was
itself built from, embedded at its own compile time
(ADR 0037) — this is
specifically the checkout's own origin remote, not a fixed upstream URL,
so that a fork's korrin pkg build links against that fork's own code.
This runs automatically after add/install/remove/update, but
only when those commands actually change the set of native dependencies
recorded in korrin.lock — adding or updating a plain .kor package in a
project that also happens to have a native dependency does not trigger a
rebuild. Run korrin pkg build directly to retry after a compile failure,
or to force a rebuild after upgrading korrin itself (a stale pinned
korrin-cli revision is otherwise left alone, not auto-detected — the same
"explicit update, not automatic tracking" stance
§12.2 already takes for a
branch-pinned package going stale).
Which binary actually runs a program is never auto-detected. The
generic korrin on $PATH and a project's generated wrapper are two
separate binaries; running the wrong one for a project with native
dependencies fails with the ordinary E0314 an absent import always
produces, not a silent fallback or a crash
(ADR 0038).
13.5 Errors
korrin pkg operations on a native dependency can fail in ways a plain
package's cannot, printed the same non-diagnostic way §12.5 describes:
korrin pkg: "gamekorr" (https://github.com/korrin-lang/gamekorr) is marked
native but has no Cargo.toml
korrin pkg: could not run `cargo` — is a Rust toolchain installed and on
PATH? (...)
korrin pkg: building this project's native korrin binary failed:
<cargo's own compiler output, unmodified>
The last of these prints cargo's/rustc's own stderr verbatim — it is
never re-parsed or re-summarized, since that output belongs to the Rust
toolchain, not to Korrin.
No new Korrin diagnostic code exists for native modules.
import "gamekorr" through a binary that never linked it in fails exactly
like an unresolved package import already does — E0314,
the same category §12.6 already covers.
import "gamekorr"
# error: E0314