Native Extensions: Safety & Threading
A native extension is Rust code loaded into the resonon process. It is fast and can reach anything the process can, which is also why its limits are strict. This page is about those limits and the reasons for them.
It computes; it does not process audio
Section titled “It computes; it does not process audio”An extension function runs synchronously on the interpreter thread — the thread that runs your code — and never on the audio thread. What crosses its boundary is numbers, booleans, strings and arrays of numbers: there is no buffer, block or channel type.
That split is deliberate. The audio thread has a deadline every few milliseconds and may not wait,
allocate or touch the filesystem; arbitrary Rust behind an FFI call can do all three. So an
extension computes things — a wavetable, a tuning, a pattern — and the audio path is written as a
dsp block, which the engine compiles into its graph under the audio thread’s rules (see
The Audio-Thread Contract).
Loading is lazy: the first call into an extension opens its library, which allocates and reads the disk — on the interpreter thread, where that is allowed.
It is trusted code
Section titled “It is trusted code”Extensions are not sandboxed. Loading one runs its code with the process’s rights — even
resonon pkg inspect loads an extension to list its functions. Install extensions you trust, as
you would a plugin.
Panics stop at the boundary
Section titled “Panics stop at the boundary”A panic may not unwind across an extern "C" boundary: when one escapes, the process aborts, and
the host cannot catch it. VST3, CLAP and AU impose the same rule on plugins.
So the functions that ext_fn, ext_new and ext_method generate catch the panic inside your
library, before it reaches the boundary, and hand it over as a resonon error with your panic
message. Code that crosses the boundary without them has to do the same itself — see
the boundary rules.
Returning an error is still the better failure: a panic in the middle of a method can leave that object half-updated. Other objects are not affected.
Handles belong to their extension
Section titled “Handles belong to their extension”An ext_class instance reaches resonon as an opaque handle that points into the loaded library.
That is why a handle means nothing to another extension — passing it there is an error, not a lookup
of some unrelated object — and why reload_ext refuses to unload an extension while any of its
objects is alive: the handles would point at nothing.
One lock per class
Section titled “One lock per class”Each class has one lock that covers all its instances, so two calls into a class never race. The price is that a long-running method holds up every other call into the same class, from any object — and every call runs on the interpreter thread, so it holds up the code you run too.
Memory is handed over, not shared
Section titled “Memory is handed over, not shared”Strings and arrays cross the boundary as copies, so neither side keeps a reference into memory the other owns. The value is handed over; what the extension keeps stays its own.
Related
Section titled “Related”- Ship a Native Extension — writing and publishing one
- Native Extensions — macros, types, boundary rules and library lookup