use std::path::PathBuf;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use std::time::SystemTime;
use libloading::{Library, Symbol};
use crate::canary::{AbiCanary, verify_probe};
use crate::traits::PluginLogic;
pub struct NativeLoader {
dylib_path: PathBuf,
library: Option<Library>,
plugin: Option<Box<dyn PluginLogic>>,
last_modified: SystemTime,
last_hash: u32,
reload_pending: Arc<AtomicBool>,
leaked_handles: Vec<Library>,
load_counter: u64,
}
unsafe impl Send for NativeLoader {}
impl NativeLoader {
pub fn new(dylib_path: PathBuf) -> Self {
let reload_pending = Arc::new(AtomicBool::new(false));
let flag = reload_pending.clone();
let path = dylib_path.clone();
std::thread::Builder::new()
.name("truce-hot-watcher".into())
.spawn(move || watch_loop(&path, &flag))
.ok();
let mut loader = Self {
dylib_path,
library: None,
plugin: None,
last_modified: SystemTime::UNIX_EPOCH,
last_hash: 0,
reload_pending,
leaked_handles: Vec::new(),
load_counter: 0,
};
loader.load();
loader
}
fn load(&mut self) -> bool {
let new_hash = crc32_file(&self.dylib_path);
if new_hash == self.last_hash && self.library.is_some() {
log::debug!("dylib unchanged (CRC32 match), skipping reload");
return true;
}
let temp = match self.copy_versioned() {
Ok(p) => p,
Err(e) => {
log::warn!("failed to copy dylib: {e}");
return false;
}
};
#[cfg(target_os = "macos")]
{
let _ = std::process::Command::new("codesign")
.args(["--sign", "-", "--force", temp.to_str().unwrap_or("")])
.output();
}
let lib = match unsafe { Library::new(&temp) } {
Ok(l) => l,
Err(e) => {
log::warn!("dlopen failed: {e}");
let _ = std::fs::remove_file(&temp);
return false;
}
};
let canary_fn: Symbol<fn() -> AbiCanary> = match unsafe { lib.get(b"truce_abi_canary") } {
Ok(f) => f,
Err(e) => {
log::warn!("missing truce_abi_canary export: {e}");
return false;
}
};
let dylib_canary = canary_fn();
let shell_canary = AbiCanary::current();
if !shell_canary.matches(&dylib_canary) {
log::error!("ABI mismatch — rebuild both shell and logic:\n{}",
shell_canary.diff_report(&dylib_canary));
return false;
}
let probe_fn: Symbol<fn() -> Box<dyn PluginLogic>> =
match unsafe { lib.get(b"truce_vtable_probe") } {
Ok(f) => f,
Err(e) => {
log::warn!("missing truce_vtable_probe export: {e}");
return false;
}
};
let probe = probe_fn();
if let Err(msg) = verify_probe(probe.as_ref()) {
log::error!("vtable probe failed: {msg}");
return false;
}
drop(probe);
let create_fn: Symbol<fn() -> Box<dyn PluginLogic>> =
match unsafe { lib.get(b"truce_create") } {
Ok(f) => f,
Err(e) => {
log::warn!("missing truce_create export: {e}");
return false;
}
};
let plugin = create_fn();
self.library = Some(lib);
self.plugin = Some(plugin);
self.last_modified = file_mtime(&self.dylib_path);
self.last_hash = new_hash;
log::info!("loaded plugin dylib: {}", self.dylib_path.display());
true
}
pub fn reload(&mut self) -> bool {
self.reload_pending.store(false, Ordering::Relaxed);
let state = self.plugin.as_ref().map(|p| p.save_state());
self.plugin = None;
if let Some(old) = self.library.take() {
self.leaked_handles.push(old);
}
if !self.load() {
log::warn!("hot-reload failed, plugin unavailable");
return false;
}
if let (Some(state), Some(plugin)) = (state, self.plugin.as_mut()) {
if !state.is_empty() {
plugin.load_state(&state);
}
}
log::info!("hot-reload complete (load #{}, {} leaked handles)",
self.load_counter, self.leaked_handles.len());
true
}
pub fn plugin(&self) -> Option<&dyn PluginLogic> {
self.plugin.as_ref().map(|p| p.as_ref())
}
pub fn plugin_mut(&mut self) -> Option<&mut dyn PluginLogic> {
self.plugin.as_mut().map(|p| p.as_mut())
}
pub fn is_reload_pending(&self) -> bool {
self.reload_pending.load(Ordering::Relaxed)
}
fn copy_versioned(&mut self) -> Result<PathBuf, std::io::Error> {
self.load_counter += 1;
let ext = self.dylib_path.extension()
.and_then(|e| e.to_str())
.unwrap_or("dylib");
let stem = self.dylib_path.file_stem()
.and_then(|s| s.to_str())
.unwrap_or("plugin");
let temp = std::env::temp_dir()
.join(format!("truce-hot-{stem}-{}.{ext}", self.load_counter));
std::fs::copy(&self.dylib_path, &temp)?;
Ok(temp)
}
}
impl Drop for NativeLoader {
fn drop(&mut self) {
self.plugin = None;
}
}
fn watch_loop(path: &std::path::Path, flag: &AtomicBool) {
let mut last_mtime = file_mtime(path);
loop {
std::thread::sleep(std::time::Duration::from_millis(500));
let mtime = file_mtime(path);
if mtime > last_mtime {
std::thread::sleep(std::time::Duration::from_millis(200));
last_mtime = file_mtime(path);
flag.store(true, Ordering::Relaxed);
}
}
}
fn file_mtime(path: &std::path::Path) -> SystemTime {
std::fs::metadata(path)
.and_then(|m| m.modified())
.unwrap_or(SystemTime::UNIX_EPOCH)
}
fn crc32_file(path: &std::path::Path) -> u32 {
let Ok(data) = std::fs::read(path) else { return 0 };
crc32(&data)
}
fn crc32(data: &[u8]) -> u32 {
let mut crc: u32 = 0xFFFFFFFF;
for &byte in data {
crc ^= byte as u32;
for _ in 0..8 {
if crc & 1 != 0 {
crc = (crc >> 1) ^ 0xEDB88320;
} else {
crc >>= 1;
}
}
}
!crc
}