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

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:

  1. Regenerates .korrin/native-runner/ — a small, gitignored, @generated — do not edit by hand Cargo project (the same convention korrin.lock uses) that depends on korrin-cli plus every native dependency korrin.lock currently records.
  2. Runs cargo build --release inside it.
  3. On success, writes a wrapper script at the project root — ./korrin on Unix, ./korrin.cmd on 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