From e8d78bdfde2c36fe52ea2895cd04f387e9d034bf Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Mon, 13 Jul 2020 13:40:51 +0800 Subject: [PATCH 1/2] Add support for String in function parameters. --- src/engine.rs | 2 +- src/fn_register.rs | 8 +++++++- tests/decrement.rs | 2 +- tests/string.rs | 15 +++++++-------- 4 files changed, 16 insertions(+), 11 deletions(-) diff --git a/src/engine.rs b/src/engine.rs index 0455c6f1..1c39120f 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -855,7 +855,7 @@ impl Engine { fn_name, args.iter() .map(|name| if name.is::() { - "&str | ImmutableString" + "&str | ImmutableString | String" } else { self.map_type_name((*name).type_name()) }) diff --git a/src/fn_register.rs b/src/fn_register.rs index 7cf7be74..23d8043f 100644 --- a/src/fn_register.rs +++ b/src/fn_register.rs @@ -6,6 +6,7 @@ use crate::engine::Engine; use crate::fn_native::{CallableFunction, FnAny, FnCallArgs, SendSync}; use crate::module::Module; use crate::parser::FnAccess; +use crate::r#unsafe::unsafe_cast_box; use crate::result::EvalAltResult; use crate::utils::ImmutableString; @@ -105,6 +106,9 @@ pub fn by_value(data: &mut Dynamic) -> T { let ref_str = data.as_str().unwrap(); let ref_T = unsafe { mem::transmute::<_, &T>(&ref_str) }; ref_T.clone() + } else if TypeId::of::() == TypeId::of::() { + // If T is String, data must be ImmutableString, so map directly to it + *unsafe_cast_box(Box::new(data.as_str().unwrap().to_string())).unwrap() } else { // We consume the argument and then replace it with () - the argument is not supposed to be used again. // This way, we avoid having to clone the argument again, because it is already a clone when passed here. @@ -154,13 +158,15 @@ pub fn map_result( data } -/// Remap `&str` to `ImmutableString`. +/// Remap `&str` | `String` to `ImmutableString`. #[inline(always)] fn map_type_id() -> TypeId { let id = TypeId::of::(); if id == TypeId::of::<&str>() { TypeId::of::() + } else if id == TypeId::of::() { + TypeId::of::() } else { id } diff --git a/tests/decrement.rs b/tests/decrement.rs index 1b74dfe9..0691eddf 100644 --- a/tests/decrement.rs +++ b/tests/decrement.rs @@ -8,7 +8,7 @@ fn test_decrement() -> Result<(), Box> { assert!(matches!( *engine.eval::(r#"let s = "test"; s -= "ing"; s"#).expect_err("expects error"), - EvalAltResult::ErrorFunctionNotFound(err, _) if err == "- (&str | ImmutableString, &str | ImmutableString)" + EvalAltResult::ErrorFunctionNotFound(err, _) if err == "- (&str | ImmutableString | String, &str | ImmutableString | String)" )); Ok(()) diff --git a/tests/string.rs b/tests/string.rs index e6b77841..908b4c6c 100644 --- a/tests/string.rs +++ b/tests/string.rs @@ -173,17 +173,16 @@ fn test_string_fn() -> Result<(), Box> { "foo" ); - engine.register_fn("foo1", |s: &str| s.len() as INT); - engine.register_fn("foo2", |s: ImmutableString| s.len() as INT); - engine.register_fn("foo3", |s: String| s.len() as INT); + engine + .register_fn("foo1", |s: &str| s.len() as INT) + .register_fn("foo2", |s: ImmutableString| s.len() as INT) + .register_fn("foo3", |s: String| s.len() as INT) + .register_fn("foo4", |s: &mut ImmutableString| s.len() as INT); assert_eq!(engine.eval::(r#"foo1("hello")"#)?, 5); assert_eq!(engine.eval::(r#"foo2("hello")"#)?, 5); - - assert!(matches!( - *engine.eval::(r#"foo3("hello")"#).expect_err("should error"), - EvalAltResult::ErrorFunctionNotFound(err, _) if err == "foo3 (&str | ImmutableString)" - )); + assert_eq!(engine.eval::(r#"foo3("hello")"#)?, 5); + assert_eq!(engine.eval::(r#"foo4("hello")"#)?, 5); Ok(()) } From 930abb8b5c8e93e9dc7fca20eebf06eb24d951cc Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Mon, 13 Jul 2020 13:41:01 +0800 Subject: [PATCH 2/2] Update docs. --- README.md | 24 ++++++++++++++------ RELEASES.md | 9 ++++---- doc/src/engine/call-fn.md | 2 +- doc/src/engine/custom-op.md | 4 ++-- doc/src/language/dynamic.md | 2 +- doc/src/language/method.md | 24 ++++++++++++++++++++ doc/src/language/strings-chars.md | 15 ++++++++----- doc/src/rust/getters-setters.md | 2 ++ doc/src/rust/indexers.md | 10 ++++----- doc/src/rust/strings.md | 34 ++++++++++++++++++++++++++--- doc/src/start/builds/performance.md | 13 +++++++++++ doc/src/start/examples/rust.md | 2 +- 12 files changed, 112 insertions(+), 29 deletions(-) diff --git a/README.md b/README.md index 285595d0..92f2ef78 100644 --- a/README.md +++ b/README.md @@ -18,8 +18,8 @@ Supported targets and builds * WebAssembly (WASM) * `no-std` -Features --------- +Standard Features +---------------- * Easy-to-use language similar to JavaScript+Rust with dynamic typing. * Tight integration with native Rust [functions](https://schungx.github.io/rhai/rust/functions.html) and [types]([#custom-types-and-methods](https://schungx.github.io/rhai/rust/custom.html)), including [getters/setters](https://schungx.github.io/rhai/rust/getters-setters.html), [methods](https://schungx.github.io/rhai/rust/custom.html) and [indexers](https://schungx.github.io/rhai/rust/indexers.html). @@ -30,20 +30,30 @@ Features * 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). -* 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. * [Function overloading](https://schungx.github.io/rhai/language/overload.html). * [Operator overloading](https://schungx.github.io/rhai/rust/operators.html). -* Support for use as a [DSL](https://schungx.github.io/rhai/engine/dsl.html) - [disabling keywords/operators](https://schungx.github.io/rhai/engine/disable.html), [custom operators](https://schungx.github.io/rhai/engine/custom-op.html) and extending the language with [custom syntax](https://schungx.github.io/rhai/engine/custom-syntax.html). * 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) (requires the `serde` feature). -* Surgically disable keywords and operators to restrict the language. * 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). +Protection Against Attacks +------------------------- + +* 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. + +For Those Who Actually Want Their Own Language +--------------------------------------------- + +* Use as a [DSL](https://schungx.github.io/rhai/engine/dsl.html). +* Define [custom operators](https://schungx.github.io/rhai/engine/custom-op.html). +* Restrict the language by surgically [disabling keywords and operators](https://schungx.github.io/rhai/engine/disable.html). +* Extend the language with [custom syntax](https://schungx.github.io/rhai/engine/custom-syntax.html). + Documentation ------------- diff --git a/RELEASES.md b/RELEASES.md index c1bbc632..541b8177 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -7,8 +7,9 @@ Version 0.17.0 This version adds: * [`serde`](https://crates.io/crates/serde) support for working with `Dynamic` values (particularly _object maps_). -* Ability to surgically disable keywords and/or operators in the language. -* Ability to define custom operators (which must be valid identifiers). +* Surgically disable keywords and/or operators in the language. +* Define custom operators. +* Extend the language via custom syntax. * Low-level API to register functions. Bug fixes @@ -22,7 +23,6 @@ Breaking changes * `EvalAltResult::ErrorMismatchOutputType` has an extra argument containing the name of the requested type. * `Engine::call_fn_dynamic` take an extra argument, allowing a `Dynamic` value to be bound to the `this` pointer. * Precedence of the `%` (modulo) operator is lowered to below `<<` ad `>>`. This is to handle the case of `x << 3 % 10`. -* Many configuration/setting API's now returns `&mut Self` so that the calls can be chained. This should not affect most code. New features ------------ @@ -38,7 +38,8 @@ New features * `FnPtr` is exposed as the function pointer type. * `rhai::module_resolvers::ModuleResolversCollection` added to try a list of module resolvers. * It is now possible to mutate the first argument of a module-qualified function call when the argument is a simple variable (but not a module constant). - +* Many configuration/setting API's now returns `&mut Self` so that the calls can be chained. +* `String` parameters in functions are supported (but inefficiently). Version 0.16.1 ============== diff --git a/doc/src/engine/call-fn.md b/doc/src/engine/call-fn.md index 7a49d6e4..058d6dab 100644 --- a/doc/src/engine/call-fn.md +++ b/doc/src/engine/call-fn.md @@ -91,5 +91,5 @@ let result = engine.call_fn_dynamic( [ 41_i64.into() ] )?; -assert_eq!(value.as_int().unwrap(), 42); +assert_eq!(value.as_int()?, 42); ``` diff --git a/doc/src/engine/custom-op.md b/doc/src/engine/custom-op.md index 0b92c8a9..79f70d7c 100644 --- a/doc/src/engine/custom-op.md +++ b/doc/src/engine/custom-op.md @@ -21,7 +21,7 @@ let mut engine = Engine::new(); // (i.e. between +|- and *|/) // Also register the implementation of the customer operator as a function engine - .register_custom_operator("foo", 160).unwrap() + .register_custom_operator("foo", 160)? .register_fn("foo", |x: i64, y: i64| (x * y) - (x + y)); // The custom operator can be used in expressions @@ -72,7 +72,7 @@ _Unary_ custom operators are not supported. ```rust engine - .register_custom_operator("foo", 160).unwrap() + .register_custom_operator("foo", 160)? .register_fn("foo", |x: i64| x * x); engine.eval::("1 + 2 * 3 foo 4 - 5 / 6")?; // error: function 'foo (i64, i64)' not found diff --git a/doc/src/language/dynamic.md b/doc/src/language/dynamic.md index 9ebe7d6f..9a3c27b7 100644 --- a/doc/src/language/dynamic.md +++ b/doc/src/language/dynamic.md @@ -66,7 +66,7 @@ item.is::() == true; // 'is' returns whether a 'Dynam let value = item.cast::(); // if the element is 'i64', this succeeds; otherwise it panics let value: i64 = item.cast(); // type can also be inferred -let value = item.try_cast::().unwrap(); // 'try_cast' does not panic when the cast fails, but returns 'None' +let value = item.try_cast::()?; // 'try_cast' does not panic when the cast fails, but returns 'None' ``` Type Name diff --git a/doc/src/language/method.md b/doc/src/language/method.md index bdce0ed6..6b6b9314 100644 --- a/doc/src/language/method.md +++ b/doc/src/language/method.md @@ -3,6 +3,10 @@ Call Method as Function {{#include ../links.md}} + +First `&mut` Reference Parameter +------------------------------- + Property [getters/setters] and [methods][custom types] in a Rust custom type registered with the [`Engine`] can be called just like a regular function. In fact, like Rust, property getters/setters and object methods are registered as regular [functions] in Rhai that take a first `&mut` parameter. @@ -31,3 +35,23 @@ update(array[0]); // <- 'array[0]' is an expression returning a calculated val array[0].update(); // <- call in method-call style will update 'a' ``` + + +Encouraged Usage +---------------- + +Using a `&mut` first parameter is highly encouraged when using types that are expensive to clone, +even when the intention is not to mutate that argument, because it avoids cloning that argument value. + +For primary types that are cheap to clone, including `ImmutableString`, this is not necessary. + + +Avoid `&mut ImmutableString` +--------------------------- + +`ImmutableString`, Rhai internal [string] type, is an exception. + +`ImmutableString` is cheap to clone, but expensive to take a mutable reference (because the underlying +string must be cloned to make a private copy). + +Therefore, avoid using `&mut ImmutableString` unless the intention is to mutate it. diff --git a/doc/src/language/strings-chars.md b/doc/src/language/strings-chars.md index b6372240..acdb06f4 100644 --- a/doc/src/language/strings-chars.md +++ b/doc/src/language/strings-chars.md @@ -6,20 +6,25 @@ Strings and Characters String in Rhai contain any text sequence of valid Unicode characters. Internally strings are stored in UTF-8 encoding. -Strings can be built up from other strings and types via the `+` operator (provided by the [`MoreStringPackage`][packages] -but excluded if using a [raw `Engine`]). This is particularly useful when printing output. +Strings can be built up from other strings and types via the `+` operator +(provided by the [`MoreStringPackage`][packages] but excluded if using a [raw `Engine`]). +This is particularly useful when printing output. [`type_of()`] a string returns `"string"`. The maximum allowed length of a string can be controlled via `Engine::set_max_string_size` (see [maximum length of strings]). + The `ImmutableString` Type ------------------------- All strings in Rhai are implemented as `ImmutableString` (see [standard types]). -`ImmutableString` should be used in place of the standard Rust type `String` when registering functions. +`ImmutableString` should be used in place of the standard Rust type `String` when registering functions +because using `String` is very inefficient (the `String` must always be cloned). + +A alternative is to use `&str` which maps straight to `ImmutableString`. String and Character Literals @@ -59,13 +64,13 @@ Unicode characters. Individual characters within a Rhai string can also be replaced just as if the string is an array of Unicode characters. -In Rhai, there is also no separate concepts of `String` and `&str` as in Rust. +In Rhai, there are also no separate concepts of `String` and `&str` as in Rust. Immutable Strings ---------------- -Rhai strings are _immutable_ and can be shared. +Rhai use _immutable_ strings (type `ImmutableString`) and can be shared. Modifying a Rhai string actually causes it first to be cloned, and then the modification made to the copy. diff --git a/doc/src/rust/getters-setters.md b/doc/src/rust/getters-setters.md index b6afd6f0..4c44399d 100644 --- a/doc/src/rust/getters-setters.md +++ b/doc/src/rust/getters-setters.md @@ -5,6 +5,8 @@ Custom Type Getters and Setters A custom type can also expose members by registering `get` and/or `set` functions. +Getters and setters each take a `&mut` reference to the first parameter. + ```rust #[derive(Clone)] struct TestStruct { diff --git a/doc/src/rust/indexers.md b/doc/src/rust/indexers.md index 35f50ba2..d5905b44 100644 --- a/doc/src/rust/indexers.md +++ b/doc/src/rust/indexers.md @@ -7,6 +7,8 @@ A custom type can also expose an _indexer_ by registering an indexer function. A custom type with an indexer function defined can use the bracket '`[]`' notation to get a property value. +Like getters and setters, indexers take a `&mut` reference to the first parameter. + Indexers are disabled when the [`no_index`] feature is used. For efficiency reasons, indexers **cannot** be used to overload (i.e. override) built-in indexing operations for @@ -33,12 +35,10 @@ impl TestStruct { let mut engine = Engine::new(); -engine.register_type::(); - -engine.register_fn("new_ts", TestStruct::new); - -// Shorthand: engine.register_indexer_get_set(TestStruct::get_field, TestStruct::set_field); engine + .register_type::() + .register_fn("new_ts", TestStruct::new) + // Shorthand: .register_indexer_get_set(TestStruct::get_field, TestStruct::set_field); .register_indexer_get(TestStruct::get_field) .register_indexer_set(TestStruct::set_field); diff --git a/doc/src/rust/strings.md b/doc/src/rust/strings.md index 5b72fe6e..0d54d87b 100644 --- a/doc/src/rust/strings.md +++ b/doc/src/rust/strings.md @@ -3,11 +3,19 @@ {{#include ../links.md}} -Rust functions accepting parameters of `String` should use `&str` instead because it maps directly to [`ImmutableString`] -which is the type that Rhai uses to represent [strings] internally. + +`&str` Maps to `ImmutableString` +------------------------------- + +Rust functions accepting parameters of `String` should use `&str` instead because it maps directly to +[`ImmutableString`][string] which is the type that Rhai uses to represent [strings] internally. + +The parameter type `String` is discouraged because it involves converting an [`ImmutableString`] into a `String`. +Using `ImmutableString` or `&str` is much more efficient. +A common mistake made by novice Rhai users is to register functions with `String` parameters. ```rust -fn get_len1(s: String) -> i64 { s.len() as i64 } // <- Rhai will not find this function +fn get_len1(s: String) -> i64 { s.len() as i64 } // <- Rhai finds this function, but very inefficient fn get_len2(s: &str) -> i64 { s.len() as i64 } // <- Rhai finds this function fine fn get_len3(s: ImmutableString) -> i64 { s.len() as i64 } // <- the above is equivalent to this @@ -20,3 +28,23 @@ let len = engine.eval::("x.len1()")?; // error: function ' let len = engine.eval::("x.len2()")?; // works fine let len = engine.eval::("x.len3()")?; // works fine ``` + + +Avoid `&mut ImmutableString` +--------------------------- + +Rhai functions can take a first `&mut` parameter. Usually this is a good idea because it avoids +cloning of the argument (except for primary types where cloning is cheap), so its use is encouraged +even though there is no intention to ever mutate that argument. + +`ImmutableString` is an exception to this rule. While `ImmutableString` is cheap to clone (only +incrementing a reference count), taking a mutable reference to it involves making a private clone +of the underlying string because Rhai has no way to find out whether that parameter will be mutated. + +If the `ImmutableString` is not shared by any other variables, then Rhai just returns a mutable +reference to it since nobody else is watching! Otherwise a private copy is made first, +because other reference holders will not expect the `ImmutableString` to ever change +(it is supposed to be _immutable_). + +Therefore, avoid using `&mut ImmutableString` as the first parameter of a function unless you really +intend to mutate that string. Use `ImmutableString` instead. diff --git a/doc/src/start/builds/performance.md b/doc/src/start/builds/performance.md index 5db1edc1..dc47a4cc 100644 --- a/doc/src/start/builds/performance.md +++ b/doc/src/start/builds/performance.md @@ -29,3 +29,16 @@ Turning on [`no_float`], and [`only_i32`] makes the key [`Dynamic`] data type on while normally it can be up to 16 bytes (e.g. on x86/x64 CPU's) in order to hold an `i64` or `f64`. Making [`Dynamic`] small helps performance due to better cache efficiency. + + +Use `ImmutableString` +-------------------- + +Internally, Rhai uses _immutable_ [strings] instead of the Rust `String` type. This is mainly to avoid excessive +cloning when passing function arguments. + +The encapsulated immutable string type is `ImmutableString`. It is cheap to clone (just an `Rc` or `Arc` reference +count increment depending on the [`sync`] feature). + +Therefore, functions taking `String` parameters should use `ImmutableString` or `&str` (which maps to `ImmutableString`) +for the best performance with Rhai. diff --git a/doc/src/start/examples/rust.md b/doc/src/start/examples/rust.md index 412589e5..b7b6a5a5 100644 --- a/doc/src/start/examples/rust.md +++ b/doc/src/start/examples/rust.md @@ -10,7 +10,7 @@ A number of examples can be found in the `examples` folder: | [`arrays_and_structs`](https://github.com/jonathandturner/rhai/tree/master/examples/arrays_and_structs.rs) | Shows how to register a custom Rust type and using [arrays] on it. | | [`custom_types_and_methods`](https://github.com/jonathandturner/rhai/tree/master/examples/custom_types_and_methods.rs) | Shows how to register a custom Rust type and methods for it. | | [`hello`](https://github.com/jonathandturner/rhai/tree/master/examples/hello.rs) | Simple example that evaluates an expression and prints the result. | -| [`no_std`](https://github.com/jonathandturner/rhai/tree/master/examples/no_std.rs) | Example to test out `no-std` builds. | +| [`no_std`](https://github.com/jonathandturner/rhai/tree/master/examples/no_std.rs) | Example to test out `no-std` builds.
The [`no_std`] feature is required to build in `no-std`. | | [`reuse_scope`](https://github.com/jonathandturner/rhai/tree/master/examples/reuse_scope.rs) | Evaluates two pieces of code in separate runs, but using a common [`Scope`]. | | [`rhai_runner`](https://github.com/jonathandturner/rhai/tree/master/examples/rhai_runner.rs) | Runs each filename passed to it as a Rhai script. | | [`serde`](https://github.com/jonathandturner/rhai/tree/master/examples/serde.rs) | Example to serialize and deserialize Rust types with [`serde`](https://crates.io/crates/serde).
The [`serde`] feature is required to run. |