Skip to main content

linkrs_query/executor/expression/functions/udf/
loader.rs

1//! Dynamic library loading for UDF plugins.
2
3use super::dynamic::DynamicPlugin;
4use super::error::UdfError;
5use super::plugin::{
6    SharedPlugin, UdfAbiVersionFn, UdfCreateFn, UdfDestroyFn, UdfPlugin, UDF_ABI_VERSION,
7    UDF_ABI_VERSION_SYMBOL, UDF_CREATE_SYMBOL, UDF_DESTROY_SYMBOL,
8};
9use libloading::{Library, Symbol};
10use std::path::{Path, PathBuf};
11use std::sync::Arc;
12use std::time::SystemTime;
13
14/// A successfully loaded plugin together with its origin metadata.
15pub struct LoadedPlugin {
16    /// Shared handle registered in the function registry.
17    pub plugin: SharedPlugin,
18    /// Canonical library path.
19    pub path: PathBuf,
20    /// File modification time observed at load, used by reload decisions.
21    pub loaded_mtime: Option<SystemTime>,
22}
23
24impl std::fmt::Debug for LoadedPlugin {
25    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
26        f.debug_struct("LoadedPlugin")
27            .field("name", &self.plugin.name())
28            .field("path", &self.path)
29            .finish()
30    }
31}
32
33/// Loader for UDF dynamic libraries.
34pub struct UdfLoader;
35
36impl UdfLoader {
37    /// Install a plugin from an `INSTALL EXTENSION ... FROM` source.
38    ///
39    /// Reservation point for a future repository module (download +
40    /// checksum + signature verification). Local file paths load directly;
41    /// `http(s)://` sources return `RepoDownloadUnsupported`.
42    pub fn install_from_source(source: &str) -> Result<LoadedPlugin, UdfError> {
43        let lower = source.to_ascii_lowercase();
44        if lower.starts_with("http://") || lower.starts_with("https://") {
45            return Err(UdfError::RepoDownloadUnsupported(source.to_string()));
46        }
47        Self::load(Path::new(source))
48    }
49
50    /// Load the plugin exported by the library at `path`.
51    pub fn load(path: &Path) -> Result<LoadedPlugin, UdfError> {
52        let canonical = Self::validate_path(path)?;
53        let path_display = canonical.to_string_lossy().to_string();
54        let loaded_mtime = std::fs::metadata(&canonical)
55            .ok()
56            .and_then(|m| m.modified().ok());
57
58        let lib = unsafe { Library::new(&canonical) }
59            .map_err(|e| UdfError::LibraryLoad(path_display.clone(), e.to_string()))?;
60        let lib = Arc::new(lib);
61
62        // Copy the raw function pointers out of the symbols so no
63        // lifetime-bound `Symbol` escapes this function.
64        let create: UdfCreateFn = unsafe {
65            let symbol: Symbol<UdfCreateFn> =
66                lib.get(UDF_CREATE_SYMBOL.as_bytes()).map_err(|_| {
67                    UdfError::SymbolNotFound(path_display.clone(), UDF_CREATE_SYMBOL.into())
68                })?;
69            *symbol
70        };
71        let destroy: UdfDestroyFn = unsafe {
72            let symbol: Symbol<UdfDestroyFn> =
73                lib.get(UDF_DESTROY_SYMBOL.as_bytes()).map_err(|_| {
74                    UdfError::SymbolNotFound(path_display.clone(), UDF_DESTROY_SYMBOL.into())
75                })?;
76            *symbol
77        };
78
79        // Optional ABI compatibility probe.
80        if let Ok(symbol) = unsafe { lib.get::<UdfAbiVersionFn>(UDF_ABI_VERSION_SYMBOL.as_bytes()) }
81        {
82            let version_fn: UdfAbiVersionFn = *symbol;
83            let reported = unsafe { version_fn() };
84            if reported != UDF_ABI_VERSION {
85                return Err(UdfError::VersionMismatch(
86                    path_display,
87                    UDF_ABI_VERSION,
88                    reported,
89                ));
90            }
91        }
92
93        let raw = unsafe { create() };
94        let owned = unsafe {
95            DynamicPlugin::from_raw(raw, destroy, Arc::clone(&lib), path_display.clone())
96        }?;
97        if owned.name().trim().is_empty() {
98            return Err(UdfError::InvalidName(path_display));
99        }
100        if owned.min_arity() > owned.max_arity() {
101            return Err(UdfError::InvalidName(format!(
102                "{path_display}: min_arity exceeds max_arity"
103            )));
104        }
105
106        Ok(LoadedPlugin {
107            plugin: Arc::new(owned),
108            path: canonical,
109            loaded_mtime,
110        })
111    }
112
113    /// Reject missing files and platform-mismatched extensions early.
114    fn validate_path(path: &Path) -> Result<PathBuf, UdfError> {
115        let display = path.to_string_lossy().to_string();
116        if !path.exists() {
117            return Err(UdfError::InvalidPath(
118                display,
119                "file does not exist".to_string(),
120            ));
121        }
122        let expected = Self::expected_extension();
123        let actual = path
124            .extension()
125            .map(|e| e.to_string_lossy().to_ascii_lowercase())
126            .unwrap_or_default();
127        if actual != expected {
128            return Err(UdfError::InvalidPath(
129                display,
130                format!("expected a '*.{expected}' dynamic library"),
131            ));
132        }
133        path.canonicalize()
134            .map_err(|e| UdfError::Io(display, e.to_string()))
135    }
136
137    fn expected_extension() -> &'static str {
138        if cfg!(target_os = "windows") {
139            "dll"
140        } else if cfg!(target_os = "macos") {
141            "dylib"
142        } else {
143            "so"
144        }
145    }
146}