rhai_dylib/module_resolvers/
libloading.rs

1use super::{locked_read, locked_write};
2use crate::loader::libloading::Libloading;
3use crate::loader::Loader;
4
5#[cfg(target_os = "linux")]
6const DYLIB_EXTENSION: &str = "so";
7#[cfg(target_os = "macos")]
8const DYLIB_EXTENSION: &str = "dylib";
9#[cfg(target_os = "windows")]
10const DYLIB_EXTENSION: &str = "dll";
11
12/// A module resolver that load dynamic libraries pointed by the `import` path.
13pub struct DylibModuleResolver {
14    /// Path prepended for each import if specified.
15    base_path: Option<std::path::PathBuf>,
16    /// Dynamic library loader.
17    loader: rhai::Locked<Libloading>,
18    /// Is module caching enabled for this resolver.
19    cache_enabled: bool,
20    /// Cache of loaded modules, empty if [`Self::cache_enabled`] is false.
21    cache: rhai::Locked<std::collections::BTreeMap<std::path::PathBuf, rhai::Shared<rhai::Module>>>,
22}
23
24impl Default for DylibModuleResolver {
25    fn default() -> Self {
26        Self {
27            base_path: None,
28            loader: Libloading::new().into(),
29            cache_enabled: true,
30            cache: rhai::Locked::new(std::collections::BTreeMap::new()),
31        }
32    }
33}
34
35impl DylibModuleResolver {
36    /// Create a new instance of the resolver.
37    #[must_use]
38    pub fn new() -> Self {
39        Self::default()
40    }
41
42    /// Enable/disable the cache.
43    pub fn enable_cache(&mut self, enable: bool) -> &mut Self {
44        self.cache_enabled = enable;
45        self
46    }
47
48    /// Is the cache enabled?
49    #[must_use]
50    pub const fn is_cache_enabled(&self) -> bool {
51        self.cache_enabled
52    }
53
54    /// Create a new [`DylibModuleResolver`] with a specific base path.
55    ///
56    /// # Example
57    ///
58    /// ```ignore
59    /// use rhai::Engine;
60    /// use rhai_dylib::module_resolvers::libloading::DylibModuleResolver;
61    ///
62    /// // Create a new 'DylibModuleResolver' loading dynamic libraries
63    /// // from the 'scripts' directory.
64    /// let resolver = DylibModuleResolver::with_path("./scripts");
65    ///
66    /// let mut engine = Engine::new();
67    /// engine.set_module_resolver(resolver);
68    /// ```
69    #[must_use]
70    pub fn with_path(path: impl Into<std::path::PathBuf>) -> Self {
71        Self {
72            base_path: Some(path.into()),
73            ..Default::default()
74        }
75    }
76
77    /// Construct a full file path.
78    #[must_use]
79    pub fn get_file_path(
80        &self,
81        path: &str,
82        source_path: Option<&std::path::Path>,
83    ) -> std::path::PathBuf {
84        let path = std::path::Path::new(path);
85
86        let mut file_path;
87
88        if path.is_relative() {
89            file_path = self
90                .base_path
91                .clone()
92                .or_else(|| source_path.map(Into::into))
93                .unwrap_or_default();
94            file_path.push(path);
95        } else {
96            file_path = path.into();
97        }
98
99        file_path.set_extension(DYLIB_EXTENSION);
100
101        file_path
102    }
103
104    /// Resolve a module based on a path.
105    #[allow(clippy::needless_pass_by_value)]
106    fn impl_resolve(
107        &self,
108        global: Option<&mut rhai::GlobalRuntimeState>,
109        source: Option<&str>,
110        path: &str,
111        position: rhai::Position,
112    ) -> Result<rhai::Shared<rhai::Module>, Box<rhai::EvalAltResult>> {
113        // Load relative paths from source if there is no base path specified
114        let source_path = global
115            .as_ref()
116            .and_then(|g| g.source())
117            .or(source)
118            .and_then(|p| std::path::Path::new(p).parent());
119
120        let path = self.get_file_path(path, source_path);
121
122        if !path.exists() {
123            return Err(Box::new(rhai::EvalAltResult::ErrorModuleNotFound(
124                path.to_str()
125                    .map_or_else(String::default, std::string::ToString::to_string),
126                position,
127            )));
128        }
129
130        if self.is_cache_enabled() {
131            let module = { locked_read(&self.cache).get(&path).cloned() };
132
133            if let Some(module) = module {
134                Ok(module)
135            } else {
136                let module = locked_write(&self.loader).load(path.as_path())?;
137                locked_write(&self.cache).insert(path, module.clone());
138
139                Ok(module)
140            }
141        } else {
142            locked_write(&self.loader).load(path.as_path())
143        }
144    }
145}
146
147impl rhai::ModuleResolver for DylibModuleResolver {
148    fn resolve(
149        &self,
150        _: &rhai::Engine,
151        source: Option<&str>,
152        path: &str,
153        position: rhai::Position,
154    ) -> Result<rhai::Shared<rhai::Module>, Box<rhai::EvalAltResult>> {
155        self.impl_resolve(None, source, path, position)
156    }
157
158    fn resolve_raw(
159        &self,
160        _: &rhai::Engine,
161        global: &mut rhai::GlobalRuntimeState,
162        _: &mut rhai::Scope,
163        path: &str,
164        position: rhai::Position,
165    ) -> Result<rhai::Shared<rhai::Module>, Box<rhai::EvalAltResult>> {
166        self.impl_resolve(Some(global), None, path, position)
167    }
168
169    /// This resolver is Rust based, so it cannot resolve ASTs.
170    /// This function will always return `None`.
171    fn resolve_ast(
172        &self,
173        _: &rhai::Engine,
174        _: Option<&str>,
175        _: &str,
176        _: rhai::Position,
177    ) -> Option<Result<rhai::AST, Box<rhai::EvalAltResult>>> {
178        None
179    }
180}
181
182#[cfg(test)]
183mod tests {
184    use super::*;
185
186    #[test]
187    fn new() {
188        let mut r = DylibModuleResolver::new();
189        let rp = DylibModuleResolver::with_path("./scripts");
190
191        r.enable_cache(false);
192        assert!(!r.is_cache_enabled());
193        assert!(rp.is_cache_enabled());
194    }
195
196    #[test]
197    fn file_path_resolution() {
198        let r = DylibModuleResolver::new();
199
200        let relative = r.get_file_path("mylib", None);
201
202        #[cfg(target_os = "linux")]
203        assert_eq!(relative, std::path::PathBuf::from("mylib.so"));
204        #[cfg(target_os = "windows")]
205        assert_eq!(relative, std::path::PathBuf::from("mylib.dll"));
206        #[cfg(target_os = "macos")]
207        assert_eq!(relative, std::path::PathBuf::from("mylib.dylib"));
208
209        let source = r.get_file_path("mylib", Some(std::path::Path::new("source")));
210
211        #[cfg(target_os = "linux")]
212        assert_eq!(source, std::path::PathBuf::from("source/mylib.so"));
213        #[cfg(target_os = "windows")]
214        assert_eq!(source, std::path::PathBuf::from("source/mylib.dll"));
215        #[cfg(target_os = "macos")]
216        assert_eq!(source, std::path::PathBuf::from("source/mylib.dylib"));
217    }
218
219    #[test]
220    fn file_path_resolution_with_path() {
221        let rp = DylibModuleResolver::with_path("scripts");
222
223        let relative = rp.get_file_path("mylib", None);
224        #[cfg(target_os = "linux")]
225        assert_eq!(relative, std::path::PathBuf::from("scripts/mylib.so"));
226        #[cfg(target_os = "windows")]
227        assert_eq!(relative, std::path::PathBuf::from("scripts/mylib.dll"));
228        #[cfg(target_os = "macos")]
229        assert_eq!(relative, std::path::PathBuf::from("scripts/mylib.dylib"));
230
231        // TODO: add tests for all platforms.
232        let absolute = rp.get_file_path("/usr/local/lib/mylib", None);
233        #[cfg(target_os = "linux")]
234        assert_eq!(
235            absolute,
236            std::path::PathBuf::from("/usr/local/lib/mylib.so")
237        );
238    }
239}