Refine no_function feature.

This commit is contained in:
Stephen Chung 2020-07-04 16:21:15 +08:00
parent 467b109c23
commit d626bf9f5b
8 changed files with 122 additions and 71 deletions

View File

@ -21,7 +21,7 @@ num-traits = { version = "0.2.11", default-features = false }
[features]
#default = ["unchecked", "sync", "no_optimize", "no_float", "only_i32", "no_index", "no_object", "no_function", "no_module"]
default = ["serde"]
default = []
plugins = []
unchecked = [] # unchecked arithmetic
sync = [] # restrict to only types that implement Send + Sync

View File

@ -29,7 +29,7 @@ Features
* Fairly efficient evaluation (1 million iterations in 0.4 sec on a single core, 2.3 GHz Linux VM).
* Relatively little `unsafe` code (yes there are some for performance reasons, and most `unsafe` code is limited to
one single source file, all with names starting with `"unsafe_"`).
* Re-entrant scripting engine can be made `Send + Sync` (via the [`sync`] feature).
* Re-entrant scripting engine can be made `Send + Sync` (via the `sync` feature).
* Sand-boxed - the scripting engine, if declared immutable, cannot mutate the containing environment unless explicitly permitted (e.g. via a `RefCell`).
* Rugged - protected against malicious attacks (such as [stack-overflow](https://schungx.github.io/rhai/safety/max-call-stack.html), [over-sized data](https://schungx.github.io/rhai/safety/max-string-size.html), and [runaway scripts](https://schungx.github.io/rhai/safety/max-operations.html) etc.) that may come from untrusted third-party user-land scripts.
* Track script evaluation [progress](https://schungx.github.io/rhai/safety/progress.html) and manually terminate a script run.
@ -38,7 +38,7 @@ Features
* Dynamic dispatch via [function pointers](https://schungx.github.io/rhai/language/fn-ptr.html).
* Some support for [object-oriented programming (OOP)](https://schungx.github.io/rhai/language/oop.html).
* Organize code base with dynamically-loadable [modules](https://schungx.github.io/rhai/language/modules.html).
* Serialization/deserialization support via [serde](https://crates.io/crates/serde)
* Serialization/deserialization support via [serde](https://crates.io/crates/serde) (requires the `serde` feature).
* Scripts are [optimized](https://schungx.github.io/rhai/engine/optimize.html) (useful for template-based machine-generated scripts) for repeated evaluations.
* Support for [minimal builds](https://schungx.github.io/rhai/start/builds/minimal.html) by excluding unneeded language [features](https://schungx.github.io/rhai/start/features.html).

View File

@ -1,10 +1,7 @@
//! Module that defines the extern API of `Engine`.
use crate::any::{Dynamic, Variant};
use crate::engine::{
get_script_function_by_signature, make_getter, make_setter, Engine, Imports, State, FN_IDX_GET,
FN_IDX_SET,
};
use crate::engine::{make_getter, make_setter, Engine, Imports, State, FN_IDX_GET, FN_IDX_SET};
use crate::error::ParseError;
use crate::fn_call::FuncArgs;
use crate::fn_native::{IteratorFn, SendSync};
@ -19,6 +16,9 @@ use crate::utils::StaticVec;
#[cfg(not(feature = "no_object"))]
use crate::engine::Map;
#[cfg(not(feature = "no_function"))]
use crate::engine::get_script_function_by_signature;
use crate::stdlib::{
any::{type_name, TypeId},
boxed::Box,
@ -1189,6 +1189,7 @@ impl Engine {
/// This is to avoid unnecessarily cloning the arguments.
/// Do not use the arguments after this call. If they are needed afterwards,
/// clone them _before_ calling this function.
#[cfg(not(feature = "no_function"))]
pub(crate) fn call_fn_dynamic_raw(
&self,
scope: &mut Scope,
@ -1240,6 +1241,7 @@ impl Engine {
mut ast: AST,
optimization_level: OptimizationLevel,
) -> AST {
#[cfg(not(feature = "no_function"))]
let lib = ast
.lib()
.iter_fn()
@ -1247,6 +1249,9 @@ impl Engine {
.map(|(_, _, _, f)| f.get_fn_def().clone())
.collect();
#[cfg(feature = "no_function")]
let lib = Default::default();
let stmt = mem::take(ast.statements_mut());
optimize_into_ast(self, scope, stmt, lib, optimization_level)
}

View File

@ -216,6 +216,7 @@ impl State {
}
/// Get a script-defined function definition from a module.
#[cfg(not(feature = "no_function"))]
pub fn get_script_function_by_signature<'a>(
module: &'a Module,
name: &str,
@ -767,22 +768,23 @@ impl Engine {
.or_else(|| self.packages.get_fn(hash_fn));
if let Some(func) = func {
// Calling pure function but the first argument is a reference?
normalize_first_arg(
is_ref && (func.is_pure() || (func.is_script() && !is_method)),
&mut this_copy,
&mut old_this_ptr,
args,
);
#[cfg(not(feature = "no_function"))]
let need_normalize = is_ref && (func.is_pure() || (func.is_script() && !is_method));
#[cfg(feature = "no_function")]
let need_normalize = is_ref && func.is_pure();
// Calling pure function but the first argument is a reference?
normalize_first_arg(need_normalize, &mut this_copy, &mut old_this_ptr, args);
#[cfg(not(feature = "no_function"))]
if func.is_script() {
// Run scripted function
let fn_def = func.get_fn_def();
// Method call of script function - map first argument to `this`
if is_method {
return if is_method {
let (first, rest) = args.split_at_mut(1);
return Ok((
Ok((
self.call_script_fn(
scope,
mods,
@ -795,7 +797,7 @@ impl Engine {
level,
)?,
false,
));
))
} else {
let result = self.call_script_fn(
scope, mods, state, lib, &mut None, fn_name, fn_def, args, level,
@ -804,42 +806,42 @@ impl Engine {
// Restore the original reference
restore_first_arg(old_this_ptr, args);
return Ok((result, false));
Ok((result, false))
};
} else {
// Run external function
let result = func.get_native_fn()(self, args)?;
// Restore the original reference
restore_first_arg(old_this_ptr, args);
// See if the function match print/debug (which requires special processing)
return Ok(match fn_name {
KEYWORD_PRINT => (
(self.print)(result.as_str().map_err(|typ| {
Box::new(EvalAltResult::ErrorMismatchOutputType(
self.map_type_name(type_name::<ImmutableString>()).into(),
typ.into(),
Position::none(),
))
})?)
.into(),
false,
),
KEYWORD_DEBUG => (
(self.debug)(result.as_str().map_err(|typ| {
Box::new(EvalAltResult::ErrorMismatchOutputType(
self.map_type_name(type_name::<ImmutableString>()).into(),
typ.into(),
Position::none(),
))
})?)
.into(),
false,
),
_ => (result, func.is_method()),
});
}
// Run external function
let result = func.get_native_fn()(self, args)?;
// Restore the original reference
restore_first_arg(old_this_ptr, args);
// See if the function match print/debug (which requires special processing)
return Ok(match fn_name {
KEYWORD_PRINT => (
(self.print)(result.as_str().map_err(|typ| {
Box::new(EvalAltResult::ErrorMismatchOutputType(
self.map_type_name(type_name::<ImmutableString>()).into(),
typ.into(),
Position::none(),
))
})?)
.into(),
false,
),
KEYWORD_DEBUG => (
(self.debug)(result.as_str().map_err(|typ| {
Box::new(EvalAltResult::ErrorMismatchOutputType(
self.map_type_name(type_name::<ImmutableString>()).into(),
typ.into(),
Position::none(),
))
})?)
.into(),
false,
),
_ => (result, func.is_method()),
});
}
// See if it is built in.
@ -2016,6 +2018,7 @@ impl Engine {
};
match func {
#[cfg(not(feature = "no_function"))]
Ok(f) if f.is_script() => {
let args = args.as_mut();
let fn_def = f.get_fn_def();

View File

@ -114,6 +114,7 @@ pub enum CallableFunction {
/// An iterator function.
Iterator(IteratorFn),
/// A script-defined function.
#[cfg(not(feature = "no_function"))]
Script(Shared<ScriptFnDef>),
}
@ -123,6 +124,8 @@ impl fmt::Debug for CallableFunction {
Self::Pure(_) => write!(f, "NativePureFunction"),
Self::Method(_) => write!(f, "NativeMethod"),
Self::Iterator(_) => write!(f, "NativeIterator"),
#[cfg(not(feature = "no_function"))]
Self::Script(fn_def) => fmt::Debug::fmt(fn_def, f),
}
}
@ -134,6 +137,8 @@ impl fmt::Display for CallableFunction {
Self::Pure(_) => write!(f, "NativePureFunction"),
Self::Method(_) => write!(f, "NativeMethod"),
Self::Iterator(_) => write!(f, "NativeIterator"),
#[cfg(not(feature = "no_function"))]
CallableFunction::Script(s) => fmt::Display::fmt(s, f),
}
}
@ -144,24 +149,34 @@ impl CallableFunction {
pub fn is_pure(&self) -> bool {
match self {
Self::Pure(_) => true,
Self::Method(_) | Self::Iterator(_) | Self::Script(_) => false,
Self::Method(_) | Self::Iterator(_) => false,
#[cfg(not(feature = "no_function"))]
Self::Script(_) => false,
}
}
/// Is this a native Rust method function?
pub fn is_method(&self) -> bool {
match self {
Self::Method(_) => true,
Self::Pure(_) | Self::Iterator(_) | Self::Script(_) => false,
Self::Pure(_) | Self::Iterator(_) => false,
#[cfg(not(feature = "no_function"))]
Self::Script(_) => false,
}
}
/// Is this an iterator function?
pub fn is_iter(&self) -> bool {
match self {
Self::Iterator(_) => true,
Self::Pure(_) | Self::Method(_) | Self::Script(_) => false,
Self::Pure(_) | Self::Method(_) => false,
#[cfg(not(feature = "no_function"))]
Self::Script(_) => false,
}
}
/// Is this a Rhai-scripted function?
#[cfg(not(feature = "no_function"))]
pub fn is_script(&self) -> bool {
match self {
Self::Script(_) => true,
@ -176,7 +191,10 @@ impl CallableFunction {
pub fn get_native_fn(&self) -> &FnAny {
match self {
Self::Pure(f) | Self::Method(f) => f.as_ref(),
Self::Iterator(_) | Self::Script(_) => unreachable!(),
Self::Iterator(_) => unreachable!(),
#[cfg(not(feature = "no_function"))]
Self::Script(_) => unreachable!(),
}
}
/// Get a shared reference to a script-defined function definition.
@ -184,6 +202,7 @@ impl CallableFunction {
/// # Panics
///
/// Panics if the `CallableFunction` is not `Script`.
#[cfg(not(feature = "no_function"))]
pub fn get_shared_fn_def(&self) -> Shared<ScriptFnDef> {
match self {
Self::Pure(_) | Self::Method(_) | Self::Iterator(_) => unreachable!(),
@ -195,6 +214,7 @@ impl CallableFunction {
/// # Panics
///
/// Panics if the `CallableFunction` is not `Script`.
#[cfg(not(feature = "no_function"))]
pub fn get_fn_def(&self) -> &ScriptFnDef {
match self {
Self::Pure(_) | Self::Method(_) | Self::Iterator(_) => unreachable!(),
@ -209,7 +229,10 @@ impl CallableFunction {
pub fn get_iter_fn(&self) -> IteratorFn {
match self {
Self::Iterator(f) => *f,
Self::Pure(_) | Self::Method(_) | Self::Script(_) => unreachable!(),
Self::Pure(_) | Self::Method(_) => unreachable!(),
#[cfg(not(feature = "no_function"))]
Self::Script(_) => unreachable!(),
}
}
/// Create a new `CallableFunction::Pure`.
@ -228,12 +251,14 @@ impl From<IteratorFn> for CallableFunction {
}
}
#[cfg(not(feature = "no_function"))]
impl From<ScriptFnDef> for CallableFunction {
fn from(func: ScriptFnDef) -> Self {
Self::Script(func.into())
}
}
#[cfg(not(feature = "no_function"))]
impl From<Shared<ScriptFnDef>> for CallableFunction {
fn from(func: Shared<ScriptFnDef>) -> Self {
Self::Script(func)

View File

@ -125,17 +125,23 @@ pub use parser::FLOAT;
pub use module::ModuleResolver;
/// Module containing all built-in _module resolvers_ available to Rhai.
///
/// Not available under the `no_module` feature.
#[cfg(not(feature = "no_module"))]
pub mod module_resolvers {
pub use crate::module::resolvers::*;
}
/// Serialization support for [`serde`](https://crates.io/crates/serde).
///
/// Requires the `serde` feature.
#[cfg(feature = "serde")]
pub mod ser {
pub use crate::serde::ser::to_dynamic;
}
/// Deserialization support for [`serde`](https://crates.io/crates/serde).
///
/// Requires the `serde` feature.
#[cfg(feature = "serde")]
pub mod de {
pub use crate::serde::de::from_dynamic;

View File

@ -236,6 +236,7 @@ impl 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.
#[cfg(not(feature = "no_function"))]
pub(crate) fn set_script_fn(&mut self, fn_def: ScriptFnDef) {
// None + function name + number of arguments.
let hash_script = calc_fn_hash(empty(), &fn_def.name, fn_def.params.len(), empty());
@ -876,6 +877,7 @@ impl Module {
.functions
.iter()
.filter(|(_, (_, _, _, v))| match v {
#[cfg(not(feature = "no_function"))]
CallableFunction::Script(ref f) => {
filter(f.access, f.name.as_str(), f.params.len())
}
@ -893,6 +895,7 @@ impl Module {
}
/// Filter out the functions, retaining only some based on a filter predicate.
#[cfg(not(feature = "no_function"))]
pub(crate) fn retain_functions(&mut self, filter: impl Fn(FnAccess, &str, usize) -> bool) {
self.functions.retain(|_, (_, _, _, v)| match v {
CallableFunction::Script(ref f) => filter(f.access, f.name.as_str(), f.params.len()),
@ -930,6 +933,7 @@ impl Module {
}
/// Get an iterator over all script-defined functions in the module.
#[cfg(not(feature = "no_function"))]
pub fn iter_script_fn<'a>(&'a self) -> impl Iterator<Item = Shared<ScriptFnDef>> + 'a {
self.functions
.values()
@ -1014,6 +1018,7 @@ impl Module {
Public => (),
}
#[cfg(not(feature = "no_function"))]
if func.is_script() {
let fn_def = func.get_shared_fn_def();
// Qualifiers + function name + number of arguments.
@ -1024,20 +1029,21 @@ impl Module {
empty(),
);
functions.push((hash_qualified_script, fn_def.into()));
} else {
// Qualified Rust functions are indexed in two steps:
// 1) Calculate a hash in a similar manner to script-defined functions,
// i.e. qualifiers + function name + number of arguments.
let hash_qualified_script =
calc_fn_hash(qualifiers.iter().map(|&v| v), name, params.len(), empty());
// 2) Calculate a second hash with no qualifiers, empty function name,
// zero number of arguments, and the actual list of argument `TypeId`'.s
let hash_fn_args = calc_fn_hash(empty(), "", 0, params.iter().cloned());
// 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()));
continue;
}
// Qualified Rust functions are indexed in two steps:
// 1) Calculate a hash in a similar manner to script-defined functions,
// i.e. qualifiers + function name + number of arguments.
let hash_qualified_script =
calc_fn_hash(qualifiers.iter().map(|&v| v), name, params.len(), empty());
// 2) Calculate a second hash with no qualifiers, empty function name,
// zero number of arguments, and the actual list of argument `TypeId`'.s
let hash_fn_args = calc_fn_hash(empty(), "", 0, params.iter().cloned());
// 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()));
}
}

View File

@ -551,11 +551,17 @@ fn optimize_expr(expr: Expr, state: &mut State) -> Expr {
// First search in functions lib (can override built-in)
// Cater for both normal function call style and method call style (one additional arguments)
if state.lib.iter_fn().find(|(_, _, _, f)| {
#[cfg(not(feature = "no_function"))]
let has_script_fn = state.lib.iter_fn().find(|(_, _, _, f)| {
if !f.is_script() { return false; }
let fn_def = f.get_fn_def();
&fn_def.name == name && (args.len()..=args.len() + 1).contains(&fn_def.params.len())
}).is_some() {
}).is_some();
#[cfg(feature = "no_function")]
const has_script_fn: bool = false;
if has_script_fn {
// A script-defined function overrides the built-in function - do not make the call
x.3 = x.3.into_iter().map(|a| optimize_expr(a, state)).collect();
return Expr::FnCall(x);