Skip to main content

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    pub fn reload_count(&self) -> u64 { self.reload_count }
131}
132
133/// Initialise a watcher.
134///
135/// This will search for the directory named `package_source_dir` and watch the
136/// files within it for any changes.
137pub fn init(
138    package: &'static str,
139    package_source_dir: &'static str,
140    features: &'static [&'static str],
141) -> Arc<Mutex<Option<LoadedLib>>> {
142    let lib_storage = Arc::new(Mutex::new(Some(LoadedLib::compile_load(package, features))));
143
144    // TODO: use crossbeam
145    let (reload_send, reload_recv) = mpsc::channel();
146
147    // Start watcher
148    let mut watcher = recommended_watcher(move |res| event_fn(res, &reload_send)).unwrap();
149
150    // Search for the source directory of the package being hot-reloaded.
151    let watch_dir = Search::Kids(1)
152        .for_folder(package_source_dir)
153        .unwrap_or_else(|_| {
154            panic!(
155                "Could not find the {} crate directory relative to the current directory",
156                package_source_dir
157            )
158        });
159
160    watcher.watch(&watch_dir, RecursiveMode::Recursive).unwrap();
161
162    // Start reloader that watcher signals
163    // "Debounces" events since I can't find the option to do this in the latest
164    // `notify`
165    let lib_storage_clone = Arc::clone(&lib_storage);
166    std::thread::Builder::new()
167        .name(format!("{}_hotreload_watcher", package))
168        .spawn(move || {
169            let mut modified_paths = std::collections::HashSet::new();
170            while let Ok(path) = reload_recv.recv() {
171                modified_paths.insert(path);
172                // Wait for any additional modify events before reloading
173                while let Ok(path) = reload_recv.recv_timeout(Duration::from_millis(300)) {
174                    modified_paths.insert(path);
175                }
176
177                info!(
178                    ?modified_paths,
179                    "Hot reloading {} because files in `{}` modified.", package, package_source_dir
180                );
181
182                hotreload(package, &lib_storage_clone, features);
183            }
184        })
185        .unwrap();
186
187    // Let the watcher live forever
188    std::mem::forget(watcher);
189
190    lib_storage
191}
192
193fn compiled_file(dyn_package: &str) -> String { dyn_lib_file(dyn_package, None) }
194
195fn active_file(dyn_package: &str, reload_count: u64) -> String {
196    dyn_lib_file(dyn_package, Some(reload_count))
197}
198
199fn dyn_lib_file(dyn_package: &str, active: Option<u64>) -> String {
200    if let Some(count) = active {
201        format!(
202            "{}{}_active{}{}",
203            DLL_PREFIX,
204            dyn_package.replace('-', "_"),
205            count,
206            DLL_SUFFIX
207        )
208    } else {
209        format!(
210            "{}{}{}",
211            DLL_PREFIX,
212            dyn_package.replace('-', "_"),
213            DLL_SUFFIX
214        )
215    }
216}
217
218/// Event function to hotreload the dynamic library
219///
220/// This is called by the watcher to filter for modify events on `.rs` files
221/// before sending them back.
222fn event_fn(res: notify::Result<notify::Event>, sender: &mpsc::Sender<String>) {
223    match res {
224        Ok(event) => {
225            if let EventKind::Modify(_) = event.kind {
226                event
227                    .paths
228                    .iter()
229                    .filter(|p| p.extension().map(|e| e == "rs").unwrap_or(false))
230                    .map(|p| p.to_string_lossy().into_owned())
231                    // Signal reloader
232                    .for_each(|p| { let _ = sender.send(p); });
233            }
234        },
235        Err(e) => error!(?e, "hotreload watcher error."),
236    }
237}
238
239/// Hotreload the dynamic library
240///
241/// This will reload the dynamic library by first internally calling compile
242/// and then reloading the library.
243fn hotreload(dyn_package: &str, loaded_lib: &Mutex<Option<LoadedLib>>, features: &[&str]) {
244    // Do nothing if recompile failed.
245    if compile(dyn_package, features) {
246        let mut lock = loaded_lib.lock().unwrap();
247
248        // Close lib.
249        let loaded_lib = lock.take().unwrap();
250        loaded_lib.lib.close().unwrap();
251        let new_count = loaded_lib.reload_count + 1;
252        copy(&loaded_lib.lib_path, dyn_package, new_count);
253
254        // Open new lib.
255        *lock = Some(LoadedLib::load(dyn_package, new_count));
256
257        info!("Updated {}.", dyn_package);
258    }
259}
260
261/// Recompile the dyn package
262///
263/// Returns `false` if the compile failed.
264fn compile(dyn_package: &str, features: &[&str]) -> bool {
265    let mut features_arg = format!("{}/be-dyn-lib", dyn_package);
266
267    for feature in features {
268        features_arg.push(',');
269        features_arg.push_str(dyn_package);
270        features_arg.push('/');
271        features_arg.push_str(feature);
272    }
273    let output = Command::new("cargo")
274        .stderr(Stdio::inherit())
275        .stdout(Stdio::inherit())
276        .arg("rustc")
277        .arg("--package")
278        .arg(dyn_package)
279        .arg("--features")
280        .arg(features_arg)
281        .arg("-Z")
282        .arg("unstable-options")
283        .arg("--crate-type")
284        .arg("dylib")
285        .output()
286        .unwrap();
287
288    output.status.success()
289}
290
291/// Copy the lib file, so we have an `_active` copy.
292///
293/// We do this for all OS's although it is only strictly necessary for windows.
294/// The reason we do this is to make the code easier to understand and debug.
295fn copy(lib_path: &Path, dyn_package: &str, reload_count: u64) {
296    // Use the platform specific names.
297    let lib_compiled_path = lib_path.with_file_name(compiled_file(dyn_package));
298    let lib_output_path = lib_path.with_file_name(active_file(dyn_package, reload_count));
299    let old_lib_output_path = reload_count
300        .checked_sub(1)
301        .map(|old_count| lib_path.with_file_name(active_file(dyn_package, old_count)));
302
303    // Get the path to where the lib was compiled to.
304    debug!(?lib_compiled_path, ?lib_output_path, "Moving.");
305
306    // delete old file
307    if let Some(old) = old_lib_output_path {
308        std::fs::remove_file(old).expect("Failed to delete old library");
309    }
310
311    // Copy the library file from where it is output, to where we are going to
312    // load it from i.e. lib_path.
313    std::fs::copy(&lib_compiled_path, &lib_output_path).unwrap_or_else(|err| {
314        panic!(
315            "Failed to rename dynamic library from {:?} to {:?}. {:?}",
316            lib_compiled_path, lib_output_path, err
317        )
318    });
319}