fidius_host/handle.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//! PluginHandle — type-safe proxy for calling plugin methods via FFI.
16
17use std::ffi::c_void;
18use std::sync::Arc;
19
20use libloading::Library;
21use serde::de::DeserializeOwned;
22use serde::Serialize;
23
24use fidius_core::status::*;
25use fidius_core::wire;
26use fidius_core::PluginError;
27
28use crate::error::CallError;
29use crate::types::PluginInfo;
30
31/// Type alias for the PluginAllocated FFI function pointer signature.
32type FfiFn = unsafe extern "C" fn(*const u8, u32, *mut *mut u8, *mut u32) -> i32;
33
34/// A handle to a loaded plugin, ready for calling methods.
35///
36/// Holds an `Arc<Library>` to keep the dylib loaded as long as any handle exists.
37/// Call methods via `call_method()` which handles serialization, FFI, and cleanup.
38///
39/// `PluginHandle` is `Send + Sync`. Plugin methods take `&self` (enforced by
40/// the macro), so concurrent calls from multiple threads are safe as long as
41/// the plugin implementation is thread-safe internally.
42pub struct PluginHandle {
43 /// Keeps the library alive.
44 _library: Arc<Library>,
45 /// Pointer to the `#[repr(C)]` vtable struct in the loaded library.
46 vtable: *const c_void,
47 /// Free function for plugin-allocated output buffers.
48 free_buffer: Option<unsafe extern "C" fn(*mut u8, usize)>,
49 /// Capability bitfield for optional method support.
50 capabilities: u64,
51 /// Total number of methods in the vtable.
52 method_count: u32,
53 /// Owned plugin metadata.
54 info: PluginInfo,
55}
56
57// SAFETY: PluginHandle is Send + Sync because:
58// - vtable and free_buffer are function pointers to static code in the loaded library
59// - Arc<Library> is Send + Sync and ensures the library stays loaded
60// - All access through call_method is read-only (no mutation of handle state)
61//
62// Plugin implementations must be thread-safe (&self methods, no &mut self)
63// if the PluginHandle is shared across threads. This is enforced at compile
64// time by the #[plugin_interface] macro which rejects &mut self methods.
65unsafe impl Send for PluginHandle {}
66unsafe impl Sync for PluginHandle {}
67
68impl PluginHandle {
69 /// Create a new PluginHandle. Crate-private — use `from_loaded()` instead.
70 #[allow(dead_code)]
71 pub(crate) fn new(
72 library: Arc<Library>,
73 vtable: *const c_void,
74 free_buffer: Option<unsafe extern "C" fn(*mut u8, usize)>,
75 capabilities: u64,
76 method_count: u32,
77 info: PluginInfo,
78 ) -> Self {
79 Self {
80 _library: library,
81 vtable,
82 free_buffer,
83 capabilities,
84 method_count,
85 info,
86 }
87 }
88
89 /// Create a PluginHandle from a LoadedPlugin.
90 pub fn from_loaded(plugin: crate::loader::LoadedPlugin) -> Self {
91 Self {
92 _library: plugin.library,
93 vtable: plugin.vtable,
94 free_buffer: plugin.free_buffer,
95 capabilities: plugin.info.capabilities,
96 method_count: plugin.method_count,
97 info: plugin.info,
98 }
99 }
100
101 /// Call a plugin method by vtable index.
102 ///
103 /// Serializes the input, calls the FFI function pointer at the given index,
104 /// checks the status code, deserializes the output, and frees the plugin-allocated buffer.
105 ///
106 /// # Arguments
107 /// * `index` - The method index in the vtable (0-based, in declaration order)
108 /// * `input` - The input argument to serialize and pass to the plugin
109 pub fn call_method<I: Serialize, O: DeserializeOwned>(
110 &self,
111 index: usize,
112 input: &I,
113 ) -> Result<O, CallError> {
114 // Bounds check: ensure index is within the vtable
115 if index >= self.method_count as usize {
116 return Err(CallError::NotImplemented { bit: index as u32 });
117 }
118
119 // Serialize input
120 let input_bytes =
121 wire::serialize(input).map_err(|e| CallError::Serialization(e.to_string()))?;
122
123 // Get the function pointer from the vtable
124 let fn_ptr = unsafe {
125 let fn_ptrs = self.vtable as *const FfiFn;
126 *fn_ptrs.add(index)
127 };
128
129 // Call the FFI function
130 let mut out_ptr: *mut u8 = std::ptr::null_mut();
131 let mut out_len: u32 = 0;
132
133 let status = unsafe {
134 fn_ptr(
135 input_bytes.as_ptr(),
136 input_bytes.len() as u32,
137 &mut out_ptr,
138 &mut out_len,
139 )
140 };
141
142 // Handle status codes
143 match status {
144 STATUS_OK => {}
145 STATUS_BUFFER_TOO_SMALL => return Err(CallError::BufferTooSmall),
146 STATUS_SERIALIZATION_ERROR => {
147 return Err(CallError::Serialization("FFI serialization failed".into()))
148 }
149 STATUS_PLUGIN_ERROR => {
150 // Output buffer contains a serialized PluginError
151 if !out_ptr.is_null() && out_len > 0 {
152 let output_slice =
153 unsafe { std::slice::from_raw_parts(out_ptr, out_len as usize) };
154 let plugin_err: PluginError = wire::deserialize(output_slice)
155 .map_err(|e| CallError::Deserialization(e.to_string()))?;
156
157 // Free the buffer
158 if let Some(free) = self.free_buffer {
159 unsafe { free(out_ptr, out_len as usize) };
160 }
161
162 return Err(CallError::Plugin(plugin_err));
163 }
164 return Err(CallError::Plugin(PluginError::new(
165 "UNKNOWN",
166 "plugin returned error but no error data",
167 )));
168 }
169 STATUS_PANIC => {
170 // Try to extract panic message from output buffer
171 let msg = if !out_ptr.is_null() && out_len > 0 {
172 let slice = unsafe { std::slice::from_raw_parts(out_ptr, out_len as usize) };
173 let msg = wire::deserialize::<String>(slice)
174 .unwrap_or_else(|_| "unknown panic".into());
175 if let Some(free) = self.free_buffer {
176 unsafe { free(out_ptr, out_len as usize) };
177 }
178 msg
179 } else {
180 "unknown panic".into()
181 };
182 return Err(CallError::Panic(msg));
183 }
184 _ => return Err(CallError::UnknownStatus { code: status }),
185 }
186
187 // Defensive check: ensure plugin set the output pointer
188 if out_ptr.is_null() {
189 return Err(CallError::Serialization(
190 "plugin returned null output buffer".into(),
191 ));
192 }
193
194 // Deserialize output
195 let output_slice = unsafe { std::slice::from_raw_parts(out_ptr, out_len as usize) };
196 let result: Result<O, CallError> =
197 wire::deserialize(output_slice).map_err(|e| CallError::Deserialization(e.to_string()));
198
199 // Free the plugin-allocated buffer
200 if let Some(free) = self.free_buffer {
201 unsafe { free(out_ptr, out_len as usize) };
202 }
203
204 result
205 }
206
207 /// Check if an optional method is supported (capability bit is set).
208 ///
209 /// Returns `false` for bit indices >= 64 rather than panicking.
210 pub fn has_capability(&self, bit: u32) -> bool {
211 if bit >= 64 {
212 return false;
213 }
214 self.capabilities & (1u64 << bit) != 0
215 }
216
217 /// Access the plugin's owned metadata.
218 pub fn info(&self) -> &PluginInfo {
219 &self.info
220 }
221}