Use the Low-Level API to Register a Rust Function

When a native Rust function is registered with an Engine using the Engine::register_XXX API, Rhai transparently converts all function arguments from Dynamic into the correct types before calling the function.

For more power and flexibility, there is a low-level API to work directly with Dynamic values without the conversions.

Raw Function Registration

The Engine::register_raw_fn method is marked volatile, meaning that it may be changed without warning.

If this is acceptable, then using this method to register a Rust function opens up more opportunities.

The function signature includes the current native call context which exposes the current Engine, among others, so the Rust function can recursively call methods on the same Engine.


#![allow(unused)]
fn main() {
engine.register_raw_fn(
    "increment_by",                                         // function name
    &[                                                      // a slice containing parameter types
        std::any::TypeId::of::<i64>(),                      // type of first parameter
        std::any::TypeId::of::<i64>()                       // type of second parameter
    ],
    |context, args| {                                       // fixed function signature
        // Arguments are guaranteed to be correct in number and of the correct types.

        // But remember this is Rust, so you can keep only one mutable reference at any one time!
        // Therefore, get a '&mut' reference to the first argument _last_.
        // Alternatively, use `args.split_first_mut()` etc. to split the slice first.

        let y = *args[1].read_lock::<i64>().unwrap();       // get a reference to the second argument
                                                            // then copy it because it is a primary type

        let y = std::mem::take(args[1]).cast::<i64>();      // alternatively, directly 'consume' it

        let y = args[1].as_int().unwrap();                  // alternatively, use 'as_xxx()'

        let x = args[0].write_lock::<i64>().unwrap();       // get a '&mut' reference to the first argument

        *x += y;                                            // perform the action

        Ok(Dynamic::UNIT)                                   // must be 'Result<Dynamic, Box<EvalAltResult>>'
    }
);

// The above is the same as (in fact, internally they are equivalent):

engine.register_fn("increment_by", |x: &mut i64, y: i64| *x += y);
}

Function Signature

The function signature passed to Engine::register_raw_fn takes the following form:

Fn(context: NativeCallContext, args: &mut [&mut Dynamic])
-> Result<T, Box<EvalAltResult>> + 'static

where:

ParameterTypeDescription
Timpl Clonereturn type of the function
contextNativeCallContextthe current native call context
args&mut [&mut Dynamic]a slice containing &mut references to Dynamic values.
The slice is guaranteed to contain enough arguments of the correct types.

Return value

The return value is the result of the function call.

Remember, in Rhai, all arguments except the first one are always passed by value (i.e. cloned). Therefore, it is unnecessary to ever mutate any argument except the first one, as all mutations will be on the cloned copy.

Extract The First &mut Argument (If Any)

To extract the first &mut argument passed by reference from the args parameter (&mut [&mut Dynamic]), use the following to get a mutable reference to the underlying value:


#![allow(unused)]
fn main() {
let value: &mut T = &mut *args[0].write_lock::<T>().unwrap();

*value = ...    // overwrite the existing value of the first `&mut` parameter
}

When there is a mutable reference to the first &mut argument, there can be no other immutable references to args, otherwise the Rust borrow checker will complain.

Therefore, always extract the mutable reference last, after all other arguments are taken.

Extract Other Pass-By-Value Arguments

To extract an argument passed by value from the args parameter (&mut [&mut Dynamic]), use the following:

Argument typeAccess (n = argument position)ResultOriginal value
INTargs[n].as_int().unwrap()INTuntouched
FLOATargs[n].as_float().unwrap()FLOATuntouched
Decimalargs[n].as_decimal().unwrap()Decimaluntouched
boolargs[n].as_bool().unwrap()booluntouched
charargs[n].as_char().unwrap()charuntouched
()args[n].as_unit().unwrap()()untouched
String&*args[n].read_lock::<ImmutableString>().unwrap()&ImmutableStringuntouched
String (consumed)std::mem::take(args[n]).cast::<ImmutableString>()ImmutableString()
Custom type&*args[n].read_lock::<T>().unwrap()&Tuntouched
Custom type (consumed)std::mem::take(args[n]).cast::<T>()T()

Example – Passing a Callback to a Rust Function

The low-level API is useful when there is a need to interact with the scripting Engine within a function.

The following example registers a function that takes a function pointer as an argument, then calls it within the same Engine. This way, a callback function can be provided to a native Rust function.


#![allow(unused)]
fn main() {
use rhai::{Engine, FnPtr};

let mut engine = Engine::new();

// Register a Rust function
engine.register_raw_fn(
    "bar",
    &[
        std::any::TypeId::of::<i64>(),                      // parameter types
        std::any::TypeId::of::<FnPtr>(),
        std::any::TypeId::of::<i64>(),
    ],
    |context, args| {
        // 'args' is guaranteed to contain enough arguments of the correct types

        let fp = std::mem::take(args[1]).cast::<FnPtr>();   // 2nd argument - function pointer
        let value = std::mem::take(args[2]);                // 3rd argument - function argument
        let this_ptr = args.get_mut(0).unwrap();            // 1st argument - this pointer

        // Use 'FnPtr::call_dynamic' to call the function pointer.
        // Beware, private script-defined functions will not be found.
        fp.call_dynamic(&context, Some(this_ptr), [value])
    },
);

let result = engine.eval::<i64>(
r#"
    fn foo(x) { this += x; }        // script-defined function 'foo'

    let x = 41;                     // object
    x.bar(Fn("foo"), 1);            // pass 'foo' as function pointer
    x
"#)?;
}

TL;DR – Why read_lock and write_lock

The Dynamic API that casts it to a reference to a particular data type is read_lock (for an immutable reference) and write_lock (for a mutable reference).

As the naming shows, something is locked in order to allow this access, and that something is a shared value created by capturing variables from closures.

Shared values are implemented as Rc<RefCell<Dynamic>> (Arc<RwLock<Dynamic>> under sync).

If the value is not a shared value, or if running under no_closure where there is no capturing, this API de-sugars to a simple reference cast.

In other words, there is no locking and reference counting overhead for the vast majority of non-shared values.

If the value is a shared value, then it is first locked and the returned lock guard allows access to the underlying value in the specified type.

Hold Multiple References

In order to access a value argument that is expensive to clone while holding a mutable reference to the first argument, use one of the following tactics:

  1. if it is a primary type other than string, use as_xxx() as above

  2. directly consume that argument via std::mem::take as above

  3. use split_first_mut to partition the slice:


#![allow(unused)]
fn main() {
// Partition the slice
let (first, rest) = args.split_first_mut().unwrap();

// Mutable reference to the first parameter, of type '&mut A'
let this_ptr = &mut *first.write_lock::<A>().unwrap();

// Immutable reference to the second value parameter, of type '&B'
// This can be mutable but there is no point because the parameter is passed by value
let value_ref = &*rest[0].read_lock::<B>().unwrap();
}