New FileModuleResolver.

This commit is contained in:
Stephen Chung 2020-09-24 22:50:28 +08:00
parent b8aeaa84de
commit c4ec93080e
7 changed files with 352 additions and 110 deletions

View File

@ -26,7 +26,8 @@ New features
* Scripted functions are allowed in packages. * Scripted functions are allowed in packages.
* `parse_int` and `parse_float` functions. * `parse_int` and `parse_float` functions.
* `AST::iter_functions` and `Module::iter_script_fn_info` to iterate functions. * `AST::iter_functions` and `Module::iter_script_fn_info` to iterate functions.
* Functions iteration functions now take `FnMut` instead of `Fn`.
* New `FileModuleResolver` that encapsulates the entire `AST` of the module script, allowing function cross-calling. The old version is renamed `MergingFileModuleResolver`.
Version 0.18.3 Version 0.18.3
============== ==============

View File

@ -10,7 +10,7 @@ Each Function is a Separate Compilation Unit
This means that individual functions can be separated, exported, re-grouped, imported, This means that individual functions can be separated, exported, re-grouped, imported,
and generally mix-'n-match-ed with other completely unrelated scripts. and generally mix-'n-match-ed with other completely unrelated scripts.
For example, the `AST::merge` method allows merging all functions in one [`AST`] into another, For example, the `AST::merge` method allows Global all functions in one [`AST`] into another,
forming a new, combined, group of functions. forming a new, combined, group of functions.
In general, there are two types of _namespaces_ where functions are looked up: In general, there are two types of _namespaces_ where functions are looked up:
@ -43,10 +43,10 @@ This aspect is very similar to JavaScript before ES6 modules.
// Compile a script into AST // Compile a script into AST
let ast1 = engine.compile( let ast1 = engine.compile(
r#" r#"
fn message() { "Hello!" } // greeting message fn get_message() { "Hello!" } // greeting message
fn say_hello() { fn say_hello() {
print(message()); // prints message print(get_message()); // prints message
} }
say_hello(); say_hello();
@ -54,7 +54,7 @@ let ast1 = engine.compile(
)?; )?;
// Compile another script with an overriding function // Compile another script with an overriding function
let ast2 = engine.compile(r#"fn message() { "Boo!" }"#)?; let ast2 = engine.compile(r#"fn get_message() { "Boo!" }"#)?;
// Merge the two AST's // Merge the two AST's
let ast = ast1.merge(ast2); // 'message' will be overwritten let ast = ast1.merge(ast2); // 'message' will be overwritten
@ -73,7 +73,7 @@ i.e. define the function in a separate module and then [`import`] it:
| message.rhai | | message.rhai |
---------------- ----------------
fn message() { "Hello!" } fn get_message() { "Hello!" }
--------------- ---------------
@ -82,40 +82,52 @@ fn message() { "Hello!" }
fn say_hello() { fn say_hello() {
import "message" as msg; import "message" as msg;
print(msg::message()); print(msg::get_message());
} }
say_hello(); say_hello();
``` ```
Module Namespaces Namespace Consideration When Not Using `FileModuleResolver`
----------------- ---------------------------------------------------------
[Modules] can be dynamically loaded into a Rhai script using the [`import`] keyword. [Modules] can be dynamically loaded into a Rhai script using the [`import`] keyword.
When that happens, functions defined within the [module] can be called with a _qualified_ name. When that happens, functions defined within the [module] can be called with a _qualified_ name.
There is a catch, though, if functions in a module script refer to global functions The [`FileModuleResolver`][module resolver] encapsulates the namespace inside the module itself,
defined _within the script_. When called later, those functions will be searched in the so everything works as expected. A function defined in the module script cannot access functions
current global namespace and may not be found. defined in the calling script, but it can freely call functions defined within the same module script.
There is a catch, though. When using anything other than the [`FileModuleResolver`][module resolver],
functions in a module script refer to functions defined in the _global namespace_.
When called later, those functions may not be found.
When using the [`GlobalFileModuleResolver`][module resolver], for example:
```rust ```rust
Using GlobalFileModuleResolver
==============================
----------------- -----------------
| greeting.rhai | | greeting.rhai |
----------------- -----------------
fn message() { "Hello!" }; fn get_message() { "Hello!" };
fn say_hello() { print(message()); } fn say_hello() {
print(get_message()); // 'get_message' is looked up in the global namespace
say_hello(); // 'message' is looked up in the global namespace // when exported
}
say_hello(); // Here, 'get_message' is found in the module namespace
--------------- ---------------
| script.rhai | | script.rhai |
--------------- ---------------
import "greeting" as g; import "greeting" as g;
g::say_hello(); // <- error: function not found - 'message' g::say_hello(); // <- error: function not found - 'get_message'
// because it does not exist in the global namespace
``` ```
In the example above, although the module `greeting.rhai` loads fine (`"Hello!"` is printed), In the example above, although the module `greeting.rhai` loads fine (`"Hello!"` is printed),
@ -123,22 +135,23 @@ the subsequent call using the _namespace-qualified_ function name fails to find
'`message`' which now essentially becomes `g::message`. The call fails as there is no more '`message`' which now essentially becomes `g::message`. The call fails as there is no more
function named '`message`' in the global namespace. function named '`message`' in the global namespace.
Therefore, when writing functions for a [module], make sure that those functions are as _pure_ Therefore, when writing functions for a [module] intended for the [`GlobalFileModuleResolver`][module resolver],
as possible and avoid cross-calling them from each other. A [function pointer] is a valid technique make sure that those functions are as _pure_ as possible and avoid cross-calling them from each other.
to call another function within a module-defined function:
A [function pointer] is a valid technique to call another function in an environment-independent manner:
```rust ```rust
----------------- -----------------
| greeting.rhai | | greeting.rhai |
----------------- -----------------
fn message() { "Hello!" }; fn get_message() { "Hello!" };
fn say_hello(msg_func) { // 'msg_func' is a function pointer fn say_hello(msg_func) { // 'msg_func' is a function pointer
print(msg_func.call()); // call via the function pointer print(msg_func.call()); // call via the function pointer
} }
say_hello(); // 'message' is looked up in the global namespace say_hello(Fn("get_message"));
--------------- ---------------
@ -149,7 +162,7 @@ import "greeting" as g;
fn my_msg() { fn my_msg() {
import "greeting" as g; // <- must import again here... import "greeting" as g; // <- must import again here...
g::message() // <- ... otherwise will not find module 'g' g::get_message() // <- ... otherwise will not find module 'g'
} }
g::say_hello(Fn("my_msg")); // prints 'Hello!' g::say_hello(Fn("my_msg")); // prints 'Hello!'

View File

@ -16,11 +16,12 @@ which simply loads a script file based on the path (with `.rhai` extension attac
Built-in module resolvers are grouped under the `rhai::module_resolvers` module namespace. Built-in module resolvers are grouped under the `rhai::module_resolvers` module namespace.
| Module Resolver | Description | | Module Resolver | Description | Namespace |
| --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | --------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :-------------------------------------: |
| `FileModuleResolver` | The default module resolution service, not available under [`no_std`] or [WASM] builds. Loads a script file (based off the current directory) with `.rhai` extension.<br/>The base directory can be changed via the `FileModuleResolver::new_with_path()` constructor function.<br/>`FileModuleResolver::create_module()` loads a script file and returns a module. | | `FileModuleResolver` | The default module resolution service, not available under [`no_std`] or [WASM] builds. Loads a script file (based off the current directory) with `.rhai` extension.<br/>The base directory can be changed via the `FileModuleResolver::new_with_path()` constructor function.<br/>`FileModuleResolver::create_module()` loads a script file and returns a module. | Module (cannot access global namespace) |
| `StaticModuleResolver` | Loads modules that are statically added. This can be used under [`no_std`]. | | `GlobalFileModuleResolver` | A simpler but more efficient version of `FileModuleResolver`, intended for short utility modules. Not available under [`no_std`] or [WASM] builds. Loads a script file (based off the current directory) with `.rhai` extension.<br/>**Note:** All functions are assumed absolutely _pure_ and cannot cross-call each other.<br/>The base directory can be changed via the `FileModuleResolver::new_with_path()` constructor function.<br/>`FileModuleResolver::create_module()` loads a script file and returns a module. | Global |
| `ModuleResolversCollection` | A collection of module resolvers. Modules will be resolved from each resolver in sequential order.<br/>This is useful when multiple types of modules are needed simultaneously. | | `StaticModuleResolver` | Loads modules that are statically added. This can be used under [`no_std`]. | Global |
| `ModuleResolversCollection` | A collection of module resolvers. Modules will be resolved from each resolver in sequential order.<br/>This is useful when multiple types of modules are needed simultaneously. | Global |
Set into `Engine` Set into `Engine`

View File

@ -1502,7 +1502,9 @@ impl Engine {
args: A, args: A,
) -> Result<T, Box<EvalAltResult>> { ) -> Result<T, Box<EvalAltResult>> {
let mut arg_values = args.into_vec(); let mut arg_values = args.into_vec();
let result = self.call_fn_dynamic_raw(scope, ast, name, &mut None, arg_values.as_mut())?; let mut args: StaticVec<_> = arg_values.as_mut().iter_mut().collect();
let result = self.call_fn_dynamic_raw(scope, ast, name, &mut None, args.as_mut())?;
let typ = self.map_type_name(result.type_name()); let typ = self.map_type_name(result.type_name());
@ -1574,7 +1576,9 @@ impl Engine {
mut this_ptr: Option<&mut Dynamic>, mut this_ptr: Option<&mut Dynamic>,
mut arg_values: impl AsMut<[Dynamic]>, mut arg_values: impl AsMut<[Dynamic]>,
) -> FuncReturn<Dynamic> { ) -> FuncReturn<Dynamic> {
self.call_fn_dynamic_raw(scope, lib, name, &mut this_ptr, arg_values.as_mut()) let mut args: StaticVec<_> = arg_values.as_mut().iter_mut().collect();
self.call_fn_dynamic_raw(scope, lib, name, &mut this_ptr, args.as_mut())
} }
/// Call a script function defined in an `AST` with multiple `Dynamic` arguments. /// Call a script function defined in an `AST` with multiple `Dynamic` arguments.
@ -1592,16 +1596,14 @@ impl Engine {
lib: impl AsRef<Module>, lib: impl AsRef<Module>,
name: &str, name: &str,
this_ptr: &mut Option<&mut Dynamic>, this_ptr: &mut Option<&mut Dynamic>,
arg_values: &mut [Dynamic], args: &mut [&mut Dynamic],
) -> FuncReturn<Dynamic> { ) -> FuncReturn<Dynamic> {
let lib = lib.as_ref(); let lib = lib.as_ref();
let mut args: StaticVec<_> = arg_values.iter_mut().collect();
let fn_def = get_script_function_by_signature(lib, name, args.len(), true) let fn_def = get_script_function_by_signature(lib, name, args.len(), true)
.ok_or_else(|| EvalAltResult::ErrorFunctionNotFound(name.into(), Position::none()))?; .ok_or_else(|| EvalAltResult::ErrorFunctionNotFound(name.into(), Position::none()))?;
let mut state = State::new(); let mut state = State::new();
let mut mods = Imports::new(); let mut mods = Imports::new();
let args = args.as_mut();
// Check for data race. // Check for data race.
if cfg!(not(feature = "no_closure")) { if cfg!(not(feature = "no_closure")) {
@ -1634,8 +1636,8 @@ impl Engine {
let lib = if cfg!(not(feature = "no_function")) { let lib = if cfg!(not(feature = "no_function")) {
ast.lib() ast.lib()
.iter_fn() .iter_fn()
.filter(|(_, _, _, f)| f.is_script()) .filter(|(_, _, _, _, f)| f.is_script())
.map(|(_, _, _, f)| f.get_fn_def().clone()) .map(|(_, _, _, _, f)| f.get_fn_def().clone())
.collect() .collect()
} else { } else {
Default::default() Default::default()

View File

@ -67,7 +67,11 @@ pub struct Module {
all_variables: HashMap<u64, Dynamic, StraightHasherBuilder>, all_variables: HashMap<u64, Dynamic, StraightHasherBuilder>,
/// External Rust functions. /// External Rust functions.
functions: HashMap<u64, (String, FnAccess, StaticVec<TypeId>, Func), StraightHasherBuilder>, functions: HashMap<
u64,
(String, FnAccess, usize, Option<StaticVec<TypeId>>, Func),
StraightHasherBuilder,
>,
/// Iterator functions, keyed by the type producing the iterator. /// Iterator functions, keyed by the type producing the iterator.
type_iterators: HashMap<TypeId, IteratorFn>, type_iterators: HashMap<TypeId, IteratorFn>,
@ -97,7 +101,7 @@ impl fmt::Debug for Module {
.join(", "), .join(", "),
self.functions self.functions
.values() .values()
.map(|(_, _, _, f)| f.to_string()) .map(|(_, _, _, _, f)| f.to_string())
.collect::<Vec<_>>() .collect::<Vec<_>>()
.join(", "), .join(", "),
) )
@ -258,20 +262,22 @@ impl Module {
/// Set a script-defined function into the module. /// Set a script-defined function into the module.
/// ///
/// If there is an existing function of the same name and number of arguments, it is replaced. /// If there is an existing function of the same name and number of arguments, it is replaced.
pub(crate) fn set_script_fn(&mut self, fn_def: ScriptFnDef) -> &mut Self { pub(crate) fn set_script_fn(&mut self, fn_def: ScriptFnDef) -> u64 {
// None + function name + number of arguments. // None + function name + number of arguments.
let hash_script = calc_fn_hash(empty(), &fn_def.name, fn_def.params.len(), empty()); let num_params = fn_def.params.len();
let hash_script = calc_fn_hash(empty(), &fn_def.name, num_params, empty());
self.functions.insert( self.functions.insert(
hash_script, hash_script,
( (
fn_def.name.to_string(), fn_def.name.to_string(),
fn_def.access, fn_def.access,
Default::default(), num_params,
None,
fn_def.into(), fn_def.into(),
), ),
); );
self.indexed = false; self.indexed = false;
self hash_script
} }
/// Does a sub-module exist in the module? /// Does a sub-module exist in the module?
@ -362,7 +368,7 @@ impl Module {
} else if public_only { } else if public_only {
self.functions self.functions
.get(&hash_fn) .get(&hash_fn)
.map(|(_, access, _, _)| match access { .map(|(_, access, _, _, _)| match access {
FnAccess::Public => true, FnAccess::Public => true,
FnAccess::Private => false, FnAccess::Private => false,
}) })
@ -412,7 +418,7 @@ impl Module {
let hash_fn = calc_fn_hash(empty(), &name, args_len, arg_types.iter().cloned()); let hash_fn = calc_fn_hash(empty(), &name, args_len, arg_types.iter().cloned());
self.functions self.functions
.insert(hash_fn, (name, access, params, func.into())); .insert(hash_fn, (name, access, args_len, Some(params), func.into()));
self.indexed = false; self.indexed = false;
@ -487,6 +493,31 @@ impl Module {
self.set_fn(name, Public, arg_types, Func::from_method(Box::new(f))) self.set_fn(name, Public, arg_types, Func::from_method(Box::new(f)))
} }
/// Set a raw function but with a signature that is a scripted function, but the implementation is in Rust.
pub(crate) fn set_raw_fn_as_scripted(
&mut self,
name: impl Into<String>,
num_args: usize,
func: impl Fn(&Engine, &Module, &mut [&mut Dynamic]) -> FuncReturn<Dynamic> + SendSync + 'static,
) -> u64 {
// None + function name + number of arguments.
let name = name.into();
let hash_script = calc_fn_hash(empty(), &name, num_args, empty());
let f = move |engine: &Engine, lib: &Module, args: &mut FnCallArgs| func(engine, lib, args);
self.functions.insert(
hash_script,
(
name,
FnAccess::Public,
num_args,
None,
Func::from_pure(Box::new(f)),
),
);
self.indexed = false;
hash_script
}
/// Set a Rust function taking no parameters into the module, returning a hash key. /// Set a Rust function taking no parameters into the module, returning a hash key.
/// ///
/// If there is a similar existing Rust function, it is replaced. /// If there is a similar existing Rust function, it is replaced.
@ -979,7 +1010,7 @@ impl Module {
} else { } else {
self.functions self.functions
.get(&hash_fn) .get(&hash_fn)
.and_then(|(_, access, _, f)| match access { .and_then(|(_, access, _, _, f)| match access {
_ if !public_only => Some(f), _ if !public_only => Some(f),
FnAccess::Public => Some(f), FnAccess::Public => Some(f),
FnAccess::Private => None, FnAccess::Private => None,
@ -1028,14 +1059,14 @@ impl Module {
/// Merge another module into this module. /// Merge another module into this module.
pub fn merge(&mut self, other: &Self) -> &mut Self { pub fn merge(&mut self, other: &Self) -> &mut Self {
self.merge_filtered(other, &|_, _, _| true) self.merge_filtered(other, &mut |_, _, _| true)
} }
/// Merge another module into this module, with only selected script-defined functions based on a filter predicate. /// Merge another module into this module, with only selected script-defined functions based on a filter predicate.
pub(crate) fn merge_filtered( pub(crate) fn merge_filtered(
&mut self, &mut self,
other: &Self, other: &Self,
_filter: &impl Fn(FnAccess, &str, usize) -> bool, mut _filter: &mut impl FnMut(FnAccess, &str, usize) -> bool,
) -> &mut Self { ) -> &mut Self {
#[cfg(not(feature = "no_function"))] #[cfg(not(feature = "no_function"))]
for (k, v) in &other.modules { for (k, v) in &other.modules {
@ -1055,7 +1086,7 @@ impl Module {
other other
.functions .functions
.iter() .iter()
.filter(|(_, (_, _, _, v))| match v { .filter(|(_, (_, _, _, _, v))| match v {
#[cfg(not(feature = "no_function"))] #[cfg(not(feature = "no_function"))]
Func::Script(ref f) => _filter(f.access, f.name.as_str(), f.params.len()), Func::Script(ref f) => _filter(f.access, f.name.as_str(), f.params.len()),
_ => true, _ => true,
@ -1076,9 +1107,9 @@ impl Module {
#[cfg(not(feature = "no_function"))] #[cfg(not(feature = "no_function"))]
pub(crate) fn retain_functions( pub(crate) fn retain_functions(
&mut self, &mut self,
filter: impl Fn(FnAccess, &str, usize) -> bool, mut filter: impl FnMut(FnAccess, &str, usize) -> bool,
) -> &mut Self { ) -> &mut Self {
self.functions.retain(|_, (_, _, _, v)| match v { self.functions.retain(|_, (_, _, _, _, v)| match v {
Func::Script(ref f) => filter(f.access, f.name.as_str(), f.params.len()), Func::Script(ref f) => filter(f.access, f.name.as_str(), f.params.len()),
_ => true, _ => true,
}); });
@ -1110,7 +1141,7 @@ impl Module {
/// Get an iterator to the functions in the module. /// Get an iterator to the functions in the module.
pub(crate) fn iter_fn( pub(crate) fn iter_fn(
&self, &self,
) -> impl Iterator<Item = &(String, FnAccess, StaticVec<TypeId>, Func)> { ) -> impl Iterator<Item = &(String, FnAccess, usize, Option<StaticVec<TypeId>>, Func)> {
self.functions.values() self.functions.values()
} }
@ -1119,17 +1150,19 @@ impl Module {
pub fn iter_script_fn<'a>(&'a self) -> impl Iterator<Item = Shared<ScriptFnDef>> + 'a { pub fn iter_script_fn<'a>(&'a self) -> impl Iterator<Item = Shared<ScriptFnDef>> + 'a {
self.functions self.functions
.values() .values()
.map(|(_, _, _, f)| f) .map(|(_, _, _, _, f)| f)
.filter(|f| f.is_script()) .filter(|f| f.is_script())
.map(|f| f.get_shared_fn_def()) .map(|f| f.get_shared_fn_def())
} }
#[cfg(not(feature = "no_function"))] #[cfg(not(feature = "no_function"))]
pub fn iter_script_fn_info(&self, action: impl Fn(FnAccess, &str, usize)) { pub fn iter_script_fn_info(&self, mut action: impl FnMut(FnAccess, &str, usize)) {
self.functions.iter().for_each(|(_, (_, _, _, v))| match v { self.functions
Func::Script(ref f) => action(f.access, f.name.as_str(), f.params.len()), .iter()
_ => (), .for_each(|(_, (_, _, _, _, v))| match v {
}); Func::Script(ref f) => action(f.access, f.name.as_str(), f.params.len()),
_ => (),
});
} }
/// Create a new `Module` by evaluating an `AST`. /// Create a new `Module` by evaluating an `AST`.
@ -1202,7 +1235,7 @@ impl Module {
variables.push((hash_var, value.clone())); variables.push((hash_var, value.clone()));
} }
// Index all Rust functions // Index all Rust functions
for (name, access, params, func) in module.functions.values() { for (&hash, (name, access, num_args, params, func)) in module.functions.iter() {
match access { match access {
// Private functions are not exported // Private functions are not exported
FnAccess::Private => continue, FnAccess::Private => continue,
@ -1210,31 +1243,31 @@ impl Module {
} }
#[cfg(not(feature = "no_function"))] #[cfg(not(feature = "no_function"))]
if func.is_script() { if params.is_none() {
let fn_def = func.get_shared_fn_def(); let hash_qualified_script = if qualifiers.is_empty() {
// Qualifiers + function name + number of arguments. hash
let hash_qualified_script = calc_fn_hash( } else {
qualifiers.iter().map(|&v| v), // Qualifiers + function name + number of arguments.
&fn_def.name, calc_fn_hash(qualifiers.iter().map(|&v| v), &name, *num_args, empty())
fn_def.params.len(), };
empty(), functions.push((hash_qualified_script, func.clone()));
);
functions.push((hash_qualified_script, fn_def.into()));
continue; continue;
} }
// Qualified Rust functions are indexed in two steps: if let Some(params) = params {
// 1) Calculate a hash in a similar manner to script-defined functions, // Qualified Rust functions are indexed in two steps:
// i.e. qualifiers + function name + number of arguments. // 1) Calculate a hash in a similar manner to script-defined functions,
let hash_qualified_script = // i.e. qualifiers + function name + number of arguments.
calc_fn_hash(qualifiers.iter().map(|&v| v), name, params.len(), empty()); let hash_qualified_script =
// 2) Calculate a second hash with no qualifiers, empty function name, calc_fn_hash(qualifiers.iter().map(|&v| v), name, params.len(), empty());
// zero number of arguments, and the actual list of argument `TypeId`'.s // 2) Calculate a second hash with no qualifiers, empty function name,
let hash_fn_args = calc_fn_hash(empty(), "", 0, params.iter().cloned()); // zero number of arguments, and the actual list of argument `TypeId`'.s
// 3) The final hash is the XOR of the two hashes. let hash_fn_args = calc_fn_hash(empty(), "", 0, params.iter().cloned());
let hash_qualified_fn = hash_qualified_script ^ hash_fn_args; // 3) The final hash is the XOR of the two hashes.
let hash_qualified_fn = hash_qualified_script ^ hash_fn_args;
functions.push((hash_qualified_fn, func.clone())); functions.push((hash_qualified_fn, func.clone()));
}
} }
} }
@ -1349,7 +1382,7 @@ pub mod resolvers {
pub use super::collection::ModuleResolversCollection; pub use super::collection::ModuleResolversCollection;
#[cfg(not(feature = "no_std"))] #[cfg(not(feature = "no_std"))]
#[cfg(not(target_arch = "wasm32"))] #[cfg(not(target_arch = "wasm32"))]
pub use super::file::FileModuleResolver; pub use super::file::{FileModuleResolver, GlobalFileModuleResolver};
pub use super::stat::StaticModuleResolver; pub use super::stat::StaticModuleResolver;
} }
#[cfg(feature = "no_module")] #[cfg(feature = "no_module")]
@ -1363,6 +1396,181 @@ mod file {
use super::*; use super::*;
use crate::stdlib::path::PathBuf; use crate::stdlib::path::PathBuf;
/// Module resolution service that loads module script files from the file system.
///
/// All functions in each module are treated as strictly _pure_ and cannot refer to
/// other functions within the same module. Functions are searched in the _global_ namespace.
///
/// For simple utility libraries, this usually performs better than the full `FileModuleResolver`.
///
/// Script files are cached so they are are not reloaded and recompiled in subsequent requests.
///
/// The `new_with_path` and `new_with_path_and_extension` constructor functions
/// allow specification of a base directory with module path used as a relative path offset
/// to the base directory. The script file is then forced to be in a specified extension
/// (default `.rhai`).
///
/// # Examples
///
/// ```
/// use rhai::Engine;
/// use rhai::module_resolvers::GlobalFileModuleResolver;
///
/// // Create a new 'GlobalFileModuleResolver' loading scripts from the 'scripts' subdirectory
/// // with file extension '.x'.
/// let resolver = GlobalFileModuleResolver::new_with_path_and_extension("./scripts", "x");
///
/// let mut engine = Engine::new();
/// engine.set_module_resolver(Some(resolver));
/// ```
#[derive(Debug)]
pub struct GlobalFileModuleResolver {
path: PathBuf,
extension: String,
#[cfg(not(feature = "sync"))]
cache: RefCell<HashMap<PathBuf, AST>>,
#[cfg(feature = "sync")]
cache: RwLock<HashMap<PathBuf, AST>>,
}
impl Default for GlobalFileModuleResolver {
fn default() -> Self {
Self::new_with_path(PathBuf::default())
}
}
impl GlobalFileModuleResolver {
/// Create a new `GlobalFileModuleResolver` with a specific base path.
///
/// # Examples
///
/// ```
/// use rhai::Engine;
/// use rhai::module_resolvers::GlobalFileModuleResolver;
///
/// // Create a new 'GlobalFileModuleResolver' loading scripts from the 'scripts' subdirectory
/// // with file extension '.rhai' (the default).
/// let resolver = GlobalFileModuleResolver::new_with_path("./scripts");
///
/// let mut engine = Engine::new();
/// engine.set_module_resolver(Some(resolver));
/// ```
pub fn new_with_path<P: Into<PathBuf>>(path: P) -> Self {
Self::new_with_path_and_extension(path, "rhai")
}
/// Create a new `GlobalFileModuleResolver` with a specific base path and file extension.
///
/// The default extension is `.rhai`.
///
/// # Examples
///
/// ```
/// use rhai::Engine;
/// use rhai::module_resolvers::GlobalFileModuleResolver;
///
/// // Create a new 'GlobalFileModuleResolver' loading scripts from the 'scripts' subdirectory
/// // with file extension '.x'.
/// let resolver = GlobalFileModuleResolver::new_with_path_and_extension("./scripts", "x");
///
/// let mut engine = Engine::new();
/// engine.set_module_resolver(Some(resolver));
/// ```
pub fn new_with_path_and_extension<P: Into<PathBuf>, E: Into<String>>(
path: P,
extension: E,
) -> Self {
Self {
path: path.into(),
extension: extension.into(),
cache: Default::default(),
}
}
/// Create a new `GlobalFileModuleResolver` with the current directory as base path.
///
/// # Examples
///
/// ```
/// use rhai::Engine;
/// use rhai::module_resolvers::GlobalFileModuleResolver;
///
/// // Create a new 'GlobalFileModuleResolver' loading scripts from the current directory
/// // with file extension '.rhai' (the default).
/// let resolver = GlobalFileModuleResolver::new();
///
/// let mut engine = Engine::new();
/// engine.set_module_resolver(Some(resolver));
/// ```
pub fn new() -> Self {
Default::default()
}
/// Create a `Module` from a file path.
pub fn create_module<P: Into<PathBuf>>(
&self,
engine: &Engine,
path: &str,
) -> Result<Module, Box<EvalAltResult>> {
self.resolve(engine, path, Default::default())
}
}
impl ModuleResolver for GlobalFileModuleResolver {
fn resolve(
&self,
engine: &Engine,
path: &str,
pos: Position,
) -> Result<Module, Box<EvalAltResult>> {
// Construct the script file path
let mut file_path = self.path.clone();
file_path.push(path);
file_path.set_extension(&self.extension); // Force extension
let scope = Default::default();
// See if it is cached
let (module, ast) = {
#[cfg(not(feature = "sync"))]
let c = self.cache.borrow();
#[cfg(feature = "sync")]
let c = self.cache.read().unwrap();
if let Some(ast) = c.get(&file_path) {
(
Module::eval_ast_as_new(scope, ast, engine)
.map_err(|err| err.new_position(pos))?,
None,
)
} else {
// Load the file and compile it if not found
let ast = engine
.compile_file(file_path.clone())
.map_err(|err| err.new_position(pos))?;
(
Module::eval_ast_as_new(scope, &ast, engine)
.map_err(|err| err.new_position(pos))?,
Some(ast),
)
}
};
if let Some(ast) = ast {
// Put it into the cache
#[cfg(not(feature = "sync"))]
self.cache.borrow_mut().insert(file_path, ast);
#[cfg(feature = "sync")]
self.cache.write().unwrap().insert(file_path, ast);
}
Ok(module)
}
}
/// Module resolution service that loads module script files from the file system. /// Module resolution service that loads module script files from the file system.
/// ///
/// Script files are cached so they are are not reloaded and recompiled in subsequent requests. /// Script files are cached so they are are not reloaded and recompiled in subsequent requests.
@ -1492,43 +1700,60 @@ mod file {
file_path.push(path); file_path.push(path);
file_path.set_extension(&self.extension); // Force extension file_path.set_extension(&self.extension); // Force extension
let scope = Default::default();
// See if it is cached // See if it is cached
let (module, ast) = { let exists = {
#[cfg(not(feature = "sync"))] #[cfg(not(feature = "sync"))]
let c = self.cache.borrow(); let c = self.cache.borrow();
#[cfg(feature = "sync")] #[cfg(feature = "sync")]
let c = self.cache.read().unwrap(); let c = self.cache.read().unwrap();
if let Some(ast) = c.get(&file_path) { c.contains_key(&file_path)
(
Module::eval_ast_as_new(scope, ast, engine)
.map_err(|err| err.new_position(pos))?,
None,
)
} else {
// Load the file and compile it if not found
let ast = engine
.compile_file(file_path.clone())
.map_err(|err| err.new_position(pos))?;
(
Module::eval_ast_as_new(scope, &ast, engine)
.map_err(|err| err.new_position(pos))?,
Some(ast),
)
}
}; };
if let Some(ast) = ast { if !exists {
// Load the file and compile it if not found
let ast = engine
.compile_file(file_path.clone())
.map_err(|err| err.new_position(pos))?;
// Put it into the cache // Put it into the cache
#[cfg(not(feature = "sync"))] #[cfg(not(feature = "sync"))]
self.cache.borrow_mut().insert(file_path, ast); self.cache.borrow_mut().insert(file_path.clone(), ast);
#[cfg(feature = "sync")] #[cfg(feature = "sync")]
self.cache.write().unwrap().insert(file_path, ast); self.cache.write().unwrap().insert(file_path.clone(), ast);
} }
#[cfg(not(feature = "sync"))]
let c = self.cache.borrow();
#[cfg(feature = "sync")]
let c = self.cache.read().unwrap();
let ast = c.get(&file_path).unwrap();
let mut module = Module::eval_ast_as_new(Scope::new(), ast, engine)?;
ast.iter_functions(|access, name, num_args| match access {
FnAccess::Private => (),
FnAccess::Public => {
let fn_name = name.to_string();
let ast_lib = ast.lib().clone();
module.set_raw_fn_as_scripted(
name,
num_args,
move |engine: &Engine, _, args: &mut [&mut Dynamic]| {
engine.call_fn_dynamic_raw(
&mut Scope::new(),
&ast_lib,
&fn_name,
&mut None,
args,
)
},
);
}
});
Ok(module) Ok(module)
} }
} }

View File

@ -575,7 +575,7 @@ fn optimize_expr(expr: Expr, state: &mut State) -> Expr {
// First search in functions lib (can override built-in) // First search in functions lib (can override built-in)
// Cater for both normal function call style and method call style (one additional arguments) // Cater for both normal function call style and method call style (one additional arguments)
let has_script_fn = cfg!(not(feature = "no_function")) && state.lib.iter_fn().find(|(_, _, _, f)| { let has_script_fn = cfg!(not(feature = "no_function")) && state.lib.iter_fn().find(|(_, _, _, _,f)| {
if !f.is_script() { return false; } if !f.is_script() { return false; }
let fn_def = f.get_fn_def(); let fn_def = f.get_fn_def();
fn_def.name == name && (args.len()..=args.len() + 1).contains(&fn_def.params.len()) fn_def.name == name && (args.len()..=args.len() + 1).contains(&fn_def.params.len())

View File

@ -130,10 +130,10 @@ impl AST {
/// This operation is cheap because functions are shared. /// This operation is cheap because functions are shared.
pub fn clone_functions_only_filtered( pub fn clone_functions_only_filtered(
&self, &self,
filter: impl Fn(FnAccess, &str, usize) -> bool, mut filter: impl FnMut(FnAccess, &str, usize) -> bool,
) -> Self { ) -> Self {
let mut functions: Module = Default::default(); let mut functions: Module = Default::default();
functions.merge_filtered(&self.1, &filter); functions.merge_filtered(&self.1, &mut filter);
Self(Default::default(), functions) Self(Default::default(), functions)
} }
@ -250,7 +250,7 @@ impl AST {
pub fn merge_filtered( pub fn merge_filtered(
&self, &self,
other: &Self, other: &Self,
filter: impl Fn(FnAccess, &str, usize) -> bool, mut filter: impl FnMut(FnAccess, &str, usize) -> bool,
) -> Self { ) -> Self {
let Self(statements, functions) = self; let Self(statements, functions) = self;
@ -266,7 +266,7 @@ impl AST {
}; };
let mut functions = functions.clone(); let mut functions = functions.clone();
functions.merge_filtered(&other.1, &filter); functions.merge_filtered(&other.1, &mut filter);
Self::new(ast, functions) Self::new(ast, functions)
} }
@ -295,13 +295,13 @@ impl AST {
/// # } /// # }
/// ``` /// ```
#[cfg(not(feature = "no_function"))] #[cfg(not(feature = "no_function"))]
pub fn retain_functions(&mut self, filter: impl Fn(FnAccess, &str, usize) -> bool) { pub fn retain_functions(&mut self, filter: impl FnMut(FnAccess, &str, usize) -> bool) {
self.1.retain_functions(filter); self.1.retain_functions(filter);
} }
/// Iterate through all functions /// Iterate through all functions
#[cfg(not(feature = "no_function"))] #[cfg(not(feature = "no_function"))]
pub fn iter_functions(&self, action: impl Fn(FnAccess, &str, usize)) { pub fn iter_functions(&self, action: impl FnMut(FnAccess, &str, usize)) {
self.1.iter_script_fn_info(action); self.1.iter_script_fn_info(action);
} }