Skip to main content

audio_plugin_bsd/
plugin.rs

1//! The host-side [`Plugin`] trait, the [`HostPlugin`] adapter that bridges the
2//! C ABI to it, and the [`AudioNodeAdapter`] that wraps an opaque plugin handle
3//! behind [`audio_core_bsd::AudioNode`].
4//!
5//! See [`crate`] for why plugins cross the `.so` boundary as `extern "C"`
6//! symbols rather than Rust trait objects.
7
8use std::ffi::c_void;
9
10use audio_core_bsd::{
11    AudioFrame, AudioNode, PortDescriptor, PortDirection, ProcessContext, SampleFormat,
12};
13
14use crate::metadata::PluginMetadata;
15use crate::symbols::{CreateFn, DestroyFn};
16
17/// Host-side abstraction over a loaded plugin.
18///
19/// `Plugin` is the trait the host calls after the ABI check passes. The
20/// loader's own implementation ([`HostPlugin`]) bridges the plugin's
21/// `extern "C"` entry symbols to this trait, but consumers that build their
22/// own plugin adapters (e.g. for in-process test plugins) may implement it
23/// directly.
24///
25/// `Send + Sync` is required so a [`PluginLoader`](crate::PluginLoader) can be
26/// moved between setup threads; instantiation produces a `Box<dyn AudioNode>`
27/// (which is only `Send`) for the single RT thread.
28pub trait Plugin: Send + Sync {
29    /// Static identity of this plugin (name, version, description, ...).
30    fn metadata(&self) -> &PluginMetadata;
31
32    /// Construct a fresh audio-node instance backed by this plugin.
33    ///
34    /// Each call yields an independent `Box<dyn AudioNode>` whose lifetime is
35    /// bounded by the caller; dropping it releases the opaque handle (when the
36    /// plugin exposes `audio_plugin_destroy`).
37    fn instantiate(&self) -> Box<dyn AudioNode>;
38}
39
40/// Host adapter that bridges a plugin's `extern "C"` entry symbols to the
41/// [`Plugin`] trait.
42///
43/// Constructed by [`PluginLoader`](crate::PluginLoader) after a successful ABI
44/// check; not intended to be built by external callers.
45pub(crate) struct HostPlugin {
46    metadata: PluginMetadata,
47    create_fn: CreateFn,
48    destroy_fn: Option<DestroyFn>,
49}
50
51impl HostPlugin {
52    /// Build a host adapter from verified entry symbols.
53    pub(crate) fn new(
54        metadata: PluginMetadata,
55        create_fn: CreateFn,
56        destroy_fn: Option<DestroyFn>,
57    ) -> Self {
58        Self {
59            metadata,
60            create_fn,
61            destroy_fn,
62        }
63    }
64}
65
66impl Plugin for HostPlugin {
67    fn metadata(&self) -> &PluginMetadata {
68        &self.metadata
69    }
70
71    fn instantiate(&self) -> Box<dyn AudioNode> {
72        // SAFETY: `create_fn` is the plugin's verified `audio_plugin_create`
73        // entry symbol. The plugin is responsible for returning a valid,
74        // non-aliased opaque handle (or null, which the adapter treats as a
75        // no-op handle). The plugin remains loaded for at least as long as the
76        // returned adapter because the loader owns the `Library`.
77        let handle = unsafe { (self.create_fn)() };
78        Box::new(AudioNodeAdapter::new(handle, self.destroy_fn))
79    }
80}
81
82/// `AudioNode` wrapper around an opaque plugin handle.
83///
84/// # 0.1.0 scope — pass-through
85///
86/// The 0.1.0 ABI defines `create`/`destroy` but not a per-cycle `process`
87/// dispatch protocol on the opaque handle. Accordingly this adapter's
88/// [`AudioNode::process`] is a **sample pass-through**: it copies the first
89/// input frame's samples into the first output frame unchanged. This lets the
90/// Task 4 integration test verify the full load → ABI check → instantiate →
91/// process → unload lifecycle without requiring a plugin DSP protocol.
92///
93/// A follow-up milestone will define the opaque-handle C process protocol and
94/// replace this pass-through with real DSP dispatch.
95pub(crate) struct AudioNodeAdapter {
96    /// Opaque handle returned by `audio_plugin_create` (may be null).
97    handle: *mut c_void,
98    /// Matching deallocator (optional; absent ⇒ leak on drop).
99    destroy_fn: Option<DestroyFn>,
100    /// Single mono F32 input port.
101    in_port: [PortDescriptor; 1],
102    /// Single mono F32 output port.
103    out_port: [PortDescriptor; 1],
104}
105
106// SAFETY: the opaque handle is owned solely by this adapter and is only
107// touched from `process` (RT thread) and `Drop` (RT or setup thread), never
108// concurrently. The graph engine guarantees a single RT thread, and the
109// adapter is moved onto it by value. Function pointers (`Option<DestroyFn>`)
110// are `Send + Sync`. Hence the adapter as a whole is safe to send.
111unsafe impl Send for AudioNodeAdapter {}
112
113impl AudioNodeAdapter {
114    /// Build a pass-through adapter around an opaque handle.
115    pub(crate) fn new(handle: *mut c_void, destroy_fn: Option<DestroyFn>) -> Self {
116        Self {
117            handle,
118            destroy_fn,
119            in_port: [PortDescriptor::new(
120                PortDirection::Input,
121                1,
122                SampleFormat::F32,
123            )],
124            out_port: [PortDescriptor::new(
125                PortDirection::Output,
126                1,
127                SampleFormat::F32,
128            )],
129        }
130    }
131}
132
133impl AudioNode for AudioNodeAdapter {
134    fn inputs(&self) -> &[PortDescriptor] {
135        &self.in_port
136    }
137
138    fn outputs(&self) -> &[PortDescriptor] {
139        &self.out_port
140    }
141
142    fn process(
143        &mut self,
144        _ctx: &mut ProcessContext,
145        in_frames: &[AudioFrame],
146        out_frames: &mut [AudioFrame],
147    ) {
148        // RT-safe: bounded slicing, no alloc / lock / panic. The handle is not
149        // dispatched to in 0.1.0 (see the struct doc for the pass-through
150        // rationale); it is held only for lifecycle correctness.
151        let _ = self.handle;
152        let Some(inp) = in_frames.first() else {
153            return;
154        };
155        let Some(out) = out_frames.get_mut(0) else {
156            return;
157        };
158        let n = inp.samples.len().min(out.samples.len());
159        out.samples[..n].copy_from_slice(&inp.samples[..n]);
160    }
161}
162
163impl Drop for AudioNodeAdapter {
164    fn drop(&mut self) {
165        if let Some(destroy) = self.destroy_fn {
166            // SAFETY: `self.handle` was obtained from `audio_plugin_create`
167            // exactly once and has not been freed before. `destroy` is the
168            // plugin's matching deallocator. A null handle is passed through;
169            // the plugin's destroy must tolerate it (or the plugin must not
170            // return null from create).
171            unsafe { destroy(self.handle) };
172        }
173        // When `destroy_fn` is `None` the handle leaks — a documented 0.1.0
174        // limitation of plugins that do not expose `audio_plugin_destroy`.
175    }
176}
177
178#[cfg(test)]
179mod tests {
180    use super::*;
181
182    #[test]
183    fn adapter_is_pass_through_preserving_samples() {
184        let mut adapter = AudioNodeAdapter::new(std::ptr::null_mut(), None);
185        let mut ctx = ProcessContext::new(4, 0, 48_000);
186        let input = AudioFrame::from_planar(1, 48_000, vec![0.1, 0.2, -0.3, 0.5]);
187        let mut output = AudioFrame::silence(1, 4, 48_000);
188        let mut out_slice = [output];
189        adapter.process(&mut ctx, [input.clone()].as_slice(), &mut out_slice);
190        output = out_slice.into_iter().next().unwrap();
191        for (a, b) in output.samples.iter().zip(input.samples.iter()) {
192            assert!(((a - b).abs() < 1e-6));
193        }
194    }
195
196    #[test]
197    fn adapter_declares_one_mono_f32_input_and_output() {
198        let adapter = AudioNodeAdapter::new(std::ptr::null_mut(), None);
199        assert_eq!(adapter.inputs().len(), 1);
200        assert_eq!(adapter.outputs().len(), 1);
201        assert_eq!(adapter.inputs()[0].channels, 1);
202        assert_eq!(adapter.inputs()[0].sample_format, SampleFormat::F32);
203        assert_eq!(adapter.outputs()[0].direction, PortDirection::Output);
204    }
205
206    #[test]
207    fn adapter_process_with_empty_frames_does_not_panic() {
208        let mut adapter = AudioNodeAdapter::new(std::ptr::null_mut(), None);
209        let mut ctx = ProcessContext::new(0, 0, 48_000);
210        adapter.process(&mut ctx, &[], &mut []);
211    }
212
213    #[test]
214    fn adapter_process_clamps_to_shorter_buffer() {
215        // Input has 2 samples, output has room for 4 — only 2 are written.
216        let mut adapter = AudioNodeAdapter::new(std::ptr::null_mut(), None);
217        let mut ctx = ProcessContext::new(2, 0, 48_000);
218        let input = AudioFrame::from_planar(1, 48_000, vec![0.5, -0.5]);
219        let mut output = AudioFrame::silence(1, 4, 48_000);
220        let mut out_slice = [output];
221        adapter.process(&mut ctx, [input].as_slice(), &mut out_slice);
222        output = out_slice.into_iter().next().unwrap();
223        assert!(((output.samples[0] - 0.5).abs() < 1e-6));
224        assert!(((output.samples[1] + 0.5).abs() < 1e-6));
225        // Untouched tail stays zero.
226        assert!(((output.samples[2]).abs() < 1e-6));
227        assert!(((output.samples[3]).abs() < 1e-6));
228    }
229}