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 {:?}...",
186                source_lib_path
187            ));
188
189            DyLibDynamicPlugin {
190                state: PluginState::Loaded(DyLibHandle::load(lib_path.as_os_str())?),
191                lib_path,
192                source_lib_path: source_lib_path.clone(),
193                _watcher: Some(watcher),
194                need_reload,
195            }
196        } else {
197            DyLibDynamicPlugin {
198                state: PluginState::Loaded(DyLibHandle::load(source_lib_path.as_os_str())?),
199                lib_path: source_lib_path.clone(),
200                source_lib_path: source_lib_path.clone(),
201                _watcher: None,
202                need_reload: Default::default(),
203            }
204        };
205        Ok(plugin)
206    }
207}
208
209impl DynamicPlugin for DyLibDynamicPlugin {
210    fn as_loaded_ref(&self) -> &dyn Plugin {
211        &*self.state.as_loaded_ref().plugin
212    }
213
214    fn as_loaded_mut(&mut self) -> &mut dyn Plugin {
215        &mut *self.state.as_loaded_mut().plugin
216    }
217
218    fn is_reload_needed_now(&self) -> bool {
219        self.need_reload.load(atomic::Ordering::Relaxed)
220    }
221
222    fn display_name(&self) -> String {
223        format!("{:?}", self.source_lib_path)
224    }
225
226    fn is_loaded(&self) -> bool {
227        matches!(self.state, PluginState::Loaded { .. })
228    }
229
230    fn reload(
231        &mut self,
232        fill_and_register: &mut dyn FnMut(&mut dyn Plugin) -> Result<(), String>,
233    ) -> Result<(), String> {
234        // Unload the plugin.
235        let PluginState::Loaded(_) = &mut self.state else {
236            return Err("cannot unload non-loaded plugin".to_string());
237        };
238
239        self.state = PluginState::Unloaded;
240
241        Log::info(format!(
242            "Plugin {:?} was unloaded successfully!",
243            self.source_lib_path
244        ));
245
246        // Replace the module.
247        try_copy_library(&self.source_lib_path, &self.lib_path)?;
248
249        Log::info(format!(
250            "{:?} plugin's module {} was successfully cloned to {}.",
251            self.source_lib_path,
252            self.source_lib_path.display(),
253            self.lib_path.display()
254        ));
255
256        let mut dynamic = DyLibHandle::load(&self.lib_path)?;
257
258        fill_and_register(dynamic.plugin_mut())?;
259
260        self.state = PluginState::Loaded(dynamic);
261
262        self.need_reload.store(false, atomic::Ordering::Relaxed);
263
264        Log::info(format!(
265            "Plugin {:?} was reloaded successfully!",
266            self.source_lib_path
267        ));
268
269        Ok(())
270    }
271}
272
273/// Actual state of a dynamic plugin.
274enum PluginState {
275    /// Unloaded plugin.
276    Unloaded,
277    /// Loaded plugin.
278    Loaded(DyLibHandle),
279}
280
281impl PluginState {
282    /// Tries to interpret the state as [`Self::Loaded`], panics if the plugin is unloaded.
283    pub fn as_loaded_ref(&self) -> &DyLibHandle {
284        match self {
285            PluginState::Unloaded => {
286                panic!("Cannot obtain a reference to the plugin, because it is unloaded!")
287            }
288            PluginState::Loaded(dynamic) => dynamic,
289        }
290    }
291
292    /// Tries to interpret the state as [`Self::Loaded`], panics if the plugin is unloaded.
293    pub fn as_loaded_mut(&mut self) -> &mut DyLibHandle {
294        match self {
295            PluginState::Unloaded => {
296                panic!("Cannot obtain a reference to the plugin, because it is unloaded!")
297            }
298            PluginState::Loaded(dynamic) => dynamic,
299        }
300    }
301}
302
303fn try_copy_library(source_lib_path: &Path, lib_path: &Path) -> Result<(), String> {
304    if let Err(err) = std::fs::copy(source_lib_path, lib_path) {
305        // The library could already be copied and loaded, thus cannot be replaced. For
306        // example - by the running editor, that also uses hot reloading. Check for matching
307        // content, and if does not match, pass the error further.
308        let mut src_lib_file = File::open(source_lib_path).map_err(|e| e.to_string())?;
309        let mut src_lib_file_content = Vec::new();
310        src_lib_file
311            .read_to_end(&mut src_lib_file_content)
312            .map_err(|e| e.to_string())?;
313        let mut lib_file = File::open(lib_path).map_err(|e| e.to_string())?;
314        let mut lib_file_content = Vec::new();
315        lib_file
316            .read_to_end(&mut lib_file_content)
317            .map_err(|e| e.to_string())?;
318        if src_lib_file_content != lib_file_content {
319            return Err(format!(
320                "Unable to clone the library {} to {}. It is required, because source \
321                        library has {} size, but loaded has {} size and the content does not match. \
322                        Exact reason: {:?}",
323                source_lib_path.display(),
324                lib_path.display(),
325                src_lib_file_content.len(),
326                lib_file_content.len(),
327                err
328            ));
329        }
330    }
331
332    Ok(())
333}