use crate::{
core::{
log::Log,
notify::{self, EventKind, RecommendedWatcher, RecursiveMode, Watcher},
},
plugin::{DynamicPlugin, Plugin},
};
use std::{
fs::File,
io::Read,
path::{Path, PathBuf},
sync::{
atomic::{self, AtomicBool},
Arc,
},
};
pub struct DyLibHandle {
pub(super) plugin: Box<dyn Plugin>,
#[allow(dead_code)]
#[cfg(any(unix, windows))]
lib: libloading::Library,
}
#[cfg(any(unix, windows))]
type PluginEntryPoint = fn() -> Box<dyn Plugin>;
impl DyLibHandle {
pub fn load<P>(#[allow(unused_variables)] path: P) -> Result<Self, String>
where
P: libloading::AsFilename,
{
#[cfg(any(unix, windows))]
unsafe {
let lib = libloading::Library::new(path).map_err(|e| e.to_string())?;
let entry = lib
.get::<PluginEntryPoint>("fyrox_plugin".as_bytes())
.map_err(|e| e.to_string())?;
Ok(Self {
plugin: entry(),
lib,
})
}
#[cfg(not(any(unix, windows)))]
{
panic!("Unsupported platform!")
}
}
pub fn plugin(&self) -> &dyn Plugin {
&*self.plugin
}
pub(crate) fn plugin_mut(&mut self) -> &mut dyn Plugin {
&mut *self.plugin
}
}
pub struct DyLibDynamicPlugin {
state: PluginState,
lib_path: PathBuf,
source_lib_path: PathBuf,
_watcher: Option<RecommendedWatcher>,
need_reload: Arc<AtomicBool>,
}
impl DyLibDynamicPlugin {
pub fn new<P>(
path: P,
reload_when_changed: bool,
use_relative_paths: bool,
) -> Result<Self, String>
where
P: AsRef<Path> + 'static,
{
let source_lib_path = if use_relative_paths {
let exe_folder = std::env::current_exe()
.map_err(|e| e.to_string())?
.parent()
.map(|p| p.to_path_buf())
.unwrap_or_default();
exe_folder.join(path.as_ref())
} else {
path.as_ref().to_path_buf()
};
let plugin = if reload_when_changed {
let mut suffix = std::env::current_exe()
.ok()
.and_then(|p| p.file_stem().map(|s| s.to_owned()))
.unwrap_or_default();
suffix.push(".module");
let lib_path = source_lib_path.with_extension(suffix);
try_copy_library(&source_lib_path, &lib_path)?;
let need_reload = Arc::new(AtomicBool::new(false));
let need_reload_clone = need_reload.clone();
let source_lib_path_clone = source_lib_path.clone();
let mut watcher =
notify::recommended_watcher(move |event: notify::Result<notify::Event>| {
if let Ok(event) = event {
if let EventKind::Modify(_) | EventKind::Create(_) = event.kind {
need_reload_clone.store(true, atomic::Ordering::Relaxed);
Log::warn(format!(
"Plugin {} was changed. Performing hot reloading...",
source_lib_path_clone.display()
))
}
}
})
.map_err(|e| e.to_string())?;
watcher
.watch(&source_lib_path, RecursiveMode::NonRecursive)
.map_err(|e| e.to_string())?;
Log::info(format!(
"Watching for changes in plugin {source_lib_path:?}..."
));
DyLibDynamicPlugin {
state: PluginState::Loaded(DyLibHandle::load(lib_path.as_os_str())?),
lib_path,
source_lib_path: source_lib_path.clone(),
_watcher: Some(watcher),
need_reload,
}
} else {
DyLibDynamicPlugin {
state: PluginState::Loaded(DyLibHandle::load(source_lib_path.as_os_str())?),
lib_path: source_lib_path.clone(),
source_lib_path: source_lib_path.clone(),
_watcher: None,
need_reload: Default::default(),
}
};
Ok(plugin)
}
}
impl DynamicPlugin for DyLibDynamicPlugin {
fn as_loaded_ref(&self) -> &dyn Plugin {
&*self.state.as_loaded_ref().plugin
}
fn as_loaded_mut(&mut self) -> &mut dyn Plugin {
&mut *self.state.as_loaded_mut().plugin
}
fn is_reload_needed_now(&self) -> bool {
self.need_reload.load(atomic::Ordering::Relaxed)
}
fn display_name(&self) -> String {
format!("{:?}", self.source_lib_path)
}
fn is_loaded(&self) -> bool {
matches!(self.state, PluginState::Loaded { .. })
}
fn reload(
&mut self,
fill_and_register: &mut dyn FnMut(&mut dyn Plugin) -> Result<(), String>,
) -> Result<(), String> {
let PluginState::Loaded(_) = &mut self.state else {
return Err("cannot unload non-loaded plugin".to_string());
};
self.state = PluginState::Unloaded;
Log::info(format!(
"Plugin {:?} was unloaded successfully!",
self.source_lib_path
));
try_copy_library(&self.source_lib_path, &self.lib_path)?;
Log::info(format!(
"{:?} plugin's module {} was successfully cloned to {}.",
self.source_lib_path,
self.source_lib_path.display(),
self.lib_path.display()
));
let mut dynamic = DyLibHandle::load(&self.lib_path)?;
fill_and_register(dynamic.plugin_mut())?;
self.state = PluginState::Loaded(dynamic);
self.need_reload.store(false, atomic::Ordering::Relaxed);
Log::info(format!(
"Plugin {:?} was reloaded successfully!",
self.source_lib_path
));
Ok(())
}
}
enum PluginState {
Unloaded,
Loaded(DyLibHandle),
}
impl PluginState {
pub fn as_loaded_ref(&self) -> &DyLibHandle {
match self {
PluginState::Unloaded => {
panic!("Cannot obtain a reference to the plugin, because it is unloaded!")
}
PluginState::Loaded(dynamic) => dynamic,
}
}
pub fn as_loaded_mut(&mut self) -> &mut DyLibHandle {
match self {
PluginState::Unloaded => {
panic!("Cannot obtain a reference to the plugin, because it is unloaded!")
}
PluginState::Loaded(dynamic) => dynamic,
}
}
}
fn try_copy_library(source_lib_path: &Path, lib_path: &Path) -> Result<(), String> {
if let Err(err) = std::fs::copy(source_lib_path, lib_path) {
let mut src_lib_file = File::open(source_lib_path).map_err(|e| e.to_string())?;
let mut src_lib_file_content = Vec::new();
src_lib_file
.read_to_end(&mut src_lib_file_content)
.map_err(|e| e.to_string())?;
let mut lib_file = File::open(lib_path).map_err(|e| e.to_string())?;
let mut lib_file_content = Vec::new();
lib_file
.read_to_end(&mut lib_file_content)
.map_err(|e| e.to_string())?;
if src_lib_file_content != lib_file_content {
return Err(format!(
"Unable to clone the library {} to {}. It is required, because source \
library has {} size, but loaded has {} size and the content does not match. \
Exact reason: {:?}",
source_lib_path.display(),
lib_path.display(),
src_lib_file_content.len(),
lib_file_content.len(),
err
));
}
}
Ok(())
}