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
//! Reference implementation of the audio-plugin-bsd C ABI contract.
//!
//! This is a `cdylib` *example* that plugin authors can copy as a starting
//! point. It exposes the five `extern "C"` symbols the host loader looks up
//! (see `audio_plugin_bsd::symbols`). It is deliberately **self-contained**:
//! it re-declares `RawPluginMetadata` with an identical `#[repr(C)]` layout
//! rather than depending on the host crate, so a real plugin can be written
//! in any language that speaks the C ABI.
//!
//! Build it with `cargo build --example test_plugin`; the resulting shared
//! object (`libtest_plugin.so` / `.dylib` / `.dll`) is consumed by the
//! `tests/load_lifecycle.rs` end-to-end test, which drives the real `dlopen`
//! path of `PluginLoader`.
//!
//! # Symbols exposed
//!
//! | Symbol | C signature | Purpose |
//! |---|---|---|
//! | `audio_plugin_abi_magic` | `extern "C" fn() -> u32` | ABI magic word (`"APLG"`). |
//! | `audio_plugin_abi_version` | `extern "C" fn() -> u32` | Encoded `(1, 0)` version. |
//! | `audio_plugin_metadata` | `extern "C" fn() -> *const RawPluginMetadata` | Static identity. |
//! | `audio_plugin_create` | `extern "C" fn() -> *mut c_void` | Allocate an opaque handle. |
//! | `audio_plugin_destroy` | `extern "C" fn(*mut c_void)` | Free an opaque handle. |

use core::ffi::c_void;

/// ABI magic word (`"APLG"` = `0x4150_4C47`).
///
/// Must match `audio_plugin_bsd::AUDIO_PLUGIN_ABI_MAGIC`. Re-declared here so
/// the plugin compiles with no host dependency.
const ABI_MAGIC: u32 = 0x4150_4C47;

/// ABI version `(1, 0)` — major in the high 16 bits, minor in the low 16.
const ABI_VERSION: u32 = 1u32 << 16;

/// `#[repr(C)]` metadata struct returned by `audio_plugin_metadata`.
///
/// This layout **must** match `audio_plugin_bsd::symbols::RawPluginMetadata`
/// field-for-field; the C ABI guarantees the match as long as the field order
/// and types are identical. Each string is a `(ptr, len)` pair pointing into
/// static byte storage owned by the plugin for the lifetime of the library.
#[repr(C)]
pub struct RawPluginMetadata {
    /// Pointer to the UTF-8 plugin name bytes.
    pub name_ptr: *const u8,
    /// Length in bytes of the `name_ptr` range.
    pub name_len: usize,
    /// Pointer to the UTF-8 version bytes.
    pub version_ptr: *const u8,
    /// Length in bytes of the `version_ptr` range.
    pub version_len: usize,
    /// Pointer to the UTF-8 description bytes.
    pub description_ptr: *const u8,
    /// Length in bytes of the `description_ptr` range.
    pub description_len: usize,
    /// Encoded ABI version the plugin was built against.
    pub abi_version: u32,
}

// --- static identity strings ---------------------------------------------
//
// These are `static` byte slices, so their pointers remain valid for the
// entire lifetime of the loaded library — exactly the contract
// `audio_plugin_metadata` promises. The host copies the bytes out immediately
// and never retains the pointers (see `audio_plugin_bsd::symbols::raw_to_metadata`).

/// Static UTF-8 bytes for the plugin name.
static NAME: &[u8] = b"test-plugin";

/// Static UTF-8 bytes for the plugin version.
static VERSION: &[u8] = b"0.1.0";

/// Static UTF-8 bytes for the human-readable description.
static DESCRIPTION: &[u8] = b"cdylib test plugin";

/// `Sync` wrapper so the raw-pointer-bearing metadata can live in a `static`.
///
/// `RawPluginMetadata` contains `*const u8`, which is `!Sync` by default
/// (raw pointers are not auto-`Sync`). A shared `static` requires a `Sync`
/// type, so this newtype asserts the necessary invariant manually.
struct StaticMetadata(RawPluginMetadata);

// SAFETY: `StaticMetadata` wraps a `RawPluginMetadata` whose `*const u8` fields
// point exclusively into the read-only `static` byte slices above (`NAME`,
// `VERSION`, `DESCRIPTION`), and whose remaining fields are plain integers.
// Once constructed the value is never mutated, and the pointed-to byte storage
// is immutable for the lifetime of the loaded library. Sharing it across
// threads (the host may call `audio_plugin_metadata` from any thread) is
// therefore sound.
unsafe impl Sync for StaticMetadata {}

static METADATA: StaticMetadata = StaticMetadata(RawPluginMetadata {
    name_ptr: NAME.as_ptr(),
    name_len: NAME.len(),
    version_ptr: VERSION.as_ptr(),
    version_len: VERSION.len(),
    description_ptr: DESCRIPTION.as_ptr(),
    description_len: DESCRIPTION.len(),
    abi_version: ABI_VERSION,
});

/// Returns the ABI magic word.
///
/// The host loader compares the returned value against
/// `audio_plugin_bsd::AUDIO_PLUGIN_ABI_MAGIC` (`0x4150_4C47`).
#[no_mangle]
pub extern "C" fn audio_plugin_abi_magic() -> u32 {
    ABI_MAGIC
}

/// Returns the encoded ABI version word (`major << 16 | minor`).
///
/// The host accepts any plugin whose **major** component matches its own;
/// see `audio_plugin_bsd::is_abi_compatible`.
#[no_mangle]
pub extern "C" fn audio_plugin_abi_version() -> u32 {
    ABI_VERSION
}

/// Returns a pointer to the plugin's static metadata.
///
/// The pointer remains valid for the lifetime of the loaded library; the host
/// copies the string fields out immediately after the call and never retains
/// the pointers.
#[no_mangle]
pub extern "C" fn audio_plugin_metadata() -> *const RawPluginMetadata {
    // `addr_of!` avoids materializing a `&RawPluginMetadata` (which would
    // surface the `!Sync`-ness of the raw-pointer fields) and yields the field
    // address directly.
    core::ptr::addr_of!(METADATA.0)
}

/// Allocates and returns an opaque plugin handle.
///
/// The 0.1.0 host adapter treats the handle as opaque (it is not dispatched to
/// for DSP in this milestone); we therefore allocate a small boxed sentinel so
/// that [`audio_plugin_destroy`] has something meaningful to reclaim. A plugin
/// is also free to return null — the adapter tolerates it — but returning a
/// real allocation exercises the full create/destroy lifecycle.
///
/// The returned pointer is owned by the caller and must be freed with
/// [`audio_plugin_destroy`].
#[no_mangle]
pub extern "C" fn audio_plugin_create() -> *mut c_void {
    // "testplug" encoded as a u64 sentinel, purely so the allocation is
    // identifiable under a debugger.
    let boxed: Box<u64> = Box::new(0x7465_7374_706c_7567);
    Box::into_raw(boxed) as *mut c_void
}

/// Frees an opaque plugin handle returned by [`audio_plugin_create`].
///
/// A null pointer is a tolerated no-op (the host adapter forwards whatever
/// `create` returned verbatim, including null).
///
/// # Safety
///
/// `handle` must be null or a pointer previously returned by
/// [`audio_plugin_create`] that has not yet been freed.
#[no_mangle]
pub unsafe extern "C" fn audio_plugin_destroy(handle: *mut c_void) {
    if handle.is_null() {
        return;
    }
    // SAFETY: caller guarantees `handle` was produced by `audio_plugin_create`
    // exactly once and not yet freed. We reconstruct the `Box<u64>` and let it
    // drop, reclaiming the allocation.
    let _ = Box::from_raw(handle.cast::<u64>());
}