Skip to main content

fyrox_impl/plugin/
dylib.rs

1// Copyright (c) 2019-present Dmitry Stepanov and Fyrox Engine contributors.
2//
3// Permission is hereby granted, free of charge, to any person obtaining a copy
4// of this software and associated documentation files (the "Software"), to deal
5// in the Software without restriction, including without limitation the rights
6// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7// copies of the Software, and to permit persons to whom the Software is
8// furnished to do so, subject to the following conditions:
9//
10// The above copyright notice and this permission notice shall be included in all
11// copies or substantial portions of the Software.
12//
13// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19// SOFTWARE.
20
21//! Dynamic plugins with hot-reloading ability.
22
23use crate::{
24    core::{
25        log::Log,
26        notify::{self, EventKind, RecommendedWatcher, RecursiveMode, Watcher},
27    },
28    plugin::{DynamicPlugin, Plugin},
29};
30use std::{
31    fs::File,
32    io::Read,
33    path::{Path, PathBuf},
34    sync::{
35        atomic::{self, AtomicBool},
36        Arc,
37    },
38};
39
40/// Dynamic plugin, that is loaded from a dynamic library. Usually it is used for hot reloading,
41/// it is strongly advised not to use it in production builds, because it is slower than statically
42/// linked plugins and it could be unsafe if different compiler versions are used.
43pub struct DyLibHandle {
44    pub(super) plugin: Box<dyn Plugin>,
45    // Keep the library loaded.
46    // Must be last!
47    #[allow(dead_code)]
48    #[cfg(any(unix, windows))]
49    lib: libloading::Library,
50}
51
52#[cfg(any(unix, windows))]
53type PluginEntryPoint = fn() -> Box<dyn Plugin>;
54
55impl DyLibHandle {
56    /// Tries to load a plugin from a dynamic library (*.dll on Windows, *.so on Unix).
57    pub fn load<P>(#[allow(unused_variables)] path: P) -> Result<Self, String>
58    where
59        P: libloading::AsFilename,
60    {
61        #[cfg(any(unix, windows))]
62        unsafe {
63            let lib = libloading::Library::new(path).map_err(|e| e.to_string())?;
64
65            let entry = lib
66                .get::<PluginEntryPoint>("fyrox_plugin".as_bytes())
67                .map_err(|e| e.to_string())?;
68
69            Ok(Self {
70                plugin: entry(),
71                lib,
72            })
73        }
74
75        #[cfg(not(any(unix, windows)))]
76        {
77            panic!("Unsupported platform!")
78        }
79    }
80
81    /// Return a reference to the plugin interface of the dynamic plugin.
82    pub fn plugin(&self) -> &dyn Plugin {
83        &*self.plugin
84    }
85
86    /// Return a reference to the plugin interface of the dynamic plugin.
87    pub(crate) fn plugin_mut(&mut self) -> &mut dyn Plugin {
88        &mut *self.plugin
89    }
90}
91
92/// Implementation of DynamicPluginTrait that (re)loads Rust code from Rust dylib .
93pub struct DyLibDynamicPlugin {
94    /// Dynamic plugin state.
95    state: PluginState,
96    /// Target path of the library of the plugin.
97    lib_path: PathBuf,
98    /// Path to the source file, that is emitted by the compiler. If hot reloading is enabled,
99    /// this library will be cloned to `lib_path` and loaded. This is needed, because usually
100    /// OS locks the library and it is not possible to overwrite it while it is loaded in a process.  
101    source_lib_path: PathBuf,
102    /// Optional file system watcher, that is configured to watch the source library and re-load
103    /// the plugin if the source library has changed. If the watcher is `None`, then hot reloading
104    /// is disabled.
105    _watcher: Option<RecommendedWatcher>,
106    /// A flag, that tells the engine that the plugin needs to be reloaded. Usually the engine
107    /// will do that at the end of the update tick.
108    need_reload: Arc<AtomicBool>,
109}
110
111impl DyLibDynamicPlugin {
112    /// Tries to create a new dynamic plugin. This method attempts to load a dynamic library by the
113    /// given path and searches for `fyrox_plugin` function. This function is called to create a
114    /// plugin instance. This method will fail if there's no dynamic library at the given path or
115    /// the `fyrox_plugin` function is not found.
116    ///
117    /// # Hot reloading
118    ///
119    /// This method can enable hot reloading for the plugin, by setting `reload_when_changed` parameter
120    /// to `true`. When enabled, the engine will clone the library to implementation-defined path
121    /// and load it. It will setup file system watcher to receive changes from the OS and reload
122    /// the plugin.
123    pub fn new<P>(
124        path: P,
125        reload_when_changed: bool,
126        use_relative_paths: bool,
127    ) -> Result<Self, String>
128    where
129        P: AsRef<Path> + 'static,
130    {
131        let source_lib_path = if use_relative_paths {
132            let exe_folder = std::env::current_exe()
133                .map_err(|e| e.to_string())?
134                .parent()
135                .map(|p| p.to_path_buf())
136                .unwrap_or_default();
137
138            exe_folder.join(path.as_ref())
139        } else {
140            path.as_ref().to_path_buf()
141        };
142
143        let plugin = if reload_when_changed {
144            // Make sure each process will its own copy of the module. This is needed to prevent
145            // issues when there are two or more running processes and a library of the plugin
146            // changes. If the library is present in one instance in both (or more) processes, then
147            // it is impossible to replace it on disk. To prevent this, we need to add a suffix with
148            // executable name.
149            let mut suffix = std::env::current_exe()
150                .ok()
151                .and_then(|p| p.file_stem().map(|s| s.to_owned()))
152                .unwrap_or_default();
153            suffix.push(".module");
154            let lib_path = source_lib_path.with_extension(suffix);
155            try_copy_library(&source_lib_path, &lib_path)?;
156
157            let need_reload = Arc::new(AtomicBool::new(false));
158            let need_reload_clone = need_reload.clone();
159            let source_lib_path_clone = source_lib_path.clone();
160
161            let mut watcher =
162                notify::recommended_watcher(move |event: notify::Result<notify::Event>| {
163                    if let Ok(event) = event {
164                        if let EventKind::Modify(_) | EventKind::Create(_) = event.kind {
165                            need_reload_clone.store(true, atomic::Ordering::Relaxed);
166
167                            Log::warn(format!(
168                                "Plugin {} was changed. Performing hot reloading...",
169                                source_lib_path_clone.display()
170                            ))
171                        }
172                    }
173                })
174                .map_err(|e| e.to_string())?;
175
176            watcher
177                .watch(&source_lib_path, RecursiveMode::NonRecursive)
178                .map_err(|e| e.to_string())?;
179
180            Log::info(format!(
181                "Watching for changes in plugin {source_lib_path:?}..."
182            ));
183
184            DyLibDynamicPlugin {
185                state: PluginState::Loaded(DyLibHandle::load(lib_path.as_os_str())?),
186                lib_path,
187                source_lib_path: source_lib_path.clone(),
188                _watcher: Some(watcher),
189                need_reload,
190            }
191        } else {
192            DyLibDynamicPlugin {
193                state: PluginState::Loaded(DyLibHandle::load(source_lib_path.as_os_str())?),
194                lib_path: source_lib_path.clone(),
195                source_lib_path: source_lib_path.clone(),
196                _watcher: None,
197                need_reload: Default::default(),
198            }
199        };
200        Ok(plugin)
201    }
202}
203
204impl DynamicPlugin for DyLibDynamicPlugin {
205    fn as_loaded_ref(&self) -> &dyn Plugin {
206        &*self.state.as_loaded_ref().plugin
207    }
208
209    fn as_loaded_mut(&mut self) -> &mut dyn Plugin {
210        &mut *self.state.as_loaded_mut().plugin
211    }
212
213    fn is_reload_needed_now(&self) -> bool {
214        self.need_reload.load(atomic::Ordering::Relaxed)
215    }
216
217    fn display_name(&self) -> String {
218        format!("{:?}", self.source_lib_path)
219    }
220
221    fn is_loaded(&self) -> bool {
222        matches!(self.state, PluginState::Loaded { .. })
223    }
224
225    fn reload(
226        &mut self,
227        fill_and_register: &mut dyn FnMut(&mut dyn Plugin) -> Result<(), String>,
228    ) -> Result<(), String> {
229        // Unload the plugin.
230        let PluginState::Loaded(_) = &mut self.state else {
231            return Err("cannot unload non-loaded plugin".to_string());
232        };
233
234        self.state = PluginState::Unloaded;
235
236        Log::info(format!(
237            "Plugin {:?} was unloaded successfully!",
238            self.source_lib_path
239        ));
240
241        // Replace the module.
242        try_copy_library(&self.source_lib_path, &self.lib_path)?;
243
244        Log::info(format!(
245            "{:?} plugin's module {} was successfully cloned to {}.",
246            self.source_lib_path,
247            self.source_lib_path.display(),
248            self.lib_path.display()
249        ));
250
251        let mut dynamic = DyLibHandle::load(&self.lib_path)?;
252
253        fill_and_register(dynamic.plugin_mut())?;
254
255        self.state = PluginState::Loaded(dynamic);
256
257        self.need_reload.store(false, atomic::Ordering::Relaxed);
258
259        Log::info(format!(
260            "Plugin {:?} was reloaded successfully!",
261            self.source_lib_path
262        ));
263
264        Ok(())
265    }
266}
267
268/// Actual state of a dynamic plugin.
269enum PluginState {
270    /// Unloaded plugin.
271    Unloaded,
272    /// Loaded plugin.
273    Loaded(DyLibHandle),
274}
275
276impl PluginState {
277    /// Tries to interpret the state as [`Self::Loaded`], panics if the plugin is unloaded.
278    pub fn as_loaded_ref(&self) -> &DyLibHandle {
279        match self {
280            PluginState::Unloaded => {
281                panic!("Cannot obtain a reference to the plugin, because it is unloaded!")
282            }
283            PluginState::Loaded(dynamic) => dynamic,
284        }
285    }
286
287    /// Tries to interpret the state as [`Self::Loaded`], panics if the plugin is unloaded.
288    pub fn as_loaded_mut(&mut self) -> &mut DyLibHandle {
289        match self {
290            PluginState::Unloaded => {
291                panic!("Cannot obtain a reference to the plugin, because it is unloaded!")
292            }
293            PluginState::Loaded(dynamic) => dynamic,
294        }
295    }
296}
297
298fn try_copy_library(source_lib_path: &Path, lib_path: &Path) -> Result<(), String> {
299    if let Err(err) = std::fs::copy(source_lib_path, lib_path) {
300        // The library could already be copied and loaded, thus cannot be replaced. For
301        // example - by the running editor, that also uses hot reloading. Check for matching
302        // content, and if does not match, pass the error further.
303        let mut src_lib_file = File::open(source_lib_path).map_err(|e| e.to_string())?;
304        let mut src_lib_file_content = Vec::new();
305        src_lib_file
306            .read_to_end(&mut src_lib_file_content)
307            .map_err(|e| e.to_string())?;
308        let mut lib_file = File::open(lib_path).map_err(|e| e.to_string())?;
309        let mut lib_file_content = Vec::new();
310        lib_file
311            .read_to_end(&mut lib_file_content)
312            .map_err(|e| e.to_string())?;
313        if src_lib_file_content != lib_file_content {
314            return Err(format!(
315                "Unable to clone the library {} to {}. It is required, because source \
316                        library has {} size, but loaded has {} size and the content does not match. \
317                        Exact reason: {:?}",
318                source_lib_path.display(),
319                lib_path.display(),
320                src_lib_file_content.len(),
321                lib_file_content.len(),
322                err
323            ));
324        }
325    }
326
327    Ok(())
328}