Skip to main content

fidius_core/
descriptor.rs

1// Copyright 2026 Colliery, Inc.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! FFI descriptor and registry types for the Fidius plugin framework.
16//!
17//! These types form the stable C ABI contract between host and plugin.
18//! All types use `#[repr(C)]` layout and are read directly from dylib memory.
19
20use std::ffi::c_char;
21use std::ffi::c_void;
22
23/// Magic bytes identifying a Fidius plugin registry.
24pub const FIDIUS_MAGIC: [u8; 8] = *b"FIDIUS\0\0";
25
26/// Current version of the `PluginRegistry` struct layout.
27pub const REGISTRY_VERSION: u32 = 1;
28
29// ABI_VERSION is derived from the crate's semver per ADR-0002.
30// Pre-1.0: every minor release is a breaking change → encode as MAJOR*10000 + MINOR*100.
31// Post-1.0: minor releases must be ABI-additive (new fields at the end, new enum variants),
32// so only MAJOR changes ABI_VERSION → encode as MAJOR*10000.
33// Patch releases are always ABI-compatible and do not affect ABI_VERSION.
34const fn parse_u32_const(s: &str) -> u32 {
35    let bytes = s.as_bytes();
36    let mut i = 0;
37    let mut n = 0u32;
38    while i < bytes.len() {
39        n = n * 10 + (bytes[i] - b'0') as u32;
40        i += 1;
41    }
42    n
43}
44
45const CRATE_MAJOR: u32 = parse_u32_const(env!("CARGO_PKG_VERSION_MAJOR"));
46const CRATE_MINOR: u32 = parse_u32_const(env!("CARGO_PKG_VERSION_MINOR"));
47
48/// Current version of the `PluginDescriptor` struct layout. Derived from the
49/// `fidius-core` crate version per ADR-0002.
50pub const ABI_VERSION: u32 = if CRATE_MAJOR == 0 {
51    CRATE_MAJOR * 10000 + CRATE_MINOR * 100
52} else {
53    CRATE_MAJOR * 10000
54};
55
56/// Buffer management strategy for an interface.
57///
58/// Selected per-trait via `#[plugin_interface(buffer = ...)]`.
59/// Determines the FFI function pointer signatures in the vtable.
60///
61/// Discriminant value `0` is reserved (previously `CallerAllocated`, removed
62/// in 0.1.0 — its value proposition was subsumed by `PluginAllocated`).
63#[repr(u8)]
64#[derive(Debug, Clone, Copy, PartialEq, Eq)]
65pub enum BufferStrategyKind {
66    /// Plugin allocates output; host frees via `PluginDescriptor::free_buffer`.
67    /// VTable fns: `(in_ptr, in_len, out_ptr, out_len) -> i32`.
68    PluginAllocated = 1,
69    /// Host provides a pre-allocated arena buffer; plugin writes its serialized
70    /// output into the buffer. Returns `STATUS_BUFFER_TOO_SMALL` (with needed
71    /// size written to `out_len`) if the arena is too small; host grows and
72    /// retries. Data is valid only until the next call.
73    ///
74    /// VTable fns: `(in_ptr, in_len, arena_ptr, arena_cap, out_offset, out_len) -> i32`.
75    ///
76    /// **Arena is allocation-avoidance, not zero-copy.** The plugin still
77    /// serializes its output (bincode-encoded by default) and copies the
78    /// bytes into the host-provided buffer; what Arena saves is the per-call
79    /// `Box<[u8]>` allocation that PluginAllocated incurs. For true byte
80    /// passthrough — the bytes themselves cross the boundary without an
81    /// encoding step — annotate the trait method with `#[wire(raw)]`. Raw
82    /// wire mode composes with both buffer strategies.
83    Arena = 2,
84}
85
86/// Static key/value pair for method-level or trait-level metadata.
87///
88/// Both `key` and `value` point to null-terminated UTF-8 C strings with
89/// `'static` lifetime (typically string literals embedded in the plugin's
90/// `.rodata`). Fidius treats values as opaque — hosts define conventions
91/// via their own metadata schemas. See ADR/spec for the `fidius.*`
92/// reserved namespace.
93#[repr(C)]
94pub struct MetaKv {
95    /// Null-terminated UTF-8 key. Never null.
96    pub key: *const c_char,
97    /// Null-terminated UTF-8 value. Never null (may be empty string).
98    pub value: *const c_char,
99}
100
101// SAFETY: MetaKv fields are static, immutable pointers to `.rodata` strings.
102unsafe impl Send for MetaKv {}
103unsafe impl Sync for MetaKv {}
104
105/// Per-method metadata entry. One entry per method in declaration order,
106/// stored in the array referenced by `PluginDescriptor::method_metadata`.
107///
108/// Methods with no `#[method_meta(...)]` annotations have `kvs: null` and
109/// `kv_count: 0` — the entry exists but is empty, so hosts can index
110/// uniformly by method index.
111#[repr(C)]
112pub struct MethodMetaEntry {
113    /// Pointer to an array of `kv_count` `MetaKv` entries, or null if this
114    /// method has no metadata.
115    pub kvs: *const MetaKv,
116    /// Number of key/value pairs for this method. Zero when `kvs` is null.
117    pub kv_count: u32,
118}
119
120// SAFETY: MethodMetaEntry fields reference static data.
121unsafe impl Send for MethodMetaEntry {}
122unsafe impl Sync for MethodMetaEntry {}
123
124impl std::fmt::Display for BufferStrategyKind {
125    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
126        match self {
127            BufferStrategyKind::PluginAllocated => write!(f, "PluginAllocated"),
128            BufferStrategyKind::Arena => write!(f, "Arena"),
129        }
130    }
131}
132
133/// Top-level registry exported by every Fidius plugin dylib.
134///
135/// Each dylib exports exactly one `FIDIUS_PLUGIN_REGISTRY` static symbol
136/// pointing to this struct. The registry contains pointers to one or more
137/// `PluginDescriptor`s (supporting multiple plugins per dylib).
138///
139/// # Safety
140///
141/// - `descriptors` must point to a valid array of `plugin_count` pointers.
142/// - Each pointer in the array must point to a valid `PluginDescriptor`.
143/// - All pointed-to data must have `'static` lifetime (typically link-time constants).
144#[repr(C)]
145pub struct PluginRegistry {
146    /// Magic bytes — must equal `FIDIUS_MAGIC` (`b"FIDIUS\0\0"`).
147    pub magic: [u8; 8],
148    /// Layout version of this struct. Must equal `REGISTRY_VERSION`.
149    pub registry_version: u32,
150    /// Number of plugin descriptors in this registry.
151    pub plugin_count: u32,
152    /// Pointer to an array of `plugin_count` descriptor pointers.
153    pub descriptors: *const *const PluginDescriptor,
154}
155
156// SAFETY: PluginRegistry contains only primitive fields and a pointer to
157// static data. The pointed-to descriptors are immutable after construction
158// and have 'static lifetime.
159unsafe impl Send for PluginRegistry {}
160unsafe impl Sync for PluginRegistry {}
161
162/// Metadata descriptor for a single plugin within a dylib.
163///
164/// Contains all information the host needs to validate and call the plugin
165/// without executing any plugin code. All string fields are pointers to
166/// static, null-terminated C strings embedded in the dylib.
167///
168/// # Safety
169///
170/// - `interface_name` and `plugin_name` must point to valid, null-terminated,
171///   UTF-8 C strings with `'static` lifetime.
172/// - `vtable` must point to a valid `#[repr(C)]` vtable struct matching the
173///   interface identified by `interface_name` and `interface_hash`.
174/// - When `buffer_strategy == PluginAllocated`, `free_buffer` must be `Some`.
175/// - All pointed-to data must outlive any `PluginHandle` derived from this descriptor.
176#[repr(C)]
177pub struct PluginDescriptor {
178    /// Size in bytes of this descriptor struct at plugin build time.
179    ///
180    /// The host reads this field FIRST (it's at offset 0) before trusting any
181    /// other offset calculation. Any field whose offset is >= `descriptor_size`
182    /// is not present in this plugin's build — the plugin was compiled against
183    /// an older fidius version that didn't have that field yet.
184    ///
185    /// Enables post-1.0 minor releases to add new fields at the end of this
186    /// struct without breaking older plugins. See ADR-0002.
187    pub descriptor_size: u32,
188    /// Descriptor struct layout version. Must equal `ABI_VERSION`.
189    pub abi_version: u32,
190    /// Null-terminated name of the trait this plugin implements (e.g., `"ImageFilter"`).
191    pub interface_name: *const c_char,
192    /// FNV-1a hash of the required method signatures. Detects ABI drift.
193    pub interface_hash: u64,
194    /// User-specified interface version from `#[plugin_interface(version = N)]`.
195    pub interface_version: u32,
196    /// Bitfield where bit N indicates optional method N is implemented.
197    /// Supports up to 64 optional methods per interface.
198    pub capabilities: u64,
199    /// Buffer management strategy this plugin's vtable expects.
200    pub buffer_strategy: u8,
201    /// Null-terminated human-readable name for this plugin implementation.
202    pub plugin_name: *const c_char,
203    /// Opaque pointer to the interface-specific `#[repr(C)]` vtable struct.
204    pub vtable: *const c_void,
205    /// Deallocation function for plugin-allocated buffers.
206    /// Must be `Some` when `buffer_strategy == PluginAllocated`.
207    /// The host calls this after reading output data to free the plugin's allocation.
208    pub free_buffer: Option<unsafe extern "C" fn(*mut u8, usize)>,
209    /// Total number of methods in the vtable (required + optional).
210    /// Used for bounds checking in `call_method`.
211    pub method_count: u32,
212    /// Pointer to an array of `method_count` `MethodMetaEntry` structs,
213    /// one per method in declaration order. Each entry may be empty
214    /// (kvs=null, kv_count=0) if the method declared no metadata.
215    ///
216    /// Null if the interface used no `#[method_meta(...)]` annotations
217    /// at all (optimization for the common case).
218    pub method_metadata: *const MethodMetaEntry,
219    /// Pointer to an array of `trait_metadata_count` `MetaKv` entries for
220    /// trait-level metadata (declared via `#[trait_meta(...)]`).
221    ///
222    /// Null if no trait-level metadata was declared.
223    pub trait_metadata: *const MetaKv,
224    /// Number of entries in `trait_metadata`. Zero when `trait_metadata`
225    /// is null.
226    pub trait_metadata_count: u32,
227}
228
229// SAFETY: PluginDescriptor fields are either primitives, pointers to static
230// data, or function pointers. All are immutable after construction and the
231// pointed-to data has 'static lifetime.
232unsafe impl Send for PluginDescriptor {}
233unsafe impl Sync for PluginDescriptor {}
234
235/// A `Sync` wrapper for a raw pointer to a `PluginDescriptor`.
236///
237/// Used in static contexts where a `*const PluginDescriptor` needs to live
238/// in a `static` variable (which requires `Sync`). The pointed-to descriptor
239/// must have `'static` lifetime.
240#[repr(transparent)]
241pub struct DescriptorPtr(pub *const PluginDescriptor);
242
243// SAFETY: The pointer targets static data that is immutable after construction.
244unsafe impl Send for DescriptorPtr {}
245unsafe impl Sync for DescriptorPtr {}
246
247impl PluginDescriptor {
248    /// Read the `interface_name` field as a Rust `&str`.
249    ///
250    /// # Safety
251    ///
252    /// `interface_name` must point to a valid, null-terminated, UTF-8 C string
253    /// that outlives the returned reference.
254    pub unsafe fn interface_name_str(&self) -> &str {
255        let cstr = unsafe { std::ffi::CStr::from_ptr(self.interface_name) };
256        cstr.to_str().expect("interface_name is not valid UTF-8")
257    }
258
259    /// Read the `plugin_name` field as a Rust `&str`.
260    ///
261    /// # Safety
262    ///
263    /// `plugin_name` must point to a valid, null-terminated, UTF-8 C string
264    /// that outlives the returned reference.
265    pub unsafe fn plugin_name_str(&self) -> &str {
266        let cstr = unsafe { std::ffi::CStr::from_ptr(self.plugin_name) };
267        cstr.to_str().expect("plugin_name is not valid UTF-8")
268    }
269
270    /// Returns the `buffer_strategy` field as a `BufferStrategyKind`.
271    ///
272    /// Returns `Err(value)` if the discriminant is unknown. This can happen
273    /// with malformed plugins — callers should reject rather than panic.
274    pub fn buffer_strategy_kind(&self) -> Result<BufferStrategyKind, u8> {
275        match self.buffer_strategy {
276            1 => Ok(BufferStrategyKind::PluginAllocated),
277            2 => Ok(BufferStrategyKind::Arena),
278            v => Err(v),
279        }
280    }
281
282    /// Check if the given optional method capability bit is set.
283    ///
284    /// Returns `false` for bit indices >= 64 rather than panicking.
285    pub fn has_capability(&self, bit: u32) -> bool {
286        if bit >= 64 {
287            return false;
288        }
289        self.capabilities & (1u64 << bit) != 0
290    }
291}