use std::ffi::OsStr;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use libloading::{Library, Symbol};
use thiserror::Error;
use rpi_plugin_sdk::{PluginApiVt, RpiPluginRegister, RPI_PLUGIN_ABI_VERSION};
use crate::registry::{ExtensionRegistry, RegistrySnapshot};
use crate::{
clear_current_api, set_current_api, ActionBridge, HostApi, NullDiagnostics, PluginDiagnostics,
};
#[derive(Debug, Error)]
pub enum PluginLoadError {
#[error("could not open library {path}: {source}")]
Open {
path: PathBuf,
#[source]
source: libloading::Error,
},
#[error("symbol `rpi_plugin_register` not found in {path}: {source}")]
Symbol {
path: PathBuf,
#[source]
source: libloading::Error,
},
#[error("register returned nonzero code {code} for {path}")]
RegisterReturned { path: PathBuf, code: i32 },
#[error(
"ABI version mismatch in {path}: plugin built for {plugin_version}, host is {host_version}"
)]
AbiVersionMismatch {
path: PathBuf,
plugin_version: u32,
host_version: u32,
},
}
pub struct LoadedPlugin {
pub library: Library,
pub path: PathBuf,
pub registry: ExtensionRegistry,
}
pub fn load_one(
path: impl AsRef<Path>,
diagnostics: Arc<dyn PluginDiagnostics>,
action_bridge: Option<Arc<ActionBridge>>,
) -> Result<LoadedPlugin, PluginLoadError> {
let path = path.as_ref().to_path_buf();
let library = unsafe { Library::new(&path) }.map_err(|e| PluginLoadError::Open {
path: path.clone(),
source: e,
})?;
let register: Symbol<RpiPluginRegister> = unsafe {
library.get(rpi_plugin_sdk::REGISTER_SYMBOL)
}
.map_err(|e| PluginLoadError::Symbol {
path: path.clone(),
source: e,
})?;
let registry = ExtensionRegistry::new();
let host_api = match action_bridge {
Some(bridge) => HostApi::with_action_bridge(registry, Arc::clone(&diagnostics), bridge),
None => HostApi::new(registry, Arc::clone(&diagnostics)),
};
let vtable = host_api.build_vtable();
unsafe { set_current_api(&host_api) };
let vt_ref: &PluginApiVt = &vtable;
let register_outcome = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
register(vt_ref as *const PluginApiVt, RPI_PLUGIN_ABI_VERSION)
}));
clear_current_api();
let rc = match register_outcome {
Ok(rc) => rc,
Err(_) => {
diagnostics.warn(&format!(
"plugin {} register panicked — skipped (unwind contained)",
path.display()
));
return Err(PluginLoadError::RegisterReturned { path, code: -1 });
}
};
if rc != 0 {
diagnostics.warn(&format!(
"plugin {} register returned code {} — skipped",
path.display(),
rc
));
return Err(PluginLoadError::RegisterReturned { path, code: rc });
}
let registry = host_api
.take_registry()
.ok_or_else(|| PluginLoadError::RegisterReturned {
path: path.clone(),
code: -2,
})?;
Ok(LoadedPlugin {
library,
path,
registry,
})
}
const CDYLIB_EXTS: &[&str] = &["dll", "so", "dylib", "pyd"];
pub fn load_dir(
dir: impl AsRef<Path>,
diagnostics: Arc<dyn PluginDiagnostics>,
action_bridge: Option<Arc<ActionBridge>>,
) -> Vec<LoadedPlugin> {
let dir = dir.as_ref();
let mut out = Vec::new();
let read = match std::fs::read_dir(dir) {
Ok(r) => r,
Err(e) => {
diagnostics.warn(&format!(
"extensions dir {} unreadable: {}",
dir.display(),
e
));
return out;
}
};
for entry in read.flatten() {
let path = entry.path();
if !is_cdylib(&path) {
continue;
}
match load_one(&path, Arc::clone(&diagnostics), action_bridge.clone()) {
Ok(p) => out.push(p),
Err(e) => diagnostics.warn(&format!("skipped plugin {}: {e}", path.display())),
}
}
out
}
fn is_cdylib(path: &Path) -> bool {
path.extension()
.and_then(OsStr::to_str)
.map(|ext| CDYLIB_EXTS.iter().any(|e| e.eq_ignore_ascii_case(ext)))
.unwrap_or(false)
}
pub fn merge_registries(plugins: &mut [LoadedPlugin]) -> ExtensionRegistry {
let mut session = ExtensionRegistry::new();
for p in plugins.iter_mut() {
let taken = std::mem::take(&mut p.registry);
session.absorb(taken);
}
session
}
pub struct PluginKeepalive {
#[allow(dead_code)]
libraries: Vec<Library>,
#[allow(dead_code)]
action_bridge: Option<Arc<ActionBridge>>,
}
impl PluginKeepalive {
pub fn new(libraries: Vec<Library>, action_bridge: Option<Arc<ActionBridge>>) -> Self {
Self {
libraries,
action_bridge,
}
}
pub fn empty() -> Arc<Self> {
Arc::new(Self::new(Vec::new(), None))
}
}
#[derive(Clone)]
pub struct ExtensionSession {
keepalive: Arc<PluginKeepalive>,
snapshot: Option<Arc<RegistrySnapshot>>,
loaded_paths: Vec<PathBuf>,
action_bridge: Option<Arc<ActionBridge>>,
}
impl ExtensionSession {
pub fn from_parts(
snapshot: Arc<RegistrySnapshot>,
keepalive: Arc<PluginKeepalive>,
loaded_paths: Vec<PathBuf>,
action_bridge: Option<Arc<ActionBridge>>,
) -> Self {
Self {
keepalive,
snapshot: Some(snapshot),
loaded_paths,
action_bridge,
}
}
pub fn none() -> Self {
Self {
keepalive: Arc::new(PluginKeepalive::new(Vec::new(), None)),
snapshot: None,
loaded_paths: Vec::new(),
action_bridge: None,
}
}
pub fn keepalive(&self) -> Arc<PluginKeepalive> {
Arc::clone(&self.keepalive)
}
pub fn snapshot(&self) -> Option<&RegistrySnapshot> {
self.snapshot.as_deref()
}
pub fn snapshot_arc(&self) -> Option<Arc<RegistrySnapshot>> {
self.snapshot.clone()
}
pub fn loaded_paths(&self) -> &[PathBuf] {
&self.loaded_paths
}
pub fn is_empty(&self) -> bool {
self.loaded_paths.is_empty()
}
pub fn summary(&self) -> Option<String> {
if self.is_empty() {
return None;
}
let tools = self.snapshot.as_ref().map(|s| s.tools().len()).unwrap_or(0);
Some(format!(
"loaded {} plugin(s) ({} tool(s))",
self.loaded_paths.len(),
tools
))
}
pub fn action_bridge(&self) -> Option<Arc<ActionBridge>> {
self.action_bridge.clone()
}
}
pub fn load_session(
dirs: &[PathBuf],
diagnostics: Arc<dyn PluginDiagnostics>,
action_bridge: Option<Arc<ActionBridge>>,
) -> ExtensionSession {
load_session_mixed(dirs, &[], diagnostics, action_bridge)
}
pub fn load_session_mixed(
dirs: &[PathBuf],
files: &[PathBuf],
diagnostics: Arc<dyn PluginDiagnostics>,
action_bridge: Option<Arc<ActionBridge>>,
) -> ExtensionSession {
let mut loaded: Vec<LoadedPlugin> = Vec::new();
for dir in dirs {
loaded.extend(load_dir(
dir,
Arc::clone(&diagnostics),
action_bridge.clone(),
));
}
for f in files {
if let Ok(plugin) = load_one(f, Arc::clone(&diagnostics), action_bridge.clone()) {
loaded.push(plugin);
}
}
if loaded.is_empty() {
return ExtensionSession::none();
}
let loaded_paths: Vec<PathBuf> = loaded.iter().map(|p| p.path.clone()).collect();
let session_registry = merge_registries(&mut loaded);
let mut libs: Vec<Library> = Vec::with_capacity(loaded.len());
for p in loaded {
let LoadedPlugin {
library,
registry: _,
path: _,
} = p;
libs.push(library);
}
let snapshot = Arc::new(session_registry.snapshot());
ExtensionSession {
keepalive: Arc::new(PluginKeepalive::new(libs, action_bridge.clone())),
snapshot: Some(snapshot),
loaded_paths,
action_bridge,
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::Mutex;
#[derive(Default)]
struct CapturingDiag {
warns: Mutex<Vec<String>>,
}
impl PluginDiagnostics for CapturingDiag {
fn warn(&self, msg: &str) {
self.warns.lock().unwrap().push(msg.to_string());
}
fn unsupported(&self, msg: &str) {
self.warn(msg);
}
}
#[test]
fn load_one_missing_file_reports_open_error() {
let diag: Arc<dyn PluginDiagnostics> = Arc::new(CapturingDiag::default());
let res = load_one("definitely_not_a_plugin.dll", diag, None);
assert!(matches!(res, Err(PluginLoadError::Open { .. })));
}
#[test]
fn load_dir_missing_dir_returns_empty_and_warns() {
let empty = load_dir(
"no_such_dir_xyz",
Arc::new(CapturingDiag::default()) as Arc<dyn PluginDiagnostics>,
None,
);
assert!(empty.is_empty());
}
#[test]
fn is_cdylib_recognizes_extensions() {
assert!(is_cdylib(Path::new("foo.dll")));
assert!(is_cdylib(Path::new("foo.so")));
assert!(is_cdylib(Path::new("foo.dylib")));
assert!(is_cdylib(Path::new("FOO.DLL")));
assert!(!is_cdylib(Path::new("foo.md")));
assert!(!is_cdylib(Path::new("foo")));
}
}
#[allow(dead_code)]
fn _ensure_nulldiagnostics_referenced() -> Arc<dyn PluginDiagnostics> {
Arc::new(NullDiagnostics)
}