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/// Current version of the `PluginDescriptor` struct layout.
30/// Bumped to 2 to add `method_count` field.
31pub const ABI_VERSION: u32 = 2;
32
33/// Buffer management strategy for an interface.
34///
35/// Selected per-trait via `#[plugin_interface(buffer = ...)]`.
36/// Determines the FFI function pointer signatures in the vtable.
37#[repr(u8)]
38#[derive(Debug, Clone, Copy, PartialEq, Eq)]
39pub enum BufferStrategyKind {
40    /// Host allocates output buffer; plugin writes into it.
41    /// Returns `-1` with needed size if buffer is too small.
42    CallerAllocated = 0,
43    /// Plugin allocates output; host frees via `PluginDescriptor::free_buffer`.
44    PluginAllocated = 1,
45    /// Host provides a pre-allocated arena; plugin writes into it.
46    /// Data is valid only until the next call.
47    Arena = 2,
48}
49
50/// Wire serialization format.
51///
52/// Determined at compile time via `cfg(debug_assertions)`.
53/// Host rejects plugins compiled with a mismatched format.
54#[repr(u8)]
55#[derive(Debug, Clone, Copy, PartialEq, Eq)]
56pub enum WireFormat {
57    /// JSON via `serde_json` — human-readable, used in debug builds.
58    Json = 0,
59    /// bincode — compact and fast, used in release builds.
60    Bincode = 1,
61}
62
63impl std::fmt::Display for BufferStrategyKind {
64    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
65        match self {
66            BufferStrategyKind::CallerAllocated => write!(f, "CallerAllocated"),
67            BufferStrategyKind::PluginAllocated => write!(f, "PluginAllocated"),
68            BufferStrategyKind::Arena => write!(f, "Arena"),
69        }
70    }
71}
72
73impl std::fmt::Display for WireFormat {
74    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
75        match self {
76            WireFormat::Json => write!(f, "Json (debug build)"),
77            WireFormat::Bincode => write!(f, "Bincode (release build)"),
78        }
79    }
80}
81
82/// Top-level registry exported by every Fidius plugin dylib.
83///
84/// Each dylib exports exactly one `FIDIUS_PLUGIN_REGISTRY` static symbol
85/// pointing to this struct. The registry contains pointers to one or more
86/// `PluginDescriptor`s (supporting multiple plugins per dylib).
87///
88/// # Safety
89///
90/// - `descriptors` must point to a valid array of `plugin_count` pointers.
91/// - Each pointer in the array must point to a valid `PluginDescriptor`.
92/// - All pointed-to data must have `'static` lifetime (typically link-time constants).
93#[repr(C)]
94pub struct PluginRegistry {
95    /// Magic bytes — must equal `FIDIUS_MAGIC` (`b"FIDIUS\0\0"`).
96    pub magic: [u8; 8],
97    /// Layout version of this struct. Must equal `REGISTRY_VERSION`.
98    pub registry_version: u32,
99    /// Number of plugin descriptors in this registry.
100    pub plugin_count: u32,
101    /// Pointer to an array of `plugin_count` descriptor pointers.
102    pub descriptors: *const *const PluginDescriptor,
103}
104
105// SAFETY: PluginRegistry contains only primitive fields and a pointer to
106// static data. The pointed-to descriptors are immutable after construction
107// and have 'static lifetime.
108unsafe impl Send for PluginRegistry {}
109unsafe impl Sync for PluginRegistry {}
110
111/// Metadata descriptor for a single plugin within a dylib.
112///
113/// Contains all information the host needs to validate and call the plugin
114/// without executing any plugin code. All string fields are pointers to
115/// static, null-terminated C strings embedded in the dylib.
116///
117/// # Safety
118///
119/// - `interface_name` and `plugin_name` must point to valid, null-terminated,
120///   UTF-8 C strings with `'static` lifetime.
121/// - `vtable` must point to a valid `#[repr(C)]` vtable struct matching the
122///   interface identified by `interface_name` and `interface_hash`.
123/// - When `buffer_strategy == PluginAllocated`, `free_buffer` must be `Some`.
124/// - All pointed-to data must outlive any `PluginHandle` derived from this descriptor.
125#[repr(C)]
126pub struct PluginDescriptor {
127    /// Descriptor struct layout version. Must equal `ABI_VERSION`.
128    pub abi_version: u32,
129    /// Null-terminated name of the trait this plugin implements (e.g., `"ImageFilter"`).
130    pub interface_name: *const c_char,
131    /// FNV-1a hash of the required method signatures. Detects ABI drift.
132    pub interface_hash: u64,
133    /// User-specified interface version from `#[plugin_interface(version = N)]`.
134    pub interface_version: u32,
135    /// Bitfield where bit N indicates optional method N is implemented.
136    /// Supports up to 64 optional methods per interface.
137    pub capabilities: u64,
138    /// Wire serialization format this plugin was compiled with.
139    pub wire_format: u8,
140    /// Buffer management strategy this plugin's vtable expects.
141    pub buffer_strategy: u8,
142    /// Null-terminated human-readable name for this plugin implementation.
143    pub plugin_name: *const c_char,
144    /// Opaque pointer to the interface-specific `#[repr(C)]` vtable struct.
145    pub vtable: *const c_void,
146    /// Deallocation function for plugin-allocated buffers.
147    /// Must be `Some` when `buffer_strategy == PluginAllocated`.
148    /// The host calls this after reading output data to free the plugin's allocation.
149    pub free_buffer: Option<unsafe extern "C" fn(*mut u8, usize)>,
150    /// Total number of methods in the vtable (required + optional).
151    /// Used for bounds checking in `call_method`.
152    pub method_count: u32,
153}
154
155// SAFETY: PluginDescriptor fields are either primitives, pointers to static
156// data, or function pointers. All are immutable after construction and the
157// pointed-to data has 'static lifetime.
158unsafe impl Send for PluginDescriptor {}
159unsafe impl Sync for PluginDescriptor {}
160
161/// A `Sync` wrapper for a raw pointer to a `PluginDescriptor`.
162///
163/// Used in static contexts where a `*const PluginDescriptor` needs to live
164/// in a `static` variable (which requires `Sync`). The pointed-to descriptor
165/// must have `'static` lifetime.
166#[repr(transparent)]
167pub struct DescriptorPtr(pub *const PluginDescriptor);
168
169// SAFETY: The pointer targets static data that is immutable after construction.
170unsafe impl Send for DescriptorPtr {}
171unsafe impl Sync for DescriptorPtr {}
172
173impl PluginDescriptor {
174    /// Read the `interface_name` field as a Rust `&str`.
175    ///
176    /// # Safety
177    ///
178    /// `interface_name` must point to a valid, null-terminated, UTF-8 C string
179    /// that outlives the returned reference.
180    pub unsafe fn interface_name_str(&self) -> &str {
181        let cstr = unsafe { std::ffi::CStr::from_ptr(self.interface_name) };
182        cstr.to_str().expect("interface_name is not valid UTF-8")
183    }
184
185    /// Read the `plugin_name` field as a Rust `&str`.
186    ///
187    /// # Safety
188    ///
189    /// `plugin_name` must point to a valid, null-terminated, UTF-8 C string
190    /// that outlives the returned reference.
191    pub unsafe fn plugin_name_str(&self) -> &str {
192        let cstr = unsafe { std::ffi::CStr::from_ptr(self.plugin_name) };
193        cstr.to_str().expect("plugin_name is not valid UTF-8")
194    }
195
196    /// Returns the `buffer_strategy` field as a `BufferStrategyKind`.
197    ///
198    /// Returns `Err(value)` if the discriminant is unknown. This can happen
199    /// with malformed plugins — callers should reject rather than panic.
200    pub fn buffer_strategy_kind(&self) -> Result<BufferStrategyKind, u8> {
201        match self.buffer_strategy {
202            0 => Ok(BufferStrategyKind::CallerAllocated),
203            1 => Ok(BufferStrategyKind::PluginAllocated),
204            2 => Ok(BufferStrategyKind::Arena),
205            v => Err(v),
206        }
207    }
208
209    /// Returns the `wire_format` field as a `WireFormat`.
210    ///
211    /// Returns `Err(value)` if the discriminant is unknown.
212    pub fn wire_format_kind(&self) -> Result<WireFormat, u8> {
213        match self.wire_format {
214            0 => Ok(WireFormat::Json),
215            1 => Ok(WireFormat::Bincode),
216            v => Err(v),
217        }
218    }
219
220    /// Check if the given optional method capability bit is set.
221    ///
222    /// Returns `false` for bit indices >= 64 rather than panicking.
223    pub fn has_capability(&self, bit: u32) -> bool {
224        if bit >= 64 {
225            return false;
226        }
227        self.capabilities & (1u64 << bit) != 0
228    }
229}