automagic hot reloading for plugins

This commit is contained in:
Dmitry Stepanov
2024-03-19 23:03:52 +03:00
parent 5762ef5e75
commit 617948cac1
3 changed files with 249 additions and 93 deletions
+188 -72
View File
@@ -78,8 +78,9 @@ use raw_window_handle::HasRawWindowHandle;
#[cfg(not(target_arch = "wasm32"))]
use std::{ffi::CString, num::NonZeroU32};
use std::ffi::OsStr;
use std::io::Cursor;
use std::sync::atomic;
use std::sync::atomic::AtomicBool;
use std::{
any::TypeId,
collections::{HashSet, VecDeque},
@@ -94,7 +95,9 @@ use std::{
};
use crate::plugin::dynamic::DynamicPlugin;
use crate::plugin::PluginContainer;
use crate::plugin::{DynamicPluginState, PluginContainer};
use fyrox_core::notify;
use fyrox_core::notify::{EventKind, RecursiveMode, Watcher};
use fyrox_core::visitor::Visitor;
use winit::{
dpi::{Position, Size},
@@ -1602,6 +1605,15 @@ impl Engine {
self.handle_async_scene_loading(dt, lag, window_target);
self.pre_update(dt, window_target, lag, switches);
self.post_update(dt, &Default::default());
#[cfg(any(target_os = "windows", target_os = "macos", target_os = "linux"))]
{
if let Err(message) = block_on(self.reload_dynamic_plugins(dt, window_target, lag)) {
Log::err(format!(
"Unable to reload dynamic plugins. Reason: {message}"
))
}
}
}
fn handle_async_scene_loading(
@@ -2301,18 +2313,25 @@ impl Engine {
}
}
fn register_plugin(&self, plugin: &dyn Plugin) {
*self
.serialization_context
fn register_plugin_internal(
serialization_context: &Arc<SerializationContext>,
resource_manager: &ResourceManager,
plugin: &dyn Plugin,
) {
*serialization_context
.script_constructors
.context_type_id
.lock() = plugin.type_id();
plugin.register(PluginRegistrationContext {
serialization_context: &self.serialization_context,
resource_manager: &self.resource_manager,
serialization_context,
resource_manager,
});
}
fn register_plugin(&self, plugin: &dyn Plugin) {
Self::register_plugin_internal(&self.serialization_context, &self.resource_manager, plugin)
}
/// Adds a new static plugin.
pub fn add_plugin<P>(&mut self, plugin: P)
where
@@ -2327,14 +2346,81 @@ impl Engine {
/// given path and searches for `fyrox_plugin` function. This function is called to create a
/// plugin instance. This method will fail if there's no dynamic library at the given path or
/// the `fyrox_plugin` function is not found.
pub fn add_dynamic_plugin<P>(&mut self, path: P) -> Result<(), String>
///
/// # Hot reloading
///
/// This method can enable hot reloading for the plugin, by setting `reload_when_changed` parameter
/// to `true`. When enabled, the engine will clone the library to implementation-defined path
/// and load it. It will setup file system watcher to receive changes from the OS and reload
/// the plugin.
pub fn add_dynamic_plugin<P>(
&mut self,
path: P,
reload_when_changed: bool,
use_relative_paths: bool,
) -> Result<(), String>
where
P: AsRef<OsStr> + 'static,
P: AsRef<Path> + 'static,
{
let dynamic = DynamicPlugin::load(path)?;
self.register_plugin(dynamic.plugin());
let plugin = PluginContainer::Dynamic(dynamic);
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 lib_path = source_lib_path.with_extension("module");
std::fs::copy(&source_lib_path, &lib_path).map_err(|e| e.to_string())?;
let need_reload = Arc::new(AtomicBool::new(false));
let need_reload_clone = need_reload.clone();
let lib_path_clone = 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...",
lib_path_clone.display()
))
}
}
})
.map_err(|e| e.to_string())?;
watcher
.watch(&source_lib_path, RecursiveMode::NonRecursive)
.map_err(|e| e.to_string())?;
PluginContainer::Dynamic {
plugin: DynamicPluginState::Loaded(DynamicPlugin::load(lib_path.as_os_str())?),
lib_path,
source_lib_path,
watcher: Some(watcher),
need_reload,
}
} else {
PluginContainer::Dynamic {
plugin: DynamicPluginState::Loaded(DynamicPlugin::load(
source_lib_path.as_os_str(),
)?),
lib_path: source_lib_path.clone(),
source_lib_path,
watcher: None,
need_reload: Default::default(),
}
};
self.register_plugin(plugin.deref());
self.plugins.push(plugin);
Ok(())
@@ -2354,27 +2440,36 @@ impl Engine {
window_target: &EventLoopWindowTarget<()>,
lag: &mut f32,
) -> Result<(), String> {
// TODO: This method is a mess, refactor this.
// Search for scenes, that has scripts provided by a dynamic plugin.
// TODO: Add scene nodes here too; plugins can provide custom scene node types.
let mut scenes = FxHashSet::default();
for plugin in self.plugins.iter() {
if let PluginContainer::Dynamic(dynamic) = plugin {
let plugin_type_id = dynamic.plugin().type_id();
if let PluginContainer::Dynamic {
plugin: DynamicPluginState::Loaded(dynamic),
need_reload,
..
} = plugin
{
if need_reload.load(atomic::Ordering::Relaxed) {
let plugin_type_id = dynamic.plugin().type_id();
for (handle, scene) in self.scenes.pair_iter() {
'node_loop: for node in scene.graph.linear_iter() {
for script in node.scripts() {
let script_id = script.deref().id();
for (handle, scene) in self.scenes.pair_iter() {
'node_loop: for node in scene.graph.linear_iter() {
for script in node.scripts() {
let script_id = script.deref().id();
if let Some(constructor) = self
.serialization_context
.script_constructors
.map()
.get(&script_id)
{
if constructor.source_type_id == plugin_type_id {
scenes.insert(handle);
break 'node_loop;
if let Some(constructor) = self
.serialization_context
.script_constructors
.map()
.get(&script_id)
{
if constructor.source_type_id == plugin_type_id {
scenes.insert(handle);
break 'node_loop;
}
}
}
}
@@ -2386,14 +2481,21 @@ impl Engine {
// Search for script constructors, that belongs to dynamic plugins.
let mut constructors = FxHashSet::default();
for plugin in self.plugins.iter() {
if let PluginContainer::Dynamic(dynamic) = plugin {
let plugin_type_id = dynamic.plugin().type_id();
if let PluginContainer::Dynamic {
plugin: DynamicPluginState::Loaded(dynamic),
need_reload,
..
} = plugin
{
if need_reload.load(atomic::Ordering::Relaxed) {
let plugin_type_id = dynamic.plugin().type_id();
for (type_uuid, constructor) in
self.serialization_context.script_constructors.map().iter()
{
if constructor.source_type_id == plugin_type_id {
constructors.insert(*type_uuid);
for (type_uuid, constructor) in
self.serialization_context.script_constructors.map().iter()
{
if constructor.source_type_id == plugin_type_id {
constructors.insert(*type_uuid);
}
}
}
}
@@ -2421,57 +2523,70 @@ impl Engine {
serialized_scenes.insert(scene_handle, (ticket, binary_blob.into_inner()));
}
// Unload the plugins.
enum TemporaryStorage {
StaticPlugin(Box<dyn Plugin>),
DynamicPlugin { path: PathBuf, binary_blob: Vec<u8> },
}
let mut plugins = Vec::new();
for container in self.plugins.drain(..) {
match container {
PluginContainer::Static(plugin) => {
plugins.push(TemporaryStorage::StaticPlugin(plugin))
}
PluginContainer::Dynamic(mut dynamic) => {
let mut visitor = Visitor::new();
dynamic
.plugin_mut()
.visit("Plugin", &mut visitor)
.map_err(|e| e.to_string())?;
let mut binary_blob = Cursor::new(Vec::<u8>::new());
visitor
.save_binary_to_memory(&mut binary_blob)
.map_err(|e| e.to_string())?;
// Unload all dynamic plugins.
for container in self.plugins.iter_mut() {
if let PluginContainer::Dynamic {
plugin,
need_reload,
source_lib_path,
lib_path,
..
} = container
{
if need_reload.load(atomic::Ordering::Relaxed) {
if let DynamicPluginState::Loaded(dynamic) = plugin {
let mut visitor = Visitor::new();
dynamic
.plugin_mut()
.visit("Plugin", &mut visitor)
.map_err(|e| e.to_string())?;
let mut binary_blob = Cursor::new(Vec::<u8>::new());
visitor
.save_binary_to_memory(&mut binary_blob)
.map_err(|e| e.to_string())?;
// Store the path of the dynamic plugin. The plugin itself will be dropped and
// thus unloaded.
plugins.push(TemporaryStorage::DynamicPlugin {
path: dynamic.path().to_path_buf(),
binary_blob: binary_blob.into_inner(),
})
*plugin = DynamicPluginState::Unloaded {
binary_blob: binary_blob.into_inner(),
};
// Replace the module.
std::fs::copy(&source_lib_path, &lib_path).map_err(|e| e.to_string())?;
}
}
}
}
for plugin in plugins {
match plugin {
TemporaryStorage::StaticPlugin(plugin) => {
self.plugins.push(PluginContainer::Static(plugin))
}
TemporaryStorage::DynamicPlugin { path, binary_blob } => {
let mut dynamic = DynamicPlugin::load(path)?;
let mut reloaded_indices = Vec::new();
for (plugin_index, container) in self.plugins.iter_mut().enumerate() {
if let PluginContainer::Dynamic {
plugin,
lib_path,
need_reload,
..
} = container
{
if let DynamicPluginState::Unloaded { binary_blob } = plugin {
let mut dynamic = DynamicPlugin::load(lib_path)?;
let mut visitor =
Visitor::load_from_memory(&binary_blob).map_err(|e| e.to_string())?;
Visitor::load_from_memory(binary_blob).map_err(|e| e.to_string())?;
dynamic
.plugin_mut()
.visit("Plugin", &mut visitor)
.map_err(|e| e.to_string())?;
*plugin = DynamicPluginState::Loaded(dynamic);
// Re-register the plugin. This is needed, because it might contain new script
// types (or removed ones too).
self.register_plugin(dynamic.plugin());
Self::register_plugin_internal(
&self.serialization_context,
&self.resource_manager,
plugin.as_loaded_ref().plugin(),
);
self.plugins.push(PluginContainer::Dynamic(dynamic))
need_reload.store(false, atomic::Ordering::Relaxed);
reloaded_indices.push(plugin_index);
}
}
}
@@ -2494,7 +2609,8 @@ impl Engine {
}
// Call `on_loaded` for plugins, so they could restore some runtime non-serializable state.
for plugin in self.plugins.iter_mut() {
for plugin_index in reloaded_indices {
let plugin = &mut self.plugins[plugin_index];
plugin.on_loaded(PluginContext {
scenes: &mut self.scenes,
resource_manager: &self.resource_manager,
+2 -12
View File
@@ -1,17 +1,13 @@
//! Dynamic plugins with hot-reloading ability.
use crate::plugin::Plugin;
use std::{
ffi::OsStr,
path::{Path, PathBuf},
};
use std::ffi::OsStr;
/// Dynamic plugin, that is loaded from a dynamic library. Usually it is used for hot reloading,
/// it is strongly advised not to use it in production builds, because it is slower than statically
/// linked plugins and it could be unsafe if different compiler versions are used.
pub struct DynamicPlugin {
pub(super) plugin: Box<dyn Plugin>,
path: PathBuf,
// Keep the library loaded.
// Must be last!
#[allow(dead_code)]
@@ -27,7 +23,7 @@ impl DynamicPlugin {
P: AsRef<OsStr>,
{
unsafe {
let lib = libloading::Library::new(path.as_ref()).map_err(|e| e.to_string())?;
let lib = libloading::Library::new(path).map_err(|e| e.to_string())?;
let entry = lib
.get::<PluginEntryPoint>("fyrox_plugin".as_bytes())
@@ -35,17 +31,11 @@ impl DynamicPlugin {
Ok(Self {
plugin: entry(),
path: PathBuf::from(path.as_ref()),
lib,
})
}
}
/// Returns the path from which the plugin was loaded from.
pub fn path(&self) -> &Path {
&self.path
}
/// Return a reference to the plugin interface of the dynamic plugin.
pub fn plugin(&self) -> &dyn Plugin {
&*self.plugin
+59 -9
View File
@@ -6,7 +6,7 @@ pub mod dynamic;
use crate::{
asset::manager::ResourceManager,
core::{pool::Handle, visitor::VisitError},
core::{notify::RecommendedWatcher, pool::Handle, visitor::Visit, visitor::VisitError},
engine::{
task::TaskPoolHandler, AsyncSceneLoader, GraphicsContext, PerformanceStatistics,
ScriptProcessor, SerializationContext,
@@ -16,21 +16,71 @@ use crate::{
plugin::dynamic::DynamicPlugin,
scene::{Scene, SceneContainer},
};
use fyrox_core::visitor::Visit;
use std::{
any::Any,
ops::{Deref, DerefMut},
path::Path,
sync::Arc,
path::{Path, PathBuf},
sync::{atomic::AtomicBool, Arc},
};
use winit::event_loop::EventLoopWindowTarget;
/// Actual state of a dynamic plugin.
pub enum DynamicPluginState {
/// Unloaded plugin.
Unloaded {
/// Serialized content of the plugin.
binary_blob: Vec<u8>,
},
/// Loaded plugin.
Loaded(DynamicPlugin),
}
impl DynamicPluginState {
/// Tries to interpret the state as [`Self::Loaded`], panics if the plugin is unloaded.
pub fn as_loaded_ref(&self) -> &DynamicPlugin {
match self {
DynamicPluginState::Unloaded { .. } => {
panic!("Cannot obtain a reference to the plugin, because it is unloaded!")
}
DynamicPluginState::Loaded(dynamic) => dynamic,
}
}
/// Tries to interpret the state as [`Self::Loaded`], panics if the plugin is unloaded.
pub fn as_loaded_mut(&mut self) -> &mut DynamicPlugin {
match self {
DynamicPluginState::Unloaded { .. } => {
panic!("Cannot obtain a reference to the plugin, because it is unloaded!")
}
DynamicPluginState::Loaded(dynamic) => dynamic,
}
}
}
/// A wrapper for various plugin types.
pub enum PluginContainer {
/// Statically linked plugin.
/// Statically linked plugin. Such plugins are meant to be used in final builds, to maximize
/// performance of the game.
Static(Box<dyn Plugin>),
/// Dynamically linked plugin.
Dynamic(DynamicPlugin),
/// Dynamically linked plugin. Such plugins are meant to be used in development mode for rapid
/// prototyping.
Dynamic {
/// Dynamic plugin state.
plugin: DynamicPluginState,
/// Target path of the library of the plugin.
lib_path: PathBuf,
/// Path to the source file, that is emitted by the compiler. If hot reloading is enabled,
/// this library will be cloned to `lib_path` and loaded. This is needed, because usually
/// OS locks the library and it is not possible to overwrite it while it is loaded in a process.
source_lib_path: PathBuf,
/// Optional file system watcher, that is configured to watch the source library and re-load
/// the plugin if the source library has changed. If the watcher is `None`, then hot reloading
/// is disabled.
watcher: Option<RecommendedWatcher>,
/// A flag, that tells the engine that the plugin needs to be reloaded. Usually the engine
/// will do that at the end of the update tick.
need_reload: Arc<AtomicBool>,
},
}
impl Deref for PluginContainer {
@@ -39,7 +89,7 @@ impl Deref for PluginContainer {
fn deref(&self) -> &Self::Target {
match self {
PluginContainer::Static(plugin) => &**plugin,
PluginContainer::Dynamic(dynamic) => &*dynamic.plugin,
PluginContainer::Dynamic { plugin, .. } => &*plugin.as_loaded_ref().plugin,
}
}
}
@@ -48,7 +98,7 @@ impl DerefMut for PluginContainer {
fn deref_mut(&mut self) -> &mut Self::Target {
match self {
PluginContainer::Static(plugin) => &mut **plugin,
PluginContainer::Dynamic(dynamic) => &mut *dynamic.plugin,
PluginContainer::Dynamic { plugin, .. } => &mut *plugin.as_loaded_mut().plugin,
}
}
}