Skip to main content

audio_plugin_bsd/
abi.rs

1//! Audio-plugin ABI version encoding and compatibility rules.
2//!
3//! Every plugin `cdylib` must expose two `extern "C"` symbols describing its
4//! ABI contract with the host:
5//!
6//! - `audio_plugin_abi_magic()  -> u32` — must equal
7//!   [`AUDIO_PLUGIN_ABI_MAGIC`] (`0x41504C47`, ASCII `"APLG"`).
8//! - `audio_plugin_abi_version() -> u32` — a version word encoded with
9//!   [`encode_abi_version`]: the **upper 16 bits are the major** version, the
10//!   **lower 16 bits are the minor** version.
11//!
12//! # Compatibility
13//!
14//! A plugin is ABI-compatible with the host when **both** hold:
15//!
16//! 1. its magic equals [`AUDIO_PLUGIN_ABI_MAGIC`], and
17//! 2. its version **major** equals the host major (see
18//!    [`is_abi_compatible`]).
19//!
20//! The minor version may differ: minor is reserved for additive,
21//! backwards-compatible changes (new optional entry symbols, new metadata
22//! fields). A major bump is a breaking change and the host will refuse to
23//! load the plugin.
24//!
25//! The magic is a separate symbol from the version word, so the loader checks
26//! it independently — compare the plugin's `audio_plugin_abi_magic()` return
27//! value against [`AUDIO_PLUGIN_ABI_MAGIC`].
28
29/// Magic word every plugin must return from `audio_plugin_abi_magic()`.
30///
31/// ASCII `"APLG"` (`0x41 0x50 0x4C 0x47`), chosen to distinguish an audio
32/// plugin from an arbitrary shared library that happens to expose a
33/// similarly-named symbol.
34pub const AUDIO_PLUGIN_ABI_MAGIC: u32 = 0x4150_4C47;
35
36/// ABI version exposed by **this host**: `1.0`.
37///
38/// Encoded as `(1 << 16) | 0` (see [`encode_abi_version`]). The unit test
39/// `host_version_matches_encode_helper` locks this relationship so the
40/// constant cannot drift from the encode helper.
41pub const AUDIO_PLUGIN_ABI_VERSION: u32 = 1 << 16;
42
43/// Encode a `(major, minor)` pair into a single ABI version word.
44///
45/// The upper 16 bits hold `major`, the lower 16 bits hold `minor`.
46#[must_use]
47pub fn encode_abi_version(major: u16, minor: u16) -> u32 {
48    (u32::from(major) << 16) | u32::from(minor)
49}
50
51/// Decode the **major** component (upper 16 bits) of an encoded ABI version
52/// word.
53///
54/// `version >> 16` is in `0..=u16::MAX` by construction, so the `try_from`
55/// always succeeds; `unwrap_or_default` is unreachable and keeps this
56/// panic-free (no `as`-cast, no panicking unwrap).
57#[must_use]
58pub fn abi_major(version: u32) -> u16 {
59    u16::try_from(version >> 16).unwrap_or_default()
60}
61
62/// Decode the **minor** component (lower 16 bits) of an encoded ABI version
63/// word.
64///
65/// `version & 0xFFFF` is in `0..=u16::MAX` by construction, so the `try_from`
66/// always succeeds; `unwrap_or_default` is unreachable and keeps this
67/// panic-free.
68#[must_use]
69pub fn abi_minor(version: u32) -> u16 {
70    u16::try_from(version & 0xFFFF).unwrap_or_default()
71}
72
73/// Return `true` when `host_version` and `plugin_version` are
74/// ABI-compatible, i.e. their **major** components are equal.
75///
76/// The minor component may differ (additive changes only). The ABI **magic**
77/// is checked separately by the loader — compare the plugin's
78/// `audio_plugin_abi_magic()` return value against
79/// [`AUDIO_PLUGIN_ABI_MAGIC`].
80#[must_use]
81pub fn is_abi_compatible(host_version: u32, plugin_version: u32) -> bool {
82    abi_major(host_version) == abi_major(plugin_version)
83}
84
85/// A typed, validated wrapper around an encoded ABI version word.
86///
87/// Construct with [`AbiVersion::new`] (from `major` / `minor`) or
88/// [`AbiVersion::decode`] (from a raw word read off a plugin symbol). It is
89/// `Copy`, so it is passed and compared by value.
90#[derive(Debug, Clone, Copy, PartialEq, Eq)]
91pub struct AbiVersion {
92    /// The raw encoded version word (`major << 16 | minor`).
93    raw: u32,
94}
95
96impl AbiVersion {
97    /// Construct an [`AbiVersion`] from explicit `(major, minor)` components.
98    #[must_use]
99    pub fn new(major: u16, minor: u16) -> Self {
100        Self {
101            raw: encode_abi_version(major, minor),
102        }
103    }
104
105    /// Wrap an already-encoded version word without re-validating.
106    #[must_use]
107    pub fn decode(raw: u32) -> Self {
108        Self { raw }
109    }
110
111    /// The raw encoded version word.
112    #[must_use]
113    pub fn raw(self) -> u32 {
114        self.raw
115    }
116
117    /// The major component (upper 16 bits).
118    #[must_use]
119    pub fn major(self) -> u16 {
120        abi_major(self.raw)
121    }
122
123    /// The minor component (lower 16 bits).
124    #[must_use]
125    pub fn minor(self) -> u16 {
126        abi_minor(self.raw)
127    }
128
129    /// Return `true` when `self` and `other` share the same **major**
130    /// component (i.e. they are ABI-compatible).
131    #[must_use]
132    pub fn is_compatible_with(self, other: Self) -> bool {
133        self.major() == other.major()
134    }
135}
136
137#[cfg(test)]
138mod tests {
139    use super::{
140        abi_major, abi_minor, encode_abi_version, is_abi_compatible, AbiVersion,
141        AUDIO_PLUGIN_ABI_MAGIC, AUDIO_PLUGIN_ABI_VERSION,
142    };
143
144    #[test]
145    fn host_version_matches_encode_helper() {
146        // Locks the host constant against the encode helper.
147        assert_eq!(AUDIO_PLUGIN_ABI_VERSION, encode_abi_version(1, 0));
148        assert_eq!(abi_major(AUDIO_PLUGIN_ABI_VERSION), 1);
149        assert_eq!(abi_minor(AUDIO_PLUGIN_ABI_VERSION), 0);
150    }
151
152    #[test]
153    fn magic_is_aplg_ascii() {
154        // "APLG" big-endian: A=0x41 P=0x50 L=0x4C G=0x47.
155        assert_eq!(AUDIO_PLUGIN_ABI_MAGIC, 0x4150_4C47);
156    }
157
158    #[test]
159    fn encode_decode_roundtrip_typical() {
160        let raw = encode_abi_version(3, 7);
161        assert_eq!(abi_major(raw), 3);
162        assert_eq!(abi_minor(raw), 7);
163        assert_eq!(raw, 0x0003_0007);
164    }
165
166    #[test]
167    fn encode_packs_major_high_minor_low() {
168        // major occupies the high half, minor the low half.
169        assert_eq!(encode_abi_version(1, 0), 0x0001_0000);
170        assert_eq!(encode_abi_version(0, 1), 0x0000_0001);
171    }
172
173    #[test]
174    fn encode_decode_roundtrip_zero_zero() {
175        let raw = encode_abi_version(0, 0);
176        assert_eq!(abi_major(raw), 0);
177        assert_eq!(abi_minor(raw), 0);
178        assert_eq!(raw, 0);
179    }
180
181    #[test]
182    fn encode_decode_roundtrip_max_max() {
183        let raw = encode_abi_version(u16::MAX, u16::MAX);
184        assert_eq!(abi_major(raw), u16::MAX);
185        assert_eq!(abi_minor(raw), u16::MAX);
186        assert_eq!(raw, u32::MAX);
187    }
188
189    #[test]
190    fn same_major_minor_differs_is_compatible() {
191        let host = encode_abi_version(1, 0);
192        let plugin = encode_abi_version(1, 9);
193        assert!(is_abi_compatible(host, plugin));
194    }
195
196    #[test]
197    fn major_differs_is_incompatible_even_if_minor_equal() {
198        let host = encode_abi_version(1, 5);
199        let plugin = encode_abi_version(2, 5);
200        assert!(!is_abi_compatible(host, plugin));
201    }
202
203    #[test]
204    fn identical_versions_are_compatible() {
205        let v = encode_abi_version(1, 0);
206        assert!(is_abi_compatible(v, v));
207    }
208
209    #[test]
210    fn is_abi_compatible_is_symmetric() {
211        let a = encode_abi_version(1, 2);
212        let b = encode_abi_version(1, 9);
213        assert_eq!(is_abi_compatible(a, b), is_abi_compatible(b, a));
214    }
215
216    #[test]
217    fn abi_version_new_roundtrips_through_major_minor() {
218        let v = AbiVersion::new(4, 2);
219        assert_eq!(v.major(), 4);
220        assert_eq!(v.minor(), 2);
221        assert_eq!(v.raw(), encode_abi_version(4, 2));
222    }
223
224    #[test]
225    fn abi_version_decode_preserves_raw() {
226        let raw = encode_abi_version(7, 1);
227        let v = AbiVersion::decode(raw);
228        assert_eq!(v.raw(), raw);
229        assert_eq!(v, AbiVersion::new(7, 1));
230    }
231
232    #[test]
233    fn abi_version_is_compatible_with_major_rule() {
234        let host = AbiVersion::new(1, 0);
235        assert!(host.is_compatible_with(AbiVersion::new(1, 99)));
236        assert!(!host.is_compatible_with(AbiVersion::new(2, 0)));
237    }
238
239    #[test]
240    fn abi_version_derives_copy_eq_debug() {
241        let a = AbiVersion::new(1, 0);
242        let b = a; // Copy
243        assert_eq!(a, b);
244        assert_eq!(format!("{a:?}"), "AbiVersion { raw: 65536 }");
245    }
246
247    // --- property tests -------------------------------------------------
248
249    use proptest::prelude::*;
250
251    proptest! {
252        #[test]
253        fn encode_decode_roundtrip(major in 0u16..=u16::MAX, minor in 0u16..=u16::MAX) {
254            let raw = encode_abi_version(major, minor);
255            prop_assert_eq!(abi_major(raw), major);
256            prop_assert_eq!(abi_minor(raw), minor);
257        }
258
259        #[test]
260        fn abiversion_new_decode_roundtrip(major in 0u16..=u16::MAX, minor in 0u16..=u16::MAX) {
261            let v = AbiVersion::new(major, minor);
262            prop_assert_eq!(AbiVersion::decode(v.raw()), v);
263            prop_assert_eq!(v.major(), major);
264            prop_assert_eq!(v.minor(), minor);
265        }
266
267        #[test]
268        fn compatibility_depends_only_on_major(
269            h_major in 0u16..=u16::MAX,
270            h_minor in 0u16..=u16::MAX,
271            p_minor in 0u16..=u16::MAX,
272        ) {
273            let host = encode_abi_version(h_major, h_minor);
274            // same major -> always compatible regardless of minor
275            let same = encode_abi_version(h_major, p_minor);
276            prop_assert!(is_abi_compatible(host, same));
277            // different major (when possible) -> never compatible
278            let other_major = if h_major == u16::MAX { 0 } else { h_major.wrapping_add(1) };
279            if other_major != h_major {
280                let diff = encode_abi_version(other_major, p_minor);
281                prop_assert!(!is_abi_compatible(host, diff));
282            }
283        }
284    }
285}