veloren_common_dynlib/
lib.rs

1use libloading::Library;
2use notify::{EventKind, RecursiveMode, Watcher, recommended_watcher};
3use std::{
4    process::{Command, Stdio},
5    sync::{Mutex, mpsc},
6    time::Duration,
7};
8
9use find_folder::Search;
10use std::{
11    env,
12    env::consts::{DLL_PREFIX, DLL_SUFFIX},
13    path::{Path, PathBuf},
14    sync::Arc,
15};
16use tracing::{debug, error, info};
17
18// Re-exports
19pub use libloading::Symbol;
20
21/// LoadedLib holds a loaded dynamic library and the location of library file
22/// with the appropriate OS specific name and extension i.e.
23/// `libvoxygen_anim_dyn_active.dylib`, `voxygen_anim_dyn_active.dll`.
24///
25/// # NOTE
26/// DOES NOT WORK ON MACOS, due to some limitations with hot-reloading the
27/// `.dylib`.
28pub struct LoadedLib {
29    /// Loaded library.
30    pub lib: Library,
31    /// Path to the library.
32    lib_path: PathBuf,
33    /// Reload count, used for naming new library (loader will reuse old library
34    /// if it has the same name).
35    reload_count: u64,
36}
37
38impl LoadedLib {
39    /// Compile and load the dynamic library
40    ///
41    /// This is necessary because the very first time you use hot reloading you
42    /// wont have the library, so you can't load it until you have compiled it!
43    fn compile_load(dyn_package: &str, features: &[&str]) -> Self {
44        let reload_count = 0; // This is the first time loading.
45
46        #[cfg(target_os = "macos")]
47        error!("The hot reloading feature does not work on macos.");
48
49        // Compile
50        if !compile(dyn_package, features) {
51            panic!("{} compile failed.", dyn_package);
52        } else {
53            info!("{} compile succeeded.", dyn_package);
54        }
55
56        copy(
57            &LoadedLib::determine_path(dyn_package, reload_count),
58            dyn_package,
59            reload_count,
60        );
61
62        Self::load(dyn_package, reload_count)
63    }
64
65    /// Load a library from disk.
66    ///
67    /// Currently this is pretty fragile, it gets the path of where it thinks
68    /// the dynamic library should be and tries to load it. It will panic if it
69    /// is missing.
70    fn load(dyn_package: &str, reload_count: u64) -> Self {
71        let lib_path = LoadedLib::determine_path(dyn_package, reload_count);
72
73        // Try to load the library.
74        let lib = match unsafe { Library::new(lib_path.clone()) } {
75            Ok(lib) => lib,
76            Err(e) => panic!(
77                "Tried to load dynamic library from {:?}, but it could not be found. A potential \
78                 reason is we may require a special case for your OS so we can find it. {:?}",
79                lib_path, e
80            ),
81        };
82
83        Self {
84            lib,
85            lib_path,
86            reload_count,
87        }
88    }
89
90    /// Determine the path to the dynamic library based on the path of the
91    /// current executable.
92    fn determine_path(dyn_package: &str, reload_count: u64) -> PathBuf {
93        let current_exe = env::current_exe();
94
95        // If we got the current_exe, we need to go up a level and then down
96        // in to debug (in case we were in release or another build dir).
97        let mut lib_path = match current_exe {
98            Ok(mut path) => {
99                // Remove the filename to get the directory.
100                path.pop();
101
102                // Search for the debug directory.
103                let dir = Search::ParentsThenKids(1, 1)
104                    .of(path)
105                    .for_folder("debug")
106                    .expect(
107                        "Could not find the debug build directory relative to the current \
108                         executable.",
109                    );
110
111                debug!(?dir, "Found the debug build directory.");
112                dir
113            },
114            Err(e) => {
115                panic!(
116                    "Could not determine the path of the current executable, this is needed to \
117                     hot-reload the dynamic library. {:?}",
118                    e
119                );
120            },
121        };
122
123        // Determine the platform specific path and push it onto our already
124        // established target/debug dir.
125        lib_path.push(active_file(dyn_package, reload_count));
126
127        lib_path
128    }
129}
130
131/// Initialise a watcher.
132///
133/// This will search for the directory named `package_source_dir` and watch the
134/// files within it for any changes.
135pub fn init(
136    package: &'static str,
137    package_source_dir: &'static str,
138    features: &'static [&'static str],
139) -> Arc<Mutex<Option<LoadedLib>>> {
140    let lib_storage = Arc::new(Mutex::new(Some(LoadedLib::compile_load(package, features))));
141
142    // TODO: use crossbeam
143    let (reload_send, reload_recv) = mpsc::channel();
144
145    // Start watcher
146    let mut watcher = recommended_watcher(move |res| event_fn(res, &reload_send)).unwrap();
147
148    // Search for the source directory of the package being hot-reloaded.
149    let watch_dir = Search::Kids(1)
150        .for_folder(package_source_dir)
151        .unwrap_or_else(|_| {
152            panic!(
153                "Could not find the {} crate directory relative to the current directory",
154                package_source_dir
155            )
156        });
157
158    watcher.watch(&watch_dir, RecursiveMode::Recursive).unwrap();
159
160    // Start reloader that watcher signals
161    // "Debounces" events since I can't find the option to do this in the latest
162    // `notify`
163    let lib_storage_clone = Arc::clone(&lib_storage);
164    std::thread::Builder::new()
165        .name(format!("{}_hotreload_watcher", package))
166        .spawn(move || {
167            let mut modified_paths = std::collections::HashSet::new();
168            while let Ok(path) = reload_recv.recv() {
169                modified_paths.insert(path);
170                // Wait for any additional modify events before reloading
171                while let Ok(path) = reload_recv.recv_timeout(Duration::from_millis(300)) {
172                    modified_paths.insert(path);
173                }
174
175                info!(
176                    ?modified_paths,
177                    "Hot reloading {} because files in `{}` modified.", package, package_source_dir
178                );
179
180                hotreload(package, &lib_storage_clone, features);
181            }
182        })
183        .unwrap();
184
185    // Let the watcher live forever
186    std::mem::forget(watcher);
187
188    lib_storage
189}
190
191fn compiled_file(dyn_package: &str) -> String { dyn_lib_file(dyn_package, None) }
192
193fn active_file(dyn_package: &str, reload_count: u64) -> String {
194    dyn_lib_file(dyn_package, Some(reload_count))
195}
196
197fn dyn_lib_file(dyn_package: &str, active: Option<u64>) -> String {
198    if let Some(count) = active {
199        format!(
200            "{}{}_active{}{}",
201            DLL_PREFIX,
202            dyn_package.replace('-', "_"),
203            count,
204            DLL_SUFFIX
205        )
206    } else {
207        format!(
208            "{}{}{}",
209            DLL_PREFIX,
210            dyn_package.replace('-', "_"),
211            DLL_SUFFIX
212        )
213    }
214}
215
216/// Event function to hotreload the dynamic library
217///
218/// This is called by the watcher to filter for modify events on `.rs` files
219/// before sending them back.
220fn event_fn(res: notify::Result<notify::Event>, sender: &mpsc::Sender<String>) {
221    match res {
222        Ok(event) => {
223            if let EventKind::Modify(_) = event.kind {
224                event
225                    .paths
226                    .iter()
227                    .filter(|p| p.extension().map(|e| e == "rs").unwrap_or(false))
228                    .map(|p| p.to_string_lossy().into_owned())
229                    // Signal reloader
230                    .for_each(|p| { let _ = sender.send(p); });
231            }
232        },
233        Err(e) => error!(?e, "hotreload watcher error."),
234    }
235}
236
237/// Hotreload the dynamic library
238///
239/// This will reload the dynamic library by first internally calling compile
240/// and then reloading the library.
241fn hotreload(dyn_package: &str, loaded_lib: &Mutex<Option<LoadedLib>>, features: &[&str]) {
242    // Do nothing if recompile failed.
243    if compile(dyn_package, features) {
244        let mut lock = loaded_lib.lock().unwrap();
245
246        // Close lib.
247        let loaded_lib = lock.take().unwrap();
248        loaded_lib.lib.close().unwrap();
249        let new_count = loaded_lib.reload_count + 1;
250        copy(&loaded_lib.lib_path, dyn_package, new_count);
251
252        // Open new lib.
253        *lock = Some(LoadedLib::load(dyn_package, new_count));
254
255        info!("Updated {}.", dyn_package);
256    }
257}
258
259/// Recompile the dyn package
260///
261/// Returns `false` if the compile failed.
262fn compile(dyn_package: &str, features: &[&str]) -> bool {
263    let mut features_arg = format!("{}/be-dyn-lib", dyn_package);
264
265    for feature in features {
266        features_arg.push(',');
267        features_arg.push_str(dyn_package);
268        features_arg.push('/');
269        features_arg.push_str(feature);
270    }
271    let output = Command::new("cargo")
272        .stderr(Stdio::inherit())
273        .stdout(Stdio::inherit())
274        .arg("rustc")
275        .arg("--package")
276        .arg(dyn_package)
277        .arg("--features")
278        .arg(features_arg)
279        .arg("-Z")
280        .arg("unstable-options")
281        .arg("--crate-type")
282        .arg("dylib")
283        .output()
284        .unwrap();
285
286    output.status.success()
287}
288
289/// Copy the lib file, so we have an `_active` copy.
290///
291/// We do this for all OS's although it is only strictly necessary for windows.
292/// The reason we do this is to make the code easier to understand and debug.
293fn copy(lib_path: &Path, dyn_package: &str, reload_count: u64) {
294    // Use the platform specific names.
295    let lib_compiled_path = lib_path.with_file_name(compiled_file(dyn_package));
296    let lib_output_path = lib_path.with_file_name(active_file(dyn_package, reload_count));
297    let old_lib_output_path = reload_count
298        .checked_sub(1)
299        .map(|old_count| lib_path.with_file_name(active_file(dyn_package, old_count)));
300
301    // Get the path to where the lib was compiled to.
302    debug!(?lib_compiled_path, ?lib_output_path, "Moving.");
303
304    // delete old file
305    if let Some(old) = old_lib_output_path {
306        std::fs::remove_file(old).expect("Failed to delete old library");
307    }
308
309    // Copy the library file from where it is output, to where we are going to
310    // load it from i.e. lib_path.
311    std::fs::copy(&lib_compiled_path, &lib_output_path).unwrap_or_else(|err| {
312        panic!(
313            "Failed to rename dynamic library from {:?} to {:?}. {:?}",
314            lib_compiled_path, lib_output_path, err
315        )
316    });
317}