Skip to content

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.

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.

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.

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.

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.

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.

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.