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