Native Extensions
Overview
Section titled “Overview”Native extensions are Rust shared libraries (.dylib / .so / .dll) that Resonon loads at runtime via FFI. They let you drop into Rust when you need:
- Performance — tight DSP loops, heavy math, real-time audio processing
- System access — file I/O, networking, hardware APIs
- The Rust ecosystem — pull in any crate and expose it to Resonon
At a high level: you annotate Rust functions and structs with attribute macros (#[ext_fn], #[ext_class], …), register them with the resonon_extension! macro, and wrap them in a lib.non file that provides the Resonon-facing API.
Prerequisites
Section titled “Prerequisites”You need the Rust toolchain installed:
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | shVerify with cargo --version.
Quick Start
Section titled “Quick Start”Scaffold a native extension project:
resonon new my-ext --nativecd my-extThis creates:
my-ext/ resonon.toml # Manifest with [native] section lib.non # Resonon wrapper native/ # Rust crate Cargo.toml src/lib.rs lib/ # Built dylib goes hereBuild and test:
resonon buildresonon -e 'use "my-ext"; PRINT my-ext.double(21);'# 42Extension Functions (ext_fn)
Section titled “Extension Functions (ext_fn)”Use #[ext_fn] to expose a Rust function to Resonon:
use resonon_ext::prelude::*;
#[ext_fn(doc = "Doubles a number")]fn double(x: f64) -> f64 { x * 2.0}
#[ext_fn(doc = "Clamp a value between min and max")]fn clamp(x: f64, lo: f64, hi: f64) -> f64 { x.max(lo).min(hi)}
#[ext_fn(doc = "Reverse an array of numbers")]fn reverse(values: Vec<f64>) -> Vec<f64> { values.into_iter().rev().collect()}
resonon_extension! { name: "my_ext", functions: [double, clamp, reverse]}The doc = "..." string appears in resonon pkg inspect output.
When an extension only has functions (no classes), use the functions-only form of resonon_extension! as shown above — the classes: field can be omitted entirely.
Supported Parameter Types
Section titled “Supported Parameter Types”| Rust Type | Resonon Type | Notes |
|---|---|---|
f64 | Number | |
bool | Boolean | |
u8 | Number | Clamped to 0-255, used for MIDI notes |
&str | String | Borrowed, read-only |
String | String | Owned |
Vec<f64> | Array | Array of numbers |
Supported Return Types
Section titled “Supported Return Types”| Rust Type | Resonon Type | Notes |
|---|---|---|
f64 | Number | |
bool | Boolean | |
u8 | Number | |
String | String | Use String, not &str |
Vec<f64> | Array | |
() | Nul | No return value |
Extension Classes (ext_class)
Section titled “Extension Classes (ext_class)”For stateful objects, mark a struct with #[ext_class]. This generates an internal object store that manages instances behind opaque handles.
use resonon_ext::prelude::*;
/// A step sequencer that cycles through values.#[ext_class]struct Stepper { values: Vec<f64>, index: usize,}When your extension uses classes, the resonon_extension! invocation must include the classes: field so that the macro generates resonon_ext_drop_handle — the cleanup function the runtime calls when handles are garbage-collected:
resonon_extension! { name: "my_ext", functions: [stepper_new, stepper_next, stepper_reset], classes: [Stepper]}Without classes: [Stepper], handles will leak because resonon_ext_drop_handle is never generated.
Constructors (ext_new)
Section titled “Constructors (ext_new)”Use #[ext_new] to define how Resonon creates an instance of a class. The function must return the class type — Resonon receives an opaque handle.
#[ext_new(doc = "Create a stepper from an array of values")]fn stepper_new(values: Vec<f64>) -> Stepper { Stepper { values, index: 0 }}Methods (ext_method)
Section titled “Methods (ext_method)”Use #[ext_method(ClassName)] to define methods on a class. The first parameter is a reference to the class instance:
&ClassName— read-only access&mut ClassName— read-write access
#[ext_method(Stepper, doc = "Get the next value in the sequence")]fn stepper_next(s: &mut Stepper) -> f64 { if s.values.is_empty() { return 0.0; } let val = s.values[s.index % s.values.len()]; s.index += 1; val}
#[ext_method(Stepper, doc = "Reset to the beginning")]fn stepper_reset(s: &mut Stepper) { s.index = 0;}
#[ext_method(Stepper, doc = "Number of values in the sequence")]fn stepper_len(s: &Stepper) -> f64 { s.values.len() as f64}All class functions (constructor + methods) go in the functions list of resonon_extension!.
The lib.non Wrapper
Section titled “The lib.non Wrapper”Native functions aren’t called directly — you wrap them in a lib.non file that provides the Resonon-facing API:
/// my-ext — custom pattern generators.
/// Doubles a number.fn double(x) { __ext("my_ext", "double", x);}
/// Create a new stepper from values.fn Stepper(values) { __ext("my_ext", "stepper_new", values);}The __ext(extension_name, function_name, ...args) built-in dispatches to native code. Users import your package normally:
use "my-ext";
let s = my-ext.Stepper([60, 62, 64, 67]);PRINT my-ext.stepper_next(s); // 60PRINT my-ext.stepper_next(s); // 62Building
Section titled “Building”Build the native extension from your project directory:
resonon buildThis runs cargo build --release on the native crate and copies the dylib to lib/.
Dylib Naming
Section titled “Dylib Naming”Resonon normalizes extension names by replacing hyphens with underscores. The naming conventions are:
| Variant | Pattern | Example |
|---|---|---|
| Generic | <name>.<ext> | my_ext.dylib |
| Platform-specific | <name>-<os>-<arch>.<ext> | my_ext-macos-aarch64.dylib |
| Cargo output | lib<name>.<ext> | libmy_ext.dylib |
When loading an extension, Resonon searches in this priority order:
- Nested platform-specific:
<dir>/<name>/lib/<name>-<os>-<arch>.<ext> - Flat platform-specific:
<dir>/<name>-<os>-<arch>.<ext> - Nested generic:
<dir>/<name>/lib/<name>.<ext> - Flat generic:
<dir>/<name>.<ext>
Platform-specific binaries are preferred over generic ones so that cross-platform packages work correctly.
Binary Distribution
Section titled “Binary Distribution”For cross-platform packages, build with the --dist flag:
resonon build --distThis produces a platform-tagged filename like my_ext-macos-aarch64.dylib. Commit these to your repo so users on that platform can skip compiling from source.
The scaffolded .gitignore is already configured:
# Generic builds (local development)lib/*.dyliblib/*.solib/*.dll
# Keep platform-specific binaries (distribution)!lib/*-*-*.dylib!lib/*-*-*.so!lib/*-*-*.dll
native/target/Hot Reload
Section titled “Hot Reload”In the REPL, reload a native extension without restarting:
reload_ext("my_ext")This unloads the old dylib and loads the fresh build. Workflow:
- Edit
native/src/lib.rs - Run
resonon buildin another terminal - In the REPL:
reload_ext("my_ext")
Distribution
Section titled “Distribution”When you publish a native extension package to GitHub:
- Build platform binaries:
resonon build --dist - Commit the
lib/*-*-*.*files - Push to GitHub
When users run resonon pkg install gh:you/my-ext:
- If a pre-built binary matches their platform, it’s used immediately
- If not, Resonon auto-builds from source (requires Rust toolchain)
Type Mapping Reference
Section titled “Type Mapping Reference”Complete mapping between Resonon values and Rust types across the FFI boundary:
| Resonon Value | ExtValue Variant | Rust Parameter | Rust Return |
|---|---|---|---|
42, 3.14 | Number(f64) | f64 | f64 |
true, false | Boolean(bool) | bool | bool |
"hello" | String | &str or String | String |
[1, 2, 3] | Array | Vec<f64> | Vec<f64> |
| MIDI note | Note(u8) | u8 | u8 |
~ (rest) | Rest | — | — |
| Object handle | Handle(u64) | — | — |
| (nothing) | Nul | — | () |
See Also
Section titled “See Also”- Package Management — Installing and publishing packages
- Modules — The
useimport system - Built-in Functions — The
__ext()function