115 lines
3.9 KiB
Rust
115 lines
3.9 KiB
Rust
use anyhow::Context;
|
|
use libloading::{Library, Symbol};
|
|
use once_cell::sync::OnceCell;
|
|
use owly_news_module_api::{take_cstring, SYMBOL_INVOKE, SYMBOL_NAME};
|
|
use std::collections::HashMap;
|
|
use std::ffi::CString;
|
|
use std::os::raw::c_char;
|
|
use std::path::{Path, PathBuf};
|
|
use std::sync::Arc;
|
|
use tokio::sync::Mutex;
|
|
use tracing::info;
|
|
|
|
type ModuleNameFn = unsafe extern "C" fn() -> *const c_char;
|
|
type ModuleInvokeFn = unsafe extern "C" fn(*const c_char, *const c_char) -> *mut c_char;
|
|
|
|
pub struct ModuleHandle {
|
|
_lib: Arc<Library>,
|
|
invoke: ModuleInvokeFn,
|
|
}
|
|
|
|
impl ModuleHandle {
|
|
pub fn invoke_json(&self, op: &str, payload: serde_json::Value) -> anyhow::Result<serde_json::Value> {
|
|
let op_c = CString::new(op)?;
|
|
let payload_c = CString::new(serde_json::to_string(&payload)?)?;
|
|
|
|
let out_ptr = unsafe { (self.invoke)(op_c.as_ptr(), payload_c.as_ptr()) };
|
|
let out = unsafe { take_cstring(out_ptr) }?;
|
|
let val = serde_json::from_str(&out).context("module returned invalid JSON")?;
|
|
Ok(val)
|
|
}
|
|
}
|
|
|
|
pub struct ModuleHost {
|
|
// Lazy cache of loaded modules by logical name
|
|
loaded: Mutex<HashMap<String, Arc<ModuleHandle>>>,
|
|
modules_dir: PathBuf,
|
|
}
|
|
|
|
static DEFAULT_HOST: OnceCell<Arc<ModuleHost>> = OnceCell::new();
|
|
|
|
impl ModuleHost {
|
|
pub fn default() -> Arc<Self> {
|
|
DEFAULT_HOST
|
|
.get_or_init(|| {
|
|
Arc::new(Self::new(
|
|
std::env::var_os("OWLY_MODULES_DIR")
|
|
.map(PathBuf::from)
|
|
.unwrap_or_else(|| PathBuf::from("target/modules")), // default location
|
|
))
|
|
})
|
|
.clone()
|
|
}
|
|
|
|
pub fn new(modules_dir: PathBuf) -> Self {
|
|
Self {
|
|
loaded: Mutex::new(HashMap::new()),
|
|
modules_dir,
|
|
}
|
|
}
|
|
|
|
pub async fn get(&self, name: &str) -> anyhow::Result<Arc<ModuleHandle>> {
|
|
if let Some(h) = self.loaded.lock().await.get(name).cloned() {
|
|
return Ok(h);
|
|
}
|
|
let handle = Arc::new(self.load_module(name)?);
|
|
self.loaded.lock().await.insert(name.to_string(), handle.clone());
|
|
Ok(handle)
|
|
}
|
|
|
|
fn load_module(&self, name: &str) -> anyhow::Result<ModuleHandle> {
|
|
let lib_path = resolve_module_path(&self.modules_dir, name)?;
|
|
info!(module = name, path = %lib_path.display(), "loading module");
|
|
|
|
// SAFETY: we keep Library alive in ModuleHandle to ensure symbols remain valid
|
|
let lib = unsafe { Library::new(lib_path) }.with_context(|| "failed to load module library")?;
|
|
|
|
// Validate and bind symbols
|
|
let name_fn: Symbol<ModuleNameFn> = unsafe { lib.get(SYMBOL_NAME.as_bytes()) }
|
|
.with_context(|| "missing symbol `module_name`")?;
|
|
let invoke_fn: Symbol<ModuleInvokeFn> = unsafe { lib.get(SYMBOL_INVOKE.as_bytes()) }
|
|
.with_context(|| "missing symbol `module_invoke`")?;
|
|
|
|
// Optional: verify reported name matches requested
|
|
let c_name_ptr = unsafe { name_fn() };
|
|
let c_name = unsafe { std::ffi::CStr::from_ptr(c_name_ptr) }.to_string_lossy().into_owned();
|
|
if c_name != name {
|
|
anyhow::bail!("module reported name `{c_name}`, expected `{name}`");
|
|
}
|
|
|
|
// Copy the function pointer before moving the library
|
|
let invoke_fn_copy = *invoke_fn;
|
|
|
|
Ok(ModuleHandle {
|
|
_lib: Arc::new(lib),
|
|
invoke: invoke_fn_copy,
|
|
})
|
|
}
|
|
}
|
|
|
|
fn resolve_module_path(dir: &Path, name: &str) -> anyhow::Result<PathBuf> {
|
|
#[cfg(target_os = "windows")]
|
|
const EXT: &str = "dll";
|
|
#[cfg(target_os = "macos")]
|
|
const EXT: &str = "dylib";
|
|
#[cfg(all(unix, not(target_os = "macos")))]
|
|
const EXT: &str = "so";
|
|
|
|
let fname = format!("lib{name}.{EXT}");
|
|
let path = dir.join(fname);
|
|
if !path.exists() {
|
|
anyhow::bail!("module `{name}` not found at {}", path.display());
|
|
}
|
|
Ok(path)
|
|
}
|