Skip to main content

fidius_host/
loader.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//! Core plugin loading and descriptor validation.
16
17use std::ffi::c_void;
18use std::path::Path;
19use std::sync::Arc;
20
21use fidius_core::descriptor::*;
22use libloading::Library;
23
24use crate::error::LoadError;
25use crate::types::PluginInfo;
26
27/// A loaded plugin library with validated descriptors.
28pub struct LoadedLibrary {
29    /// The dynamically loaded library. Must stay alive while any PluginHandle exists.
30    pub library: Arc<Library>,
31    /// Validated plugin descriptors with owned metadata.
32    pub plugins: Vec<LoadedPlugin>,
33}
34
35/// A single validated plugin from a loaded library.
36pub struct LoadedPlugin {
37    /// Owned metadata copied from the FFI descriptor.
38    pub info: PluginInfo,
39    /// Raw vtable pointer (points into the loaded library's memory).
40    pub vtable: *const c_void,
41    /// Free function for plugin-allocated buffers.
42    pub free_buffer: Option<unsafe extern "C" fn(*mut u8, usize)>,
43    /// Total number of methods in the vtable.
44    pub method_count: u32,
45    /// Raw pointer to the plugin's descriptor in library memory. Kept so the
46    /// host can read metadata fields (`method_metadata`, `trait_metadata`)
47    /// without re-walking the registry. Valid for the lifetime of `library`.
48    pub descriptor: *const PluginDescriptor,
49    /// Reference to the library to keep it alive.
50    pub library: Arc<Library>,
51}
52
53// SAFETY: vtable and free_buffer point to static data in the loaded library.
54// The Arc<Library> ensures the library stays loaded.
55unsafe impl Send for LoadedPlugin {}
56unsafe impl Sync for LoadedPlugin {}
57
58impl std::fmt::Debug for LoadedPlugin {
59    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
60        f.debug_struct("LoadedPlugin")
61            .field("info", &self.info)
62            .field("vtable", &self.vtable)
63            .finish()
64    }
65}
66
67/// Load a plugin library from a path.
68///
69/// Opens the dylib, calls `fidius_get_registry()`, validates the registry
70/// and all descriptors, copies FFI data to owned types.
71pub fn load_library(path: &Path) -> Result<LoadedLibrary, LoadError> {
72    let path_str = path.display().to_string();
73
74    #[cfg(feature = "tracing")]
75    tracing::debug!(path = %path_str, "loading library");
76
77    // Check architecture before dlopen
78    crate::arch::check_architecture(path)?;
79
80    // dlopen
81    let library = unsafe { Library::new(path) }.map_err(|e| {
82        if e.to_string().contains("No such file") || e.to_string().contains("not found") {
83            LoadError::LibraryNotFound {
84                path: path_str.clone(),
85            }
86        } else {
87            LoadError::LibLoading(e)
88        }
89    })?;
90
91    // dlsym("fidius_get_registry")
92    let get_registry: libloading::Symbol<unsafe extern "C" fn() -> *const PluginRegistry> =
93        unsafe { library.get(b"fidius_get_registry") }.map_err(|_| LoadError::SymbolNotFound {
94            path: path_str.clone(),
95        })?;
96
97    // Call to get the registry pointer
98    let registry = unsafe { &*get_registry() };
99
100    // Validate magic
101    if registry.magic != FIDIUS_MAGIC {
102        return Err(LoadError::InvalidMagic);
103    }
104
105    // Validate registry version
106    if registry.registry_version != REGISTRY_VERSION {
107        return Err(LoadError::IncompatibleRegistryVersion {
108            got: registry.registry_version,
109            expected: REGISTRY_VERSION,
110        });
111    }
112
113    let library = Arc::new(library);
114
115    // Iterate descriptors and validate each
116    let mut plugins = Vec::with_capacity(registry.plugin_count as usize);
117    for i in 0..registry.plugin_count {
118        let desc = unsafe { &**registry.descriptors.add(i as usize) };
119        let plugin = validate_descriptor(desc, &library)?;
120        plugins.push(plugin);
121    }
122
123    Ok(LoadedLibrary { library, plugins })
124}
125
126/// Validate a single descriptor and copy to owned types.
127fn validate_descriptor(
128    desc: &PluginDescriptor,
129    library: &Arc<Library>,
130) -> Result<LoadedPlugin, LoadError> {
131    // Check ABI version
132    if desc.abi_version != ABI_VERSION {
133        return Err(LoadError::IncompatibleAbiVersion {
134            got: desc.abi_version,
135            expected: ABI_VERSION,
136        });
137    }
138
139    // Copy FFI strings to owned
140    let interface_name = unsafe { desc.interface_name_str() }.to_string();
141    let plugin_name = unsafe { desc.plugin_name_str() }.to_string();
142
143    let info = PluginInfo {
144        name: plugin_name,
145        interface_name,
146        interface_hash: desc.interface_hash,
147        interface_version: desc.interface_version,
148        capabilities: desc.capabilities,
149        buffer_strategy: desc
150            .buffer_strategy_kind()
151            .map_err(|v| LoadError::UnknownBufferStrategy { value: v })?,
152        runtime: crate::types::PluginRuntimeKind::Cdylib,
153    };
154
155    Ok(LoadedPlugin {
156        info,
157        vtable: desc.vtable,
158        free_buffer: desc.free_buffer,
159        method_count: desc.method_count,
160        descriptor: desc as *const PluginDescriptor,
161        library: Arc::clone(library),
162    })
163}
164
165/// Validate a loaded plugin against expected interface parameters.
166pub fn validate_against_interface(
167    plugin: &LoadedPlugin,
168    expected_hash: Option<u64>,
169    expected_strategy: Option<BufferStrategyKind>,
170) -> Result<(), LoadError> {
171    if let Some(hash) = expected_hash {
172        if plugin.info.interface_hash != hash {
173            return Err(LoadError::InterfaceHashMismatch {
174                got: plugin.info.interface_hash,
175                expected: hash,
176            });
177        }
178    }
179
180    if let Some(strategy) = expected_strategy {
181        if plugin.info.buffer_strategy != strategy {
182            return Err(LoadError::BufferStrategyMismatch {
183                got: plugin.info.buffer_strategy,
184                expected: strategy,
185            });
186        }
187    }
188
189    Ok(())
190}