From 89371e038c9ceeb41adae67689a54562e5f4ad6d Mon Sep 17 00:00:00 2001 From: kkolyan Date: Thu, 10 Oct 2024 03:41:41 +0400 Subject: [PATCH] refactor hot reload to allow custom dynamic plugins besides dylib-based --- editor/src/lib.rs | 22 ++- fyrox-impl/src/engine/mod.rs | 243 ++++++---------------------- fyrox-impl/src/plugin/dynamic.rs | 262 ++++++++++++++++++++++++++++++- fyrox-impl/src/plugin/mod.rs | 90 ++++------- 4 files changed, 355 insertions(+), 262 deletions(-) diff --git a/editor/src/lib.rs b/editor/src/lib.rs index 8dfb4b6a8..e8e4e16a2 100644 --- a/editor/src/lib.rs +++ b/editor/src/lib.rs @@ -122,7 +122,7 @@ use crate::{ shader::{ShaderResource, ShaderResourceExtension}, Material, MaterialResource, }, - plugin::{Plugin, PluginContainer}, + plugin::{Plugin, PluginContainer, AbstractDynamicPlugin, dynamic::DyLibPlugin}, resource::texture::{ CompressionOptions, TextureImportOptions, TextureKind, TextureMinificationFilter, TextureResource, TextureResourceExtension, @@ -2641,10 +2641,20 @@ impl Editor { ) -> Result<(), String> where P: AsRef + 'static, + { + self.add_dynamic_plugin_custom(DyLibPlugin::new(path, reload_when_changed, use_relative_paths)?) + } + + pub fn add_dynamic_plugin_custom

( + &mut self, + plugin: P, + ) -> Result<(), String> + where + P: AbstractDynamicPlugin + 'static, { let plugin = self.engine - .add_dynamic_plugin(path, reload_when_changed, use_relative_paths)?; + .add_dynamic_plugin(plugin); *self.inspector.property_editors.context_type_id.lock() = plugin.type_id(); self.inspector .property_editors @@ -2946,13 +2956,11 @@ fn update(editor: &mut Editor, window_target: &EventLoopWindowTarget<()>) { for plugin_index in 0..editor.engine.plugins().len() { let plugin = &editor.engine.plugins()[plugin_index]; - if let PluginContainer::Dynamic { - need_reload, state, .. - } = plugin + if let PluginContainer::Dynamic(plugin) = plugin { - let plugin_type_id = state.as_loaded_ref().plugin().type_id(); + let plugin_type_id = plugin.as_loaded_ref().type_id(); - if need_reload.load(Ordering::SeqCst) { + if plugin.is_reload_needed_now() { // Clear command stacks for scenes. This is mandatory step, because command stack // could contain objects from plugins and any attempt to use them after the plugin is // unloaded will cause crash. diff --git a/fyrox-impl/src/engine/mod.rs b/fyrox-impl/src/engine/mod.rs index 66fafbbf6..c11a2d0b0 100644 --- a/fyrox-impl/src/engine/mod.rs +++ b/fyrox-impl/src/engine/mod.rs @@ -42,8 +42,6 @@ use crate::{ futures::{executor::block_on, future::join_all}, instant, log::Log, - notify, - notify::{EventKind, RecursiveMode, Watcher}, pool::Handle, reflect::Reflect, task::TaskPool, @@ -65,7 +63,7 @@ use crate::{ Material, }, plugin::{ - dynamic::DynamicPlugin, DynamicPluginState, Plugin, PluginContainer, PluginContext, + AbstractDynamicPlugin, Plugin, PluginContainer, PluginContext, PluginRegistrationContext, }, renderer::{framework::error::FrameworkError, Renderer}, @@ -105,12 +103,10 @@ use std::{ any::TypeId, collections::{HashSet, VecDeque}, fmt::{Display, Formatter}, - fs::File, - io::{Cursor, Read}, + io::Cursor, ops::{Deref, DerefMut}, path::{Path, PathBuf}, sync::{ - atomic::{self, AtomicBool}, mpsc::{channel, Receiver, Sender}, Arc, }, @@ -1243,38 +1239,6 @@ pub(crate) fn initialize_resource_manager_loaders( state.loaders.set(TileMapBrushLoader {}); } -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) { - // The library could already be copied and loaded, thus cannot be replaced. For - // example - by the running editor, that also uses hot reloading. Check for matching - // content, and if does not match, pass the error further. - 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(()) -} - impl Engine { /// Creates new instance of engine from given initialization parameters. Automatically creates all sub-systems /// (sound, ui, resource manager, etc.) **except** graphics context. Graphics context should be created manually @@ -2299,106 +2263,25 @@ impl Engine { self.plugins.push(PluginContainer::Static(Box::new(plugin))); } - /// Tries to add a new dynamic plugin. This method attempts to load a dynamic library by the - /// 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. - /// - /// # 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. + /// Adds a new abstract dynamic plugin pub fn add_dynamic_plugin

( &mut self, - path: P, - reload_when_changed: bool, - use_relative_paths: bool, - ) -> Result<&dyn Plugin, String> + plugin: P, + ) -> &dyn Plugin where - P: AsRef + 'static, + P: AbstractDynamicPlugin + '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 { - // Make sure each process will its own copy of the module. This is needed to prevent - // issues when there are two or more running processes and a library of the plugin - // changes. If the library is present in one instance in both (or more) processes, then - // it is impossible to replace it on disk. To prevent this, we need to add a suffix with - // executable name. - 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| { - 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())?; + let display_name = plugin.display_name(); - watcher - .watch(&source_lib_path, RecursiveMode::NonRecursive) - .map_err(|e| e.to_string())?; - - Log::info(format!( - "Watching for changes in plugin {source_lib_path:?}..." - )); - - PluginContainer::Dynamic { - state: DynamicPluginState::Loaded(DynamicPlugin::load(lib_path.as_os_str())?), - lib_path, - source_lib_path: source_lib_path.clone(), - watcher: Some(watcher), - need_reload, - } - } else { - PluginContainer::Dynamic { - state: DynamicPluginState::Loaded(DynamicPlugin::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(), - } - }; + let plugin_container = PluginContainer::Dynamic(Box::new(plugin)); - self.register_plugin(plugin.deref()); - self.plugins.push(plugin); + self.register_plugin(plugin_container.deref()); + self.plugins.push(plugin_container); Log::info(format!( - "Plugin {source_lib_path:?} was loaded successfully" - )); + "Plugin {display_name:?} was loaded successfully")); - Ok(&**self.plugins.last().unwrap()) + &**self.plugins.last().unwrap() } /// Tries to reload a specified plugin. This method tries to perform least invasive reloading, by @@ -2411,25 +2294,22 @@ impl Engine { lag: &mut f32, ) -> Result<(), String> { let plugin_container = &mut self.plugins[plugin_index]; - let PluginContainer::Dynamic { - state, - source_lib_path, - lib_path, - need_reload, - .. - } = plugin_container + let PluginContainer::Dynamic(plugin) = plugin_container else { return Err(format!( "Plugin {plugin_index} is static and cannot be reloaded!", )); }; - if matches!(state, DynamicPluginState::Unloaded { .. }) { + if !plugin.is_loaded() { + // TODO: this means that something bad happened during plugin reloading. + // don't we want to recover from this situation by trying to load it again + // (maybe with clearing `need_reload` flag, to perform new attempt only when something is changed) return Err(format!("Cannot reload unloaded plugin {plugin_index}!")); } - let plugin_type_id = state.as_loaded_ref().plugin().type_id(); - let plugin_assembly_name = state.as_loaded_ref().plugin().assembly_name(); + let plugin_type_id = plugin.as_loaded_ref().type_id(); + let plugin_assembly_name = plugin.as_loaded_ref().assembly_name(); // Collect all the data that belongs to the plugin let mut scenes_state = Vec::new(); @@ -2438,7 +2318,7 @@ impl Engine { scene_handle, scene, &self.serialization_context, - state.as_loaded_ref().plugin(), + plugin.as_loaded_ref(), )? { scenes_state.push(data); } @@ -2455,7 +2335,7 @@ impl Engine { Handle::NONE, &mut data.scene, &self.serialization_context, - state.as_loaded_ref().plugin(), + plugin.as_loaded_ref(), )? { prefab_scenes.push((model.clone(), scene_state)); } @@ -2541,47 +2421,29 @@ impl Engine { } } - // Unload the plugin. - if let DynamicPluginState::Loaded(dynamic) = state { - let mut visitor = hotreload::make_writing_visitor(); - dynamic - .plugin_mut() - .visit("Plugin", &mut visitor) - .map_err(|e| e.to_string())?; - let mut binary_blob = Cursor::new(Vec::::new()); - visitor - .save_binary_to_memory(&mut binary_blob) - .map_err(|e| e.to_string())?; - - Log::info(format!( - "Plugin {plugin_index} was serialized successfully!" - )); - - // Explicitly drop the visitor to prevent any destructors from the previous version of - // the plugin to run at the end of the scope. This could happen, because the visitor - // manages serialized smart pointers and if they'll be kept alive longer than the plugin - // there's a very high chance of hard crash. - drop(visitor); - - *state = DynamicPluginState::Unloaded { - binary_blob: binary_blob.into_inner(), - }; - - Log::info(format!("Plugin {plugin_index} was unloaded successfully!")); + let mut visitor = hotreload::make_writing_visitor(); + plugin + .as_loaded_mut() + .visit("Plugin", &mut visitor) + .map_err(|e| e.to_string())?; + let mut binary_blob = Cursor::new(Vec::::new()); + visitor + .save_binary_to_memory(&mut binary_blob) + .map_err(|e| e.to_string())?; - // Replace the module. - try_copy_library(source_lib_path, lib_path)?; + Log::info(format!( + "Plugin {plugin_index} was serialized successfully!" + )); - Log::info(format!( - "{plugin_index} plugin's module {} was successfully cloned to {}.", - source_lib_path.display(), - lib_path.display() - )); - } + // Explicitly drop the visitor to prevent any destructors from the previous version of + // the plugin to run at the end of the scope. This could happen, because the visitor + // manages serialized smart pointers and if they'll be kept alive longer than the plugin + // there's a very high chance of hard crash. + drop(visitor); - if let DynamicPluginState::Unloaded { binary_blob } = state { - let mut dynamic = DynamicPlugin::load(lib_path)?; + let binary_blob = binary_blob.into_inner(); + plugin.reload(&mut |plugin| { // Re-register the plugin. This is needed, because it might contain new script/node/widget // types (or removed ones too). This is done right before deserialization, because plugin // might contain some entities, that have dynamic registration. @@ -2589,27 +2451,22 @@ impl Engine { &self.serialization_context, &self.widget_constructors, &self.resource_manager, - dynamic.plugin(), + plugin, ); let mut visitor = hotreload::make_reading_visitor( - binary_blob, + &binary_blob, &self.serialization_context, &self.resource_manager, &self.widget_constructors, ) .map_err(|e| e.to_string())?; - dynamic - .plugin_mut() + + plugin .visit("Plugin", &mut visitor) .map_err(|e| e.to_string())?; - - *state = DynamicPluginState::Loaded(dynamic); - - need_reload.store(false, atomic::Ordering::Relaxed); - - Log::info(format!("Plugin {plugin_index} was reloaded successfully!")); - } + Ok(()) + })?; // Deserialize prefab scene content. for (model, scene_state) in prefab_scenes { @@ -2635,7 +2492,7 @@ impl Engine { } // Call `on_loaded` for plugins, so they could restore some runtime non-serializable state. - state.as_loaded_mut().plugin_mut().on_loaded(PluginContext { + plugin.as_loaded_mut().on_loaded(PluginContext { scenes: &mut self.scenes, resource_manager: &self.resource_manager, user_interfaces: &mut self.user_interfaces, @@ -2674,11 +2531,9 @@ impl Engine { F: FnMut(&dyn Plugin), { for plugin_index in 0..self.plugins.len() { - if let PluginContainer::Dynamic { - ref need_reload, .. - } = self.plugins[plugin_index] + if let PluginContainer::Dynamic(plugin) = &self.plugins[plugin_index] { - if need_reload.load(atomic::Ordering::Relaxed) { + if plugin.is_reload_needed_now() { self.reload_plugin(plugin_index, dt, window_target, lag)?; on_reloaded(self.plugins[plugin_index].deref_mut()); diff --git a/fyrox-impl/src/plugin/dynamic.rs b/fyrox-impl/src/plugin/dynamic.rs index e093265c1..51ceb69c0 100644 --- a/fyrox-impl/src/plugin/dynamic.rs +++ b/fyrox-impl/src/plugin/dynamic.rs @@ -20,8 +20,26 @@ //! Dynamic plugins with hot-reloading ability. -use crate::plugin::Plugin; -use std::ffi::OsStr; +use crate::{ + core::{ + log::Log, + notify::{self, EventKind, RecursiveMode, Watcher}, + }, + plugin::Plugin, +}; +use crate::core::notify::RecommendedWatcher; +use std::{ + fs::File, + io::Read, + path::{Path, PathBuf}, + sync::{ + atomic::{self, AtomicBool}, + Arc, + }, + ffi::OsStr +}; + +use crate::plugin::AbstractDynamicPlugin; /// 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 @@ -74,3 +92,243 @@ impl DynamicPlugin { &mut *self.plugin } } + + +/// Implementation of DynamicPluginTrait that [re]loads Rust code from Rust dylib . +pub struct DyLibPlugin { + /// Dynamic plugin state. + state: 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, + /// 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, +} + +impl DyLibPlugin { + + /// Tries to create a new dynamic plugin. This method attempts to load a dynamic library by the + /// 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. + /// + /// # 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 new

( + path: P, + reload_when_changed: bool, + use_relative_paths: bool, + ) -> Result + where + P: AsRef + '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 { + // Make sure each process will its own copy of the module. This is needed to prevent + // issues when there are two or more running processes and a library of the plugin + // changes. If the library is present in one instance in both (or more) processes, then + // it is impossible to replace it on disk. To prevent this, we need to add a suffix with + // executable name. + 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| { + 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 + )); + + DyLibPlugin { + state: DynamicPluginState::Loaded(DynamicPlugin::load(lib_path.as_os_str())?), + lib_path, + source_lib_path: source_lib_path.clone(), + watcher: Some(watcher), + need_reload, + } + } else { + DyLibPlugin { + state: DynamicPluginState::Loaded(DynamicPlugin::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 AbstractDynamicPlugin for DyLibPlugin { + 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, DynamicPluginState::Loaded { .. }) + } + + fn reload(&mut self, fill_and_register: &mut dyn FnMut(&mut dyn Plugin) -> Result<(), String>) -> Result<(), String> { + // Unload the plugin. + let DynamicPluginState::Loaded(_) = &mut self.state else { + return Err("cannot unload non-loaded plugin".to_string()); + }; + + self.state = DynamicPluginState::Unloaded; + + Log::info(format!( + "Plugin {:?} was unloaded successfully!", + self.source_lib_path + )); + + // Replace the module. + 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 = DynamicPlugin::load(&self.lib_path)?; + + fill_and_register(dynamic.plugin_mut())?; + + self.state = DynamicPluginState::Loaded(dynamic); + + self.need_reload.store(false, atomic::Ordering::Relaxed); + + Log::info(format!( + "Plugin {:?} was reloaded successfully!", + self.source_lib_path + )); + + Ok(()) + } +} + +/// Actual state of a dynamic plugin. +pub enum DynamicPluginState { + /// Unloaded plugin. + Unloaded, + /// 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, + } + } +} + +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) { + // The library could already be copied and loaded, thus cannot be replaced. For + // example - by the running editor, that also uses hot reloading. Check for matching + // content, and if does not match, pass the error further. + 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(()) +} diff --git a/fyrox-impl/src/plugin/mod.rs b/fyrox-impl/src/plugin/mod.rs index 67956c70d..ad560ab79 100644 --- a/fyrox-impl/src/plugin/mod.rs +++ b/fyrox-impl/src/plugin/mod.rs @@ -27,62 +27,26 @@ pub mod dynamic; use crate::{ asset::manager::ResourceManager, core::{ - notify::RecommendedWatcher, pool::Handle, reflect::Reflect, visitor::Visit, - visitor::VisitError, + pool::Handle, reflect::Reflect, visitor::{Visit, VisitError}, }, engine::{ - task::TaskPoolHandler, AsyncSceneLoader, GraphicsContext, PerformanceStatistics, - ScriptProcessor, SerializationContext, + task::TaskPoolHandler, AsyncSceneLoader, GraphicsContext, PerformanceStatistics, ScriptProcessor, SerializationContext }, event::Event, gui::{ constructor::WidgetConstructorContainer, inspector::editors::PropertyEditorDefinitionContainer, message::UiMessage, UiContainer, }, - plugin::dynamic::DynamicPlugin, scene::{Scene, SceneContainer}, }; use std::{ any::Any, ops::{Deref, DerefMut}, - path::{Path, PathBuf}, - sync::{atomic::AtomicBool, Arc}, + path::Path, + sync::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, - }, - /// 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. Such plugins are meant to be used in final builds, to maximize @@ -90,23 +54,31 @@ pub enum PluginContainer { Static(Box), /// Dynamically linked plugin. Such plugins are meant to be used in development mode for rapid /// prototyping. - Dynamic { - /// Dynamic plugin state. - state: 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, - /// 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, - }, + Dynamic(Box), +} + +/// Abstraction over different kind of plugins that can be reloaded on the fly (whatever it mean). +/// The instance is polled by engine with `is_reload_needed_now()` time to time. if it returns true, +/// then engine serializes current plugin state, then calls `unload()` and then calls `load()` +pub trait AbstractDynamicPlugin { + /// returns human-redable short description of the plugin + fn display_name(&self) -> String; + + /// engine polls is time to time to determine if it's time to reload plugin + fn is_reload_needed_now(&self) -> bool; + + /// panics if not loaded + fn as_loaded_ref(&self) -> &dyn Plugin; + + /// panics if not loaded + fn as_loaded_mut(&mut self) -> &mut dyn Plugin; + + /// returns false if something bad happends during `reload`. + fn is_loaded(&self) -> bool; + + /// it should call `fill_and_register` function on the new fresh plugin instance as + /// soon as it's created, before making `is_loaded` return true. + fn reload(&mut self, fill_and_register: &mut dyn FnMut(&mut dyn Plugin) -> Result<(), String>) -> Result<(), String>; } impl Deref for PluginContainer { @@ -115,7 +87,7 @@ impl Deref for PluginContainer { fn deref(&self) -> &Self::Target { match self { PluginContainer::Static(plugin) => &**plugin, - PluginContainer::Dynamic { state: plugin, .. } => &*plugin.as_loaded_ref().plugin, + PluginContainer::Dynamic(plugin) => plugin.as_loaded_ref(), } } } @@ -124,7 +96,7 @@ impl DerefMut for PluginContainer { fn deref_mut(&mut self) -> &mut Self::Target { match self { PluginContainer::Static(plugin) => &mut **plugin, - PluginContainer::Dynamic { state: plugin, .. } => &mut *plugin.as_loaded_mut().plugin, + PluginContainer::Dynamic(plugin) => plugin.as_loaded_mut(), } } }