Skip to main content

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}