Skip to content

Ship a Native Extension

A native extension is a Rust library that resonon loads and calls like a module. Use one to compute something — a wavetable, a scale, a pattern — that is too slow or impossible in resonon. It runs on the interpreter thread, not the audio thread: audio processing belongs in a dsp effect or dsp instrument. See Native Extensions: Safety & Threading before you ship one.

You need the Rust toolchain; check it with cargo --version.

Terminal window
resonon new my-ext --native
cd my-ext
my-ext/
resonon.toml # manifest, with a [native] section
src/
lib.non # the resonon-facing API
native/ # the Rust crate
Cargo.toml
src/lib.rs
bin/ # built libraries go here

In native/src/lib.rs, mark each function with #[ext_fn], and list it in resonon_extension!:

use resonon_ext::prelude::*;
#[ext_fn(doc = "Doubles a number")]
fn double(x: f64) -> f64 {
x * 2.0
}
resonon_extension! {
name: "my_ext",
functions: [double]
}

For an object that keeps state, add a class, a constructor and methods. Every constructor and method goes into functions, and the class into classes:

use resonon_ext::prelude::*;
/// A step sequencer that cycles through values.
#[ext_class]
struct Stepper {
values: Vec<f64>,
index: usize,
}
#[ext_new(doc = "Create a stepper from an array of values")]
fn stepper_new(values: Vec<f64>) -> Stepper {
Stepper { values, index: 0 }
}
#[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
}
resonon_extension! {
name: "my_ext",
functions: [stepper_new, stepper_next],
classes: [Stepper]
}

The macros and the types they accept are in the Native Extensions reference.

Users don’t call Rust directly. src/lib.non is an ordinary module whose functions call into the extension with __ext(extension, function, ...args). Each #[ext_class] is declared as a native class of the same name, whose methods pass this back to the extension:

//! my-ext — custom pattern generators.
/// Doubles a number.
fn double(x) {
return __ext("my_ext", "double", x);
}
/// A step sequencer that cycles through values.
native class Stepper {
/// The next value in the sequence.
fn next() -> Number {
return __ext("my_ext", "stepper_next", this);
}
}
/// Creates a stepper from values.
fn Stepper(values) -> Stepper {
return __ext("my_ext", "stepper_new", values);
}

An object the extension returns is a Stepper because lib.non declares one. An object whose class no native class declares is refused with an error.

Terminal window
resonon build

This builds the crate in release mode and copies the library into bin/. Import the package by its name; the module is bound under that name with - changed to _:

use "my-ext";
print(my_ext.double(21)); // 42
let s = my_ext.Stepper(#[60, 62, 64, 67]);
print(s.next()); // 60
print(s.next()); // 62

After a change to the Rust code, build again in a terminal, then reload the extension in your session instead of restarting it:

reload_ext("my_ext");

The reload fails while any object from the extension is still alive. Reassign or drop the variables that hold them first — s above.

Build a binary named for your platform, and commit it:

Terminal window
resonon build --dist

This writes, for example, bin/my_ext-macos-aarch64.dylib. The scaffolded .gitignore already ignores the local build and keeps these. Build and commit one per platform you support, then push the repository.

When someone runs resonon pkg install gh:you/my-ext, resonon builds the extension from source. If that build fails — on a machine without the Rust toolchain, say — the committed binary for their platform is used instead.