# audio-plugin-bsd
[](./LICENSE)
[](https://crates.io/crates/audio-plugin-bsd)
Dynamic `.so` audio-plugin loader with ABI verification and FreeBSD
Capsicum/`pdfork` per-process sandboxing for real-time audio in Rust.
> **Note:** The doc comments on each public item are the primary reference.
> This README is an overview only.
## Role
A standalone dynamic plugin loader for real-time audio in Rust. It loads
third-party audio plugins compiled as `cdylib` (`.so` / `.dylib` / `.dll`)
shared objects, verifies their ABI against the host, instantiates them as
`AudioNode` implementations behind a stable `extern "C"` entry point, and —
on FreeBSD — confines each plugin to a Capsicum capability sandbox
(optionally one isolated child process per plugin via `pdfork`).
## Core types
| [`PluginError`] | Loader / sandbox error enum (thiserror-backed) + `Result` alias. |
| [`AUDIO_PLUGIN_ABI_MAGIC`] | Magic word (`0x41504C47`, ASCII `"APLG"`) every plugin must return. |
| [`AUDIO_PLUGIN_ABI_VERSION`] | Host ABI version (`1.0`), encoded by [`encode_abi_version`]. |
| [`encode_abi_version`] / [`abi_major`] / [`abi_minor`] | Encode/decode the `(major, minor)` ABI version word. |
| [`is_abi_compatible`] | Major-only ABI compatibility rule. |
| [`AbiVersion`] | Typed, validated wrapper around an encoded ABI version word. |
| [`PluginMetadata`] | Plugin identity (name / version / description / author / license). |
## Wire ABI
Plugins do **not** cross the `.so` boundary as Rust trait objects
(`Box<dyn AudioNode>`). A Rust trait object's layout and vtable are defined by
the *compiling* toolchain and are **not** ABI-stable; casting a foreign
`.so`'s pointer to a Rust trait object is undefined behaviour. Instead every
plugin exposes plain `extern "C"` symbols:
- `audio_plugin_abi_magic() -> u32` — must equal `AUDIO_PLUGIN_ABI_MAGIC`.
- `audio_plugin_abi_version() -> u32` — encoded via `encode_abi_version`;
the host accepts any plugin whose **major** matches its own.
- `audio_plugin_metadata()` / `audio_plugin_create()` — identity + opaque
handle (consumed by the loader in a follow-up task).
The host never reinterpret-casts a returned pointer as a Rust trait object; it
only calls further `extern "C"` functions on it.
## Dependencies
| `audio-core-bsd` | 0.1.0 (path) | Shared `AudioNode` / `AudioFrame` contract. |
| `libloading` | 0.9 | `dlopen` / `dlsym` shared-library loading. |
| `thiserror` | 2.0 | `PluginError` derive. |
| `libc` | 0.2 | FFI types for `pdfork`. |
| `capsicum` | 0.4.5 | **FreeBSD only** — Capsicum capability mode. |
| `pdfork` | 0.1.1 | **FreeBSD only** — process-descriptor isolation. |
`capsicum` and `pdfork` are gated behind `cfg(target_os = "freebsd")` and are
**not** compiled on Linux/macOS/Windows (see [`NOTICE`](./NOTICE) for the
MPL-2.0 attribution).
## Status
**0.x — experimental.** The API is not yet frozen. Breaking changes are
expected before a 1.0 release.
- edition: 2021
- MSRV: 1.85
- license: BSD-2-Clause
## Example
```rust
use audio_plugin_bsd::{
abi_major, abi_minor, encode_abi_version, is_abi_compatible, AbiVersion,
AUDIO_PLUGIN_ABI_MAGIC, AUDIO_PLUGIN_ABI_VERSION, PluginError, PluginMetadata,
};
// The host advertises ABI 1.0.
assert_eq!(AUDIO_PLUGIN_ABI_VERSION, encode_abi_version(1, 0));
assert_eq!(abi_major(AUDIO_PLUGIN_ABI_VERSION), 1);
assert_eq!(abi_minor(AUDIO_PLUGIN_ABI_VERSION), 0);
// A plugin built against 1.3 is compatible (same major).
let plugin_v = encode_abi_version(1, 3);
assert!(is_abi_compatible(AUDIO_PLUGIN_ABI_VERSION, plugin_v));
// A plugin built against 2.0 is NOT compatible (major bump).
assert!(!is_abi_compatible(AUDIO_PLUGIN_ABI_VERSION, encode_abi_version(2, 0)));
// Typed wrapper.
let host = AbiVersion::decode(AUDIO_PLUGIN_ABI_VERSION);
assert!(host.is_compatible_with(AbiVersion::new(1, 9)));
assert!(!host.is_compatible_with(AbiVersion::new(2, 0)));
// Metadata a plugin might expose.
let meta = PluginMetadata::new("reverb-plate", "0.3.1", "plate reverb", plugin_v)
.with_author("acme-audio")
.with_license("MIT");
assert_eq!(meta.name, "reverb-plate");
// The error path the loader raises on a mismatch.
let _err = PluginError::AbiVersionMismatch {
host: AUDIO_PLUGIN_ABI_VERSION,
plugin: encode_abi_version(2, 0),
};
// The magic is checked separately from the version word.
let _magic = AUDIO_PLUGIN_ABI_MAGIC;
```
## Security warning — untrusted shared objects
Loading a `.so` executes its initialisation code in the host process.
**Never load a plugin you do not fully trust** unless it is confined. On
FreeBSD this crate confines plugins with Capsicum; on other platforms the
sandbox is a no-op and **untrusted plugins must not be loaded at all**.
## License
BSD-2-Clause. See [`LICENSE`](./LICENSE) and [`NOTICE`](./NOTICE).
Design decisions are recorded in [`docs/adr/`](./docs/adr/).