mirror of
https://github.com/FyroxEngine/Fyrox.git
synced 2026-10-12 02:02:20 +00:00
automagic hot reloading for plugins
This commit is contained in:
+188
-72
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user