diff --git a/README.md b/README.md index 4ed22d10..a8329aa3 100644 --- a/README.md +++ b/README.md @@ -268,6 +268,7 @@ let ast = engine.compile_file("hello_world.rhai".into())?; ### Calling Rhai functions from Rust Rhai also allows working _backwards_ from the other direction - i.e. calling a Rhai-scripted function from Rust via `call_fn`. +Functions declared with `private` are hidden and cannot be called from Rust (see also [modules]). ```rust // Define functions in a script. @@ -287,6 +288,11 @@ let ast = engine.compile(true, fn hello() { 42 } + + // this one is private and cannot be called by 'call_fn' + private hidden() { + throw "you shouldn't see me!"; + } ")?; // A custom scope can also contain any variables/constants available to the functions @@ -300,11 +306,15 @@ let result: i64 = engine.call_fn(&mut scope, &ast, "hello", ( String::from("abc" // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ // put arguments in a tuple -let result: i64 = engine.call_fn(&mut scope, &ast, "hello", (123_i64,) )? +let result: i64 = engine.call_fn(&mut scope, &ast, "hello", (123_i64,) )?; // ^^^^^^^^^^ tuple of one -let result: i64 = engine.call_fn(&mut scope, &ast, "hello", () )? +let result: i64 = engine.call_fn(&mut scope, &ast, "hello", () )?; // ^^ unit = tuple of zero + +// The following call will return a function-not-found error because +// 'hidden' is declared with 'private'. +let result: () = engine.call_fn(&mut scope, &ast, "hidden", ())?; ``` ### Creating Rust anonymous functions from Rhai script @@ -2052,13 +2062,17 @@ Modules can be disabled via the [`no_module`] feature. A module is a single script (or pre-compiled `AST`) containing global variables and functions. The `export` statement, which can only be at global level, exposes selected variables as members of a module. Variables not exported are private and invisible to the outside. +All functions are automatically exported, unless it is prefixed with `private`. +Functions declared `private` are invisible to the outside. -All functions are automatically exported. Everything exported from a module is **constant** (**read-only**). +Everything exported from a module is **constant** (**read-only**). ```rust // This is a module script. -fn inc(x) { x + 1 } // function +fn inc(x) { x + 1 } // public function + +private fn foo() {} // private function - invisible to outside let private = 123; // variable not exported - invisible to outside let x = 42; // this will be exported below diff --git a/src/api.rs b/src/api.rs index 6f624b76..db4b00c8 100644 --- a/src/api.rs +++ b/src/api.rs @@ -999,7 +999,7 @@ impl Engine { let pos = Position::none(); let fn_def = fn_lib - .get_function_by_signature(name, args.len()) + .get_function_by_signature(name, args.len(), true) .ok_or_else(|| Box::new(EvalAltResult::ErrorFunctionNotFound(name.to_string(), pos)))?; let state = State::new(fn_lib); diff --git a/src/engine.rs b/src/engine.rs index 6a52dcf4..c438602d 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -7,7 +7,7 @@ use crate::optimize::OptimizationLevel; use crate::packages::{ CorePackage, Package, PackageLibrary, PackageStore, PackagesCollection, StandardPackage, }; -use crate::parser::{Expr, FnDef, ReturnType, Stmt, AST}; +use crate::parser::{Expr, FnAccess, FnDef, ReturnType, Stmt, AST}; use crate::result::EvalAltResult; use crate::scope::{EntryType as ScopeEntryType, Scope}; use crate::token::Position; @@ -234,10 +234,22 @@ impl FunctionsLib { self.get(&hash).map(|fn_def| fn_def.as_ref()) } /// Get a function definition from the `FunctionsLib`. - pub fn get_function_by_signature(&self, name: &str, params: usize) -> Option<&FnDef> { + pub fn get_function_by_signature( + &self, + name: &str, + params: usize, + public_only: bool, + ) -> Option<&FnDef> { // Qualifiers (none) + function name + placeholders (one for each parameter). let hash = calc_fn_hash(empty(), name, repeat(EMPTY_TYPE_ID()).take(params)); - self.get_function(hash) + let fn_def = self.get_function(hash); + + match fn_def.as_ref().map(|f| f.access) { + None => None, + Some(FnAccess::Private) if public_only => None, + Some(FnAccess::Private) => fn_def, + Some(FnAccess::Public) => fn_def, + } } /// Merge another `FunctionsLib` into this `FunctionsLib`. pub fn merge(&self, other: &Self) -> Self { diff --git a/src/module.rs b/src/module.rs index ef74ec6a..32ae0874 100644 --- a/src/module.rs +++ b/src/module.rs @@ -4,7 +4,7 @@ use crate::any::{Dynamic, Variant}; use crate::calc_fn_hash; use crate::engine::{Engine, FnAny, FnCallArgs, FunctionsLib, NativeFunction, ScriptedFunction}; -use crate::parser::{FnDef, AST}; +use crate::parser::{FnAccess, FnDef, AST}; use crate::result::EvalAltResult; use crate::scope::{Entry as ScopeEntry, EntryType as ScopeEntryType, Scope}; use crate::token::{Position, Token}; @@ -57,7 +57,7 @@ pub struct Module { all_variables: HashMap, /// External Rust functions. - functions: HashMap, NativeFunction)>, + functions: HashMap, NativeFunction)>, /// Flattened collection of all external Rust functions, including those in sub-modules. all_functions: HashMap, @@ -260,15 +260,21 @@ impl Module { /// Set a Rust function into the module, returning a hash key. /// /// If there is an existing Rust function of the same hash, it is replaced. - pub fn set_fn(&mut self, fn_name: String, params: Vec, func: Box) -> u64 { + pub fn set_fn( + &mut self, + fn_name: String, + access: FnAccess, + params: Vec, + func: Box, + ) -> u64 { let hash = calc_fn_hash(empty(), &fn_name, params.iter().cloned()); #[cfg(not(feature = "sync"))] self.functions - .insert(hash, (fn_name, params, Rc::new(func))); + .insert(hash, (fn_name, access, params, Rc::new(func))); #[cfg(feature = "sync")] self.functions - .insert(hash, (fn_name, params, Arc::new(func))); + .insert(hash, (fn_name, access, params, Arc::new(func))); hash } @@ -283,7 +289,7 @@ impl Module { /// use rhai::Module; /// /// let mut module = Module::new(); - /// let hash = module.set_fn_0("calc", || Ok(42_i64)); + /// let hash = module.set_fn_0("calc", || Ok(42_i64), false); /// assert!(module.get_fn(hash).is_some()); /// ``` pub fn set_fn_0, T: Into>( @@ -291,6 +297,7 @@ impl Module { fn_name: K, #[cfg(not(feature = "sync"))] func: impl Fn() -> FuncReturn + 'static, #[cfg(feature = "sync")] func: impl Fn() -> FuncReturn + Send + Sync + 'static, + is_private: bool, ) -> u64 { let f = move |_: &mut FnCallArgs, pos| { func() @@ -298,7 +305,12 @@ impl Module { .map_err(|err| EvalAltResult::set_position(err, pos)) }; let arg_types = vec![]; - self.set_fn(fn_name.into(), arg_types, Box::new(f)) + let access = if is_private { + FnAccess::Private + } else { + FnAccess::Public + }; + self.set_fn(fn_name.into(), access, arg_types, Box::new(f)) } /// Set a Rust function taking one parameter into the module, returning a hash key. @@ -311,7 +323,7 @@ impl Module { /// use rhai::Module; /// /// let mut module = Module::new(); - /// let hash = module.set_fn_1("calc", |x: i64| Ok(x + 1)); + /// let hash = module.set_fn_1("calc", |x: i64| Ok(x + 1), false); /// assert!(module.get_fn(hash).is_some()); /// ``` pub fn set_fn_1, A: Variant + Clone, T: Into>( @@ -319,6 +331,7 @@ impl Module { fn_name: K, #[cfg(not(feature = "sync"))] func: impl Fn(A) -> FuncReturn + 'static, #[cfg(feature = "sync")] func: impl Fn(A) -> FuncReturn + Send + Sync + 'static, + is_private: bool, ) -> u64 { let f = move |args: &mut FnCallArgs, pos| { func(mem::take(args[0]).cast::()) @@ -326,7 +339,12 @@ impl Module { .map_err(|err| EvalAltResult::set_position(err, pos)) }; let arg_types = vec![TypeId::of::()]; - self.set_fn(fn_name.into(), arg_types, Box::new(f)) + let access = if is_private { + FnAccess::Private + } else { + FnAccess::Public + }; + self.set_fn(fn_name.into(), access, arg_types, Box::new(f)) } /// Set a Rust function taking one mutable parameter into the module, returning a hash key. @@ -339,7 +357,7 @@ impl Module { /// use rhai::Module; /// /// let mut module = Module::new(); - /// let hash = module.set_fn_1_mut("calc", |x: &mut i64| { *x += 1; Ok(*x) }); + /// let hash = module.set_fn_1_mut("calc", |x: &mut i64| { *x += 1; Ok(*x) }, false); /// assert!(module.get_fn(hash).is_some()); /// ``` pub fn set_fn_1_mut, A: Variant + Clone, T: Into>( @@ -347,6 +365,7 @@ impl Module { fn_name: K, #[cfg(not(feature = "sync"))] func: impl Fn(&mut A) -> FuncReturn + 'static, #[cfg(feature = "sync")] func: impl Fn(&mut A) -> FuncReturn + Send + Sync + 'static, + is_private: bool, ) -> u64 { let f = move |args: &mut FnCallArgs, pos| { func(args[0].downcast_mut::().unwrap()) @@ -354,7 +373,12 @@ impl Module { .map_err(|err| EvalAltResult::set_position(err, pos)) }; let arg_types = vec![TypeId::of::()]; - self.set_fn(fn_name.into(), arg_types, Box::new(f)) + let access = if is_private { + FnAccess::Private + } else { + FnAccess::Public + }; + self.set_fn(fn_name.into(), access, arg_types, Box::new(f)) } /// Set a Rust function taking two parameters into the module, returning a hash key. @@ -369,7 +393,7 @@ impl Module { /// let mut module = Module::new(); /// let hash = module.set_fn_2("calc", |x: i64, y: String| { /// Ok(x + y.len() as i64) - /// }); + /// }, false); /// assert!(module.get_fn(hash).is_some()); /// ``` pub fn set_fn_2, A: Variant + Clone, B: Variant + Clone, T: Into>( @@ -377,6 +401,7 @@ impl Module { fn_name: K, #[cfg(not(feature = "sync"))] func: impl Fn(A, B) -> FuncReturn + 'static, #[cfg(feature = "sync")] func: impl Fn(A, B) -> FuncReturn + Send + Sync + 'static, + is_private: bool, ) -> u64 { let f = move |args: &mut FnCallArgs, pos| { let a = mem::take(args[0]).cast::(); @@ -387,7 +412,12 @@ impl Module { .map_err(|err| EvalAltResult::set_position(err, pos)) }; let arg_types = vec![TypeId::of::(), TypeId::of::()]; - self.set_fn(fn_name.into(), arg_types, Box::new(f)) + let access = if is_private { + FnAccess::Private + } else { + FnAccess::Public + }; + self.set_fn(fn_name.into(), access, arg_types, Box::new(f)) } /// Set a Rust function taking two parameters (the first one mutable) into the module, @@ -401,7 +431,7 @@ impl Module { /// let mut module = Module::new(); /// let hash = module.set_fn_2_mut("calc", |x: &mut i64, y: String| { /// *x += y.len() as i64; Ok(*x) - /// }); + /// }, false); /// assert!(module.get_fn(hash).is_some()); /// ``` pub fn set_fn_2_mut< @@ -414,6 +444,7 @@ impl Module { fn_name: K, #[cfg(not(feature = "sync"))] func: impl Fn(&mut A, B) -> FuncReturn + 'static, #[cfg(feature = "sync")] func: impl Fn(&mut A, B) -> FuncReturn + Send + Sync + 'static, + is_private: bool, ) -> u64 { let f = move |args: &mut FnCallArgs, pos| { let b = mem::take(args[1]).cast::(); @@ -424,7 +455,12 @@ impl Module { .map_err(|err| EvalAltResult::set_position(err, pos)) }; let arg_types = vec![TypeId::of::(), TypeId::of::()]; - self.set_fn(fn_name.into(), arg_types, Box::new(f)) + let access = if is_private { + FnAccess::Private + } else { + FnAccess::Public + }; + self.set_fn(fn_name.into(), access, arg_types, Box::new(f)) } /// Set a Rust function taking three parameters into the module, returning a hash key. @@ -439,7 +475,7 @@ impl Module { /// let mut module = Module::new(); /// let hash = module.set_fn_3("calc", |x: i64, y: String, z: i64| { /// Ok(x + y.len() as i64 + z) - /// }); + /// }, false); /// assert!(module.get_fn(hash).is_some()); /// ``` pub fn set_fn_3< @@ -453,6 +489,7 @@ impl Module { fn_name: K, #[cfg(not(feature = "sync"))] func: impl Fn(A, B, C) -> FuncReturn + 'static, #[cfg(feature = "sync")] func: impl Fn(A, B, C) -> FuncReturn + Send + Sync + 'static, + is_private: bool, ) -> u64 { let f = move |args: &mut FnCallArgs, pos| { let a = mem::take(args[0]).cast::(); @@ -464,7 +501,12 @@ impl Module { .map_err(|err| EvalAltResult::set_position(err, pos)) }; let arg_types = vec![TypeId::of::(), TypeId::of::(), TypeId::of::()]; - self.set_fn(fn_name.into(), arg_types, Box::new(f)) + let access = if is_private { + FnAccess::Private + } else { + FnAccess::Public + }; + self.set_fn(fn_name.into(), access, arg_types, Box::new(f)) } /// Set a Rust function taking three parameters (the first one mutable) into the module, @@ -480,7 +522,7 @@ impl Module { /// let mut module = Module::new(); /// let hash = module.set_fn_3_mut("calc", |x: &mut i64, y: String, z: i64| { /// *x += y.len() as i64 + z; Ok(*x) - /// }); + /// }, false); /// assert!(module.get_fn(hash).is_some()); /// ``` pub fn set_fn_3_mut< @@ -494,6 +536,7 @@ impl Module { fn_name: K, #[cfg(not(feature = "sync"))] func: impl Fn(&mut A, B, C) -> FuncReturn + 'static, #[cfg(feature = "sync")] func: impl Fn(&mut A, B, C) -> FuncReturn + Send + Sync + 'static, + is_private: bool, ) -> u64 { let f = move |args: &mut FnCallArgs, pos| { let b = mem::take(args[1]).cast::(); @@ -505,7 +548,12 @@ impl Module { .map_err(|err| EvalAltResult::set_position(err, pos)) }; let arg_types = vec![TypeId::of::(), TypeId::of::(), TypeId::of::()]; - self.set_fn(fn_name.into(), arg_types, Box::new(f)) + let access = if is_private { + FnAccess::Private + } else { + FnAccess::Public + }; + self.set_fn(fn_name.into(), access, arg_types, Box::new(f)) } /// Get a Rust function. @@ -523,7 +571,7 @@ impl Module { /// assert!(module.get_fn(hash).is_some()); /// ``` pub fn get_fn(&self, hash: u64) -> Option<&Box> { - self.functions.get(&hash).map(|(_, _, v)| v.as_ref()) + self.functions.get(&hash).map(|(_, _, _, v)| v.as_ref()) } /// Get a modules-qualified function. @@ -588,11 +636,7 @@ impl Module { scope.into_iter().for_each( |ScopeEntry { - name, - typ, - value, - alias, - .. + typ, value, alias, .. }| { match typ { // Variables with an alias left in the scope become module variables @@ -641,7 +685,12 @@ impl Module { variables.push((hash, value.clone())); } // Index all Rust functions - for (fn_name, params, func) in module.functions.values() { + for (fn_name, access, params, func) in module.functions.values() { + match access { + // Private functions are not exported + FnAccess::Private => continue, + FnAccess::Public => (), + } // Rust functions are indexed in two steps: // 1) Calculate a hash in a similar manner to script-defined functions, // i.e. qualifiers + function name + dummy parameter types (one for each parameter). @@ -660,6 +709,11 @@ impl Module { } // Index all script-defined functions for fn_def in module.fn_lib.values() { + match fn_def.access { + // Private functions are not exported + FnAccess::Private => continue, + FnAccess::Public => (), + } // Qualifiers + function name + placeholders (one for each parameter) let hash = calc_fn_hash( qualifiers.iter().map(|v| *v), diff --git a/src/parser.rs b/src/parser.rs index b3b05e1b..9585b53a 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -178,11 +178,22 @@ impl Add for &AST { } } -/// A script-function definition. +/// A type representing the access mode of a scripted function. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum FnAccess { + /// Private function. + Private, + /// Public function. + Public, +} + +/// A scripted function definition. #[derive(Debug, Clone)] pub struct FnDef { /// Function name. pub name: String, + /// Function access mode. + pub access: FnAccess, /// Names of function parameters. pub params: Vec, /// Function body. @@ -2208,6 +2219,7 @@ fn parse_stmt<'a>( fn parse_fn<'a>( input: &mut Peekable>, stack: &mut Stack, + access: FnAccess, allow_stmt_expr: bool, ) -> Result> { let pos = eat_token(input, Token::Fn); @@ -2283,6 +2295,7 @@ fn parse_fn<'a>( Ok(FnDef { name, + access, params, body, pos, @@ -2330,19 +2343,37 @@ fn parse_global_level<'a>( // Collect all the function definitions #[cfg(not(feature = "no_function"))] { - if let (Token::Fn, _) = input.peek().unwrap() { - let mut stack = Stack::new(); - let func = parse_fn(input, &mut stack, true)?; + let mut access = FnAccess::Public; + let mut must_be_fn = false; - // Qualifiers (none) + function name + argument `TypeId`'s - let hash = calc_fn_hash( - empty(), - &func.name, - repeat(EMPTY_TYPE_ID()).take(func.params.len()), - ); + if match_token(input, Token::Private)? { + access = FnAccess::Private; + must_be_fn = true; + } - functions.insert(hash, func); - continue; + match input.peek().unwrap() { + (Token::Fn, _) => { + let mut stack = Stack::new(); + let func = parse_fn(input, &mut stack, access, true)?; + + // Qualifiers (none) + function name + argument `TypeId`'s + let hash = calc_fn_hash( + empty(), + &func.name, + repeat(EMPTY_TYPE_ID()).take(func.params.len()), + ); + + functions.insert(hash, func); + continue; + } + (_, pos) if must_be_fn => { + return Err(PERR::MissingToken( + Token::Fn.into(), + format!("following '{}'", Token::Private.syntax()), + ) + .into_err(*pos)) + } + _ => (), } } // Actual statement diff --git a/src/token.rs b/src/token.rs index f67a8e14..9692b112 100644 --- a/src/token.rs +++ b/src/token.rs @@ -196,6 +196,7 @@ pub enum Token { XOrAssign, ModuloAssign, PowerOfAssign, + Private, Import, Export, As, @@ -279,6 +280,7 @@ impl Token { ModuloAssign => "%=", PowerOf => "~", PowerOfAssign => "~=", + Private => "private", Import => "import", Export => "export", As => "as", @@ -750,6 +752,7 @@ impl<'a> TokenIterator<'a> { "throw" => Token::Throw, "for" => Token::For, "in" => Token::In, + "private" => Token::Private, #[cfg(not(feature = "no_module"))] "import" => Token::Import, diff --git a/tests/modules.rs b/tests/modules.rs index 7ec17483..1fc4d895 100644 --- a/tests/modules.rs +++ b/tests/modules.rs @@ -18,7 +18,12 @@ fn test_module_sub_module() -> Result<(), Box> { let mut sub_module2 = Module::new(); sub_module2.set_var("answer", 41 as INT); - let hash = sub_module2.set_fn_1("inc", |x: INT| Ok(x + 1)); + let hash_inc = sub_module2.set_fn_1("inc", |x: INT| Ok(x + 1), false); + let hash_hidden = sub_module2.set_fn_0( + "hidden", + || Err("shouldn't see me!".into()) as Result<(), Box>, + true, + ); sub_module.set_sub_module("universe", sub_module2); module.set_sub_module("life", sub_module); @@ -30,11 +35,12 @@ fn test_module_sub_module() -> Result<(), Box> { let m2 = m.get_sub_module("universe").unwrap(); assert!(m2.contains_var("answer")); - assert!(m2.contains_fn(hash)); + assert!(m2.contains_fn(hash_inc)); + assert!(m2.contains_fn(hash_hidden)); assert_eq!(m2.get_var_value::("answer").unwrap(), 41); - let mut engine = Engine::new(); + let engine = Engine::new(); let mut scope = Scope::new(); scope.push_module("question", module); @@ -53,6 +59,11 @@ fn test_module_sub_module() -> Result<(), Box> { )?, 42 ); + assert!(matches!( + *engine.eval_expression_with_scope::<()>(&mut scope, "question::life::universe::hidden()") + .expect_err("should error"), + EvalAltResult::ErrorFunctionNotFound(fn_name, _) if fn_name == "hidden" + )); Ok(()) } @@ -102,6 +113,9 @@ fn test_module_from_ast() -> Result<(), Box> { fn add_len(x, y) { x + y.len() } + private fn hidden() { + throw "you shouldn't see me!"; + } // Imported modules become sub-modules import "another module" as extra; @@ -152,6 +166,12 @@ fn test_module_from_ast() -> Result<(), Box> { )?, 59 ); + assert!(matches!( + *engine + .eval_expression_with_scope::<()>(&mut scope, "testing::hidden()") + .expect_err("should error"), + EvalAltResult::ErrorFunctionNotFound(fn_name, _) if fn_name == "hidden" + )); Ok(()) }