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):

//! my-ext — custom pattern generators.
/// Doubles a number.
fn double(x) {
return __ext("my_ext", "double", x);
}
/// Creates a stepper from values.
fn Stepper(values) {
return __ext("my_ext", "stepper_new", values);
}
/// The stepper's next value.
fn stepper_next(s) {
return __ext("my_ext", "stepper_next", s);
}
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(my_ext.stepper_next(s)); // 60
print(my_ext.stepper_next(s)); // 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.