Skip to content

Native Extensions

This content is for v0.7. Switch to the latest version for up-to-date documentation.

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.

You need the Rust toolchain installed:

Terminal window
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

Verify with cargo --version.

Scaffold a native extension project:

Terminal window
resonon new my-ext --native
cd my-ext

This 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 here

Build and test:

Terminal window
resonon build
resonon -e 'use "my-ext"; PRINT my-ext.double(21);'
# 42

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.

Rust TypeResonon TypeNotes
f64Number
boolBoolean
u8NumberClamped to 0-255, used for MIDI notes
&strStringBorrowed, read-only
StringStringOwned
Vec<f64>ArrayArray of numbers
Rust TypeResonon TypeNotes
f64Number
boolBoolean
u8Number
StringStringUse String, not &str
Vec<f64>Array
()NulNo return value

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.

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 }
}

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!.

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); // 60
PRINT my-ext.stepper_next(s); // 62

Build the native extension from your project directory:

Terminal window
resonon build

This runs cargo build --release on the native crate and copies the dylib to lib/.

Resonon normalizes extension names by replacing hyphens with underscores. The naming conventions are:

VariantPatternExample
Generic<name>.<ext>my_ext.dylib
Platform-specific<name>-<os>-<arch>.<ext>my_ext-macos-aarch64.dylib
Cargo outputlib<name>.<ext>libmy_ext.dylib

When loading an extension, Resonon searches in this priority order:

  1. Nested platform-specific: <dir>/<name>/lib/<name>-<os>-<arch>.<ext>
  2. Flat platform-specific: <dir>/<name>-<os>-<arch>.<ext>
  3. Nested generic: <dir>/<name>/lib/<name>.<ext>
  4. Flat generic: <dir>/<name>.<ext>

Platform-specific binaries are preferred over generic ones so that cross-platform packages work correctly.

For cross-platform packages, build with the --dist flag:

Terminal window
resonon build --dist

This 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/*.dylib
lib/*.so
lib/*.dll
# Keep platform-specific binaries (distribution)
!lib/*-*-*.dylib
!lib/*-*-*.so
!lib/*-*-*.dll
native/target/

In the REPL, reload a native extension without restarting:

reload_ext("my_ext")

This unloads the old dylib and loads the fresh build. Workflow:

  1. Edit native/src/lib.rs
  2. Run resonon build in another terminal
  3. In the REPL: reload_ext("my_ext")

When you publish a native extension package to GitHub:

  1. Build platform binaries: resonon build --dist
  2. Commit the lib/*-*-*.* files
  3. 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)

Complete mapping between Resonon values and Rust types across the FFI boundary:

Resonon ValueExtValue VariantRust ParameterRust Return
42, 3.14Number(f64)f64f64
true, falseBoolean(bool)boolbool
"hello"String&str or StringString
[1, 2, 3]ArrayVec<f64>Vec<f64>
MIDI noteNote(u8)u8u8
~ (rest)Rest
Object handleHandle(u64)
(nothing)Nul()