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}