audio_plugin_bsd/symbols.rs
1//! C ABI symbol contract: the entry symbols every plugin must expose, their
2//! function-pointer types, and the `#[repr(C)]` metadata struct returned by
3//! `audio_plugin_metadata`.
4//!
5//! Plugins cross the `.so` boundary exclusively through the plain `extern "C"`
6//! symbols named here. The host never reinterpret-casts the returned pointers
7//! as Rust trait objects (that would be undefined behaviour); it only calls
8//! further `extern "C"` functions on them.
9//!
10//! # Required symbols
11//!
12//! | Symbol | C signature | Purpose |
13//! |---|---|---|
14//! | [`AUDIO_PLUGIN_ABI_MAGIC_SYMBOL`] | `unsafe extern "C" fn() -> u32` | Returns [`AUDIO_PLUGIN_ABI_MAGIC`]. |
15//! | [`AUDIO_PLUGIN_ABI_VERSION_SYMBOL`] | `unsafe extern "C" fn() -> u32` | Encoded `(major, minor)` word. |
16//! | [`AUDIO_PLUGIN_METADATA_SYMBOL`] | `unsafe extern "C" fn() -> *const RawPluginMetadata` | Static identity. |
17//! | [`AUDIO_PLUGIN_CREATE_SYMBOL`] | `unsafe extern "C" fn() -> *mut c_void` | Allocate an opaque plugin handle. |
18//!
19//! # Optional symbol
20//!
21//! | Symbol | C signature | Purpose |
22//! |---|---|---|
23//! | [`AUDIO_PLUGIN_DESTROY_SYMBOL`] | `unsafe extern "C" fn(*mut c_void)` | Free a handle from `create`. If absent the handle leaks (documented 0.1.0 limitation). |
24
25use crate::error::{PluginError, Result};
26use crate::metadata::PluginMetadata;
27
28/// Symbol name a plugin exposes to report the ABI magic word.
29pub const AUDIO_PLUGIN_ABI_MAGIC_SYMBOL: &str = "audio_plugin_abi_magic";
30
31/// Symbol name a plugin exposes to report its encoded ABI version.
32pub const AUDIO_PLUGIN_ABI_VERSION_SYMBOL: &str = "audio_plugin_abi_version";
33
34/// Symbol name a plugin exposes to report its static metadata.
35pub const AUDIO_PLUGIN_METADATA_SYMBOL: &str = "audio_plugin_metadata";
36
37/// Symbol name a plugin exposes to construct an opaque plugin handle.
38pub const AUDIO_PLUGIN_CREATE_SYMBOL: &str = "audio_plugin_create";
39
40/// Optional symbol name a plugin exposes to free an opaque plugin handle.
41///
42/// When absent, [`PluginLoader`](crate::PluginLoader) cannot reclaim the
43/// handle and it leaks. This is a documented limitation of the 0.1.0
44/// ABI surface.
45pub const AUDIO_PLUGIN_DESTROY_SYMBOL: &str = "audio_plugin_destroy";
46
47/// `unsafe extern "C" fn() -> u32` — returns the ABI magic word.
48pub type AbiMagicFn = unsafe extern "C" fn() -> u32;
49
50/// `unsafe extern "C" fn() -> u32` — returns the encoded ABI version word.
51pub type AbiVersionFn = unsafe extern "C" fn() -> u32;
52
53/// `unsafe extern "C" fn() -> *const RawPluginMetadata` — returns a pointer to
54/// the plugin's static metadata (owned by the plugin for the lifetime of the
55/// loaded library).
56pub type MetadataFn = unsafe extern "C" fn() -> *const RawPluginMetadata;
57
58/// `unsafe extern "C" fn() -> *mut c_void` — allocates and returns an opaque
59/// plugin handle.
60pub type CreateFn = unsafe extern "C" fn() -> *mut core::ffi::c_void;
61
62/// `unsafe extern "C" fn(*mut c_void)` — frees an opaque plugin handle
63/// previously returned by [`CreateFn`].
64pub type DestroyFn = unsafe extern "C" fn(*mut core::ffi::c_void);
65
66/// `#[repr(C)]` metadata struct returned by `audio_plugin_metadata`.
67///
68/// Each string is a `(ptr, len)` pair pointing into a byte buffer owned by the
69/// plugin for the lifetime of the loaded library (`'static` from the host's
70/// perspective). The host copies the bytes out into owned [`String`]s
71/// immediately (see [`raw_to_metadata`]) and never retains the pointers.
72///
73/// # Layout
74///
75/// The fields are ordered to match the C declaration plugin authors write:
76///
77/// ```c
78/// struct raw_plugin_metadata {
79/// const uint8_t *name_ptr; size_t name_len;
80/// const uint8_t *version_ptr; size_t version_len;
81/// const uint8_t *description_ptr; size_t description_len;
82/// uint32_t abi_version;
83/// };
84/// ```
85#[repr(C)]
86pub struct RawPluginMetadata {
87 /// Pointer to the UTF-8 plugin name bytes.
88 pub name_ptr: *const u8,
89 /// Length in bytes of the `name_ptr` range.
90 pub name_len: usize,
91 /// Pointer to the UTF-8 version bytes.
92 pub version_ptr: *const u8,
93 /// Length in bytes of the `version_ptr` range.
94 pub version_len: usize,
95 /// Pointer to the UTF-8 description bytes.
96 pub description_ptr: *const u8,
97 /// Length in bytes of the `description_ptr` range.
98 pub description_len: usize,
99 /// Encoded ABI version the plugin was built against.
100 pub abi_version: u32,
101}
102
103/// Copy a `(ptr, len)` byte range into an owned [`String`], validating UTF-8.
104///
105/// Returns [`PluginError::InvalidMetadata`] when the pointer is null or the
106/// bytes are not valid UTF-8.
107///
108/// # Safety
109///
110/// `ptr` must be null or point to at least `len` readable bytes that remain
111/// valid for the duration of this call.
112unsafe fn ptr_len_to_string(ptr: *const u8, len: usize, field: &'static str) -> Result<String> {
113 if ptr.is_null() {
114 return Err(PluginError::InvalidMetadata(format!(
115 "{field} pointer is null"
116 )));
117 }
118 // SAFETY: caller guarantees `ptr` points to `len` readable bytes.
119 let bytes = unsafe { core::slice::from_raw_parts(ptr, len) };
120 match std::str::from_utf8(bytes) {
121 Ok(s) => Ok(s.to_owned()),
122 Err(_) => Err(PluginError::InvalidMetadata(format!(
123 "{field} is not valid UTF-8"
124 ))),
125 }
126}
127
128/// Convert a raw FFI metadata pointer into an owned [`PluginMetadata`].
129///
130/// Copies every string field out of plugin-owned memory into host-owned
131/// [`String`]s, so the returned value is independent of the plugin's lifetime.
132/// `author` and `license` are not part of the 0.1.0 ABI struct and default to
133/// [`None`].
134///
135/// # Safety
136///
137/// `raw` must be null or point to a valid [`RawPluginMetadata`] whose string
138/// `(ptr, len)` fields reference readable byte ranges valid for the duration
139/// of this call (typically the lifetime of the loaded plugin library).
140///
141/// # Errors
142///
143/// Returns [`PluginError::InvalidMetadata`] when `raw` is null or any string
144/// field is null or not valid UTF-8.
145pub unsafe fn raw_to_metadata(raw: *const RawPluginMetadata) -> Result<PluginMetadata> {
146 if raw.is_null() {
147 return Err(PluginError::InvalidMetadata(
148 "metadata symbol returned a null pointer".into(),
149 ));
150 }
151 // SAFETY: caller guarantees `raw` is a valid, non-null pointer to a
152 // `RawPluginMetadata` whose fields are readable for this call.
153 let raw = unsafe { &*raw };
154 let name = unsafe { ptr_len_to_string(raw.name_ptr, raw.name_len, "name")? };
155 let version = unsafe { ptr_len_to_string(raw.version_ptr, raw.version_len, "version")? };
156 let description =
157 unsafe { ptr_len_to_string(raw.description_ptr, raw.description_len, "description")? };
158 Ok(PluginMetadata::new(
159 name,
160 version,
161 description,
162 raw.abi_version,
163 ))
164}
165
166#[cfg(test)]
167mod tests {
168 use super::*;
169
170 #[test]
171 fn symbol_name_constants_match_extern_c_convention() {
172 assert_eq!(AUDIO_PLUGIN_ABI_MAGIC_SYMBOL, "audio_plugin_abi_magic");
173 assert_eq!(AUDIO_PLUGIN_ABI_VERSION_SYMBOL, "audio_plugin_abi_version");
174 assert_eq!(AUDIO_PLUGIN_METADATA_SYMBOL, "audio_plugin_metadata");
175 assert_eq!(AUDIO_PLUGIN_CREATE_SYMBOL, "audio_plugin_create");
176 assert_eq!(AUDIO_PLUGIN_DESTROY_SYMBOL, "audio_plugin_destroy");
177 }
178
179 #[test]
180 fn raw_to_metadata_null_pointer_is_err() {
181 // SAFETY: passing a null pointer is explicitly handled.
182 let err = unsafe { raw_to_metadata(core::ptr::null()) }.unwrap_err();
183 assert!(matches!(err, PluginError::InvalidMetadata(_)));
184 assert!(err.to_string().contains("null pointer"));
185 }
186
187 #[test]
188 fn raw_to_metadata_roundtrips_owned_strings() {
189 let name = b"reverb-plate";
190 let version = b"0.3.1";
191 let description = b"plate reverb DSP";
192 let raw = RawPluginMetadata {
193 name_ptr: name.as_ptr(),
194 name_len: name.len(),
195 version_ptr: version.as_ptr(),
196 version_len: version.len(),
197 description_ptr: description.as_ptr(),
198 description_len: description.len(),
199 abi_version: 0x0001_0000,
200 };
201 // SAFETY: `raw` is a valid stack pointer whose string fields point into
202 // the static byte slices above, valid for this call.
203 let md = unsafe { raw_to_metadata(std::ptr::addr_of!(raw)) }.expect("valid metadata");
204 assert_eq!(md.name, "reverb-plate");
205 assert_eq!(md.version, "0.3.1");
206 assert_eq!(md.description, "plate reverb DSP");
207 assert_eq!(md.abi_version, 0x0001_0000);
208 assert_eq!(md.author, None);
209 assert_eq!(md.license, None);
210 }
211
212 #[test]
213 fn raw_to_metadata_null_string_pointer_is_err() {
214 let valid = b"x";
215 let raw = RawPluginMetadata {
216 name_ptr: core::ptr::null(),
217 name_len: 0,
218 version_ptr: valid.as_ptr(),
219 version_len: valid.len(),
220 description_ptr: valid.as_ptr(),
221 description_len: valid.len(),
222 abi_version: 0,
223 };
224 // SAFETY: pointer is valid; one inner field is deliberately null.
225 let err = unsafe { raw_to_metadata(std::ptr::addr_of!(raw)) }.unwrap_err();
226 assert!(matches!(err, PluginError::InvalidMetadata(_)));
227 assert!(err.to_string().contains("name pointer is null"));
228 }
229
230 #[test]
231 fn raw_to_metadata_invalid_utf8_is_err() {
232 // 0xFF is never valid UTF-8 on its own.
233 let bad = [0xFFu8, 0xFE, 0xFD];
234 let raw = RawPluginMetadata {
235 name_ptr: bad.as_ptr(),
236 name_len: bad.len(),
237 version_ptr: b"1".as_ptr(),
238 version_len: 1,
239 description_ptr: b"d".as_ptr(),
240 description_len: 1,
241 abi_version: 0,
242 };
243 // SAFETY: pointer is valid; the name bytes are deliberately invalid UTF-8.
244 let err = unsafe { raw_to_metadata(std::ptr::addr_of!(raw)) }.unwrap_err();
245 assert!(matches!(err, PluginError::InvalidMetadata(_)));
246 assert!(err.to_string().contains("name is not valid UTF-8"));
247 }
248}