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}