Documentation

Native modules

A screen's logic in Rust, C, C++, or Zig — no JNI, no header files to write by hand.

A screen's own logic

A sibling file next to +screen.wh is that screen's native logic. The extension picks the language: +screen.rs, .c, .cc, .cpp, or .zig.

// src/routes/report/+screen.rs — plain Rust; `pub` is the export rule
pub struct Summary { pub total: f64, pub outliers: Vec<f64> }
pub fn analyse(json: &str) -> Result<Summary, String> { … }
// src/routes/report/+screen.wh
import $native

val s = $derived(analyse(raw))

Types are declared in the native source and flow outward; the .wh file imports and declares nothing. $native always resolves to the current screen's sibling — it names neither the file nor the language, because the extension already chose.

What crosses

Primitives, String, structs and enums built from those, Vec<T>, Option<T>, and Result<T, E> — as a value, not an exception. Anything else is a build error naming the vocabulary. Every crossing function is suspend, since a call across the boundary is never assumed to be fast.

An app-scoped module

Logic more than one screen needs lives under src/lib/ instead, named the way any other $lib import is:

// src/lib/codec.rs
pub fn decode(bytes: &[u8]) -> Result<String, String> { … }
import $native.codec

val text = decode(bytes)

Declaring dependencies

[native]
abis = ["arm64-v8a", "x86_64"]

[native.rust.dependencies]
serde = "1"

[native.module.codec]
include = ["vendor/codec"]
link = ["z"]
standard = "c17"
flags = ["-O2"]

Rust crates go in [native.rust.dependencies]. A C or C++ module states its own include, link, standard, and flags under [native.module.<name>]. [native] abis is stated once for the app, not per module.

Go, TypeScript, and Python are refused as native modules. Their runtimes are process singletons — there is no way to have two independent copies in one app the way Rust, C, C++, and Zig all allow. A tool written in one of those languages is a separate process, reached the way any program is, not a module.

$tool

Running whitehall init --app inside an existing program gives that program a screen without discarding it. $tool is the bridge: $tool.run is the program's main with its arguments, and $tool.output, $tool.errors, $tool.running read its state.

See Also