audio-plugin-bsd 0.1.1

Dynamic .so audio-plugin loader with ABI verification and FreeBSD Capsicum/pdfork per-process sandboxing for real-time audio in Rust
Documentation
# audio-plugin-bsd

[![License: BSD-2-Clause](https://img.shields.io/badge/license-BSD--2--Clause-blue.svg)](./LICENSE)
[![Crates.io](https://img.shields.io/crates/v/audio-plugin-bsd.svg)](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

| Type | Description |
|------|-------------|
| [`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

| Dependency | Version | Purpose |
|------------|---------|---------|
| `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/).