Chapter 33. Registering Native Functions
Goal
By the end of this chapter you will be able to register host functions that scripts call, by both the ergonomic and the lower-level routes.
What a native function is
A native function is a Rust function the host registers with the VM under
a name. A script calls it by that name, after a use declaration. Native
functions are the bridge: the script decides what should happen, the
native does it. The piano roll’s host::play is a native; the script
calls it, and the Rust side updates the audio voice state.
Native functions are registered after Vm::new and before the script
runs.
The ergonomic route: register_fn
The recommended route is register_fn. It accepts any Rust function or
closure of arity zero through four whose argument and return types
implement the KeleusmaType marshalling trait, which the primitive types
already do:
#![allow(unused)]
fn main() {
vm.register_fn("math::add", |a: i64, b: i64| -> i64 { a + b });
vm.register_fn("math::sin", |x: f64| -> f64 { libm::sin(x) });
}
Argument extraction, arity checking, and return-value wrapping are
handled automatically. For a function that may fail, register_fn_fallible
accepts a Result<R, VmError> return:
#![allow(unused)]
fn main() {
vm.register_fn_fallible("io::read_setting", |key: String| -> Result<String, VmError> {
fetch(&key).map_err(|e| VmError::NativeError(format!("{}", e)))
});
}
A host struct or enum crosses the boundary by deriving KeleusmaType:
#![allow(unused)]
fn main() {
#[derive(KeleusmaType, Debug, Clone)]
struct Point { x: f64, y: f64 }
}
The lower-level route: register_native_closure
register_fn cannot capture host state, because its argument is a plain
function shape. When a native must read or write state the host owns, use
register_native_closure. It takes a closure that receives the raw
&[Value] arguments and returns Result<Value, VmError>:
#![allow(unused)]
fn main() {
let voices = shared_voices.clone();
vm.register_native_closure("host::silence", move |args: &[Value]| {
let channel = match args[0] {
Value::Int(n) => n as usize,
ref other => return Err(VmError::TypeError(
format!("expected Int, got {:?}", other))),
};
voices.lock().unwrap()[channel].gate = false;
Ok(Value::Unit)
});
}
This is the route the piano roll uses for every one of its natives,
because each one captures the shared Arc<Mutex<[Voice; 8]>> voice
table. The closure owns its captured clone, and inspecting the raw
Value lets the native validate its arguments explicitly. A plain
function pointer with no captured state can instead use register_native.
Bundled libraries
The keleusma::stddsl module ships three bundles of natives, registered
in one call each:
#![allow(unused)]
fn main() {
vm.register_library(stddsl::Math); // math::sqrt, math::pow, ...
vm.register_library(stddsl::Audio); // audio::midi_to_freq, ...
vm.register_library(stddsl::Shell); // shell::getenv, shell::run, shell::sleep_ms,
// shell::read_file, shell::write_file, ...
}
The Shell bundle in V0.2.1 covers environment access (getenv,
has_env, setenv), subprocess execution (run, run_full,
run_checked, run_timeout), process termination (exit), timing (sleep_ms,
now_unix_ms), file I/O (read_file, write_file, append_file,
file_exists), stderr output (write_err, writeln_err), and host
metadata (pid, hostname, arg_count, arg, pwd, cd). See
STANDARD_LIBRARY.md for the full list.
Each bundle exposes a public SIGNATURES constant containing
source-form use declarations. Hosts that want compile-time type and
arity validation prepend the constant to the script source before
parsing; the bundled keleusma-cli does this for all three bundles. A
custom bundle can implement the Library trait and expose a parallel
SIGNATURES constant to participate in the same validation flow.
What you now know
- A native function is a host Rust function a script calls by name.
register_fnandregister_fn_fallibleare the ergonomic route, for functions whose types implementKeleusmaType.register_native_closureis the route for natives that capture host state, as every piano roll native does.register_libraryinstalls a bundled or custom set of natives.- A fallible native’s failure is handled on the script side with the
native-error construct
native(args) { ok(v) => ..., error(code) => ... }. The host reports theWorderror code by returning an error built with theKeleusmaErrorderive, which maps a fieldless enum’s variants to their discriminants. See Chapter 23.
The next chapter drives a script that yields.