194 lines
6.6 KiB
Markdown
194 lines
6.6 KiB
Markdown
`Dynamic` Parameters in Rust Functions
|
|
======================================
|
|
|
|
{{#include ../links.md}}
|
|
|
|
It is possible for Rust functions to contain parameters of type [`Dynamic`].
|
|
|
|
A [`Dynamic`] value can hold any clonable type.
|
|
|
|
```admonish question.small "Trivia"
|
|
|
|
The `push` method of an [array] is implemented as follows (minus code for [safety] protection
|
|
against [over-sized arrays][maximum size of arrays]), allowing the function to be called with
|
|
all item types.
|
|
|
|
~~~rust
|
|
// 'item: Dynamic' matches all data types
|
|
fn push(array: &mut Array, item: Dynamic) {
|
|
array.push(item);
|
|
}
|
|
~~~
|
|
```
|
|
|
|
|
|
Precedence
|
|
----------
|
|
|
|
Any parameter in a registered Rust function with a specific type has higher precedence over
|
|
[`Dynamic`], so it is important to understand which _version_ of a function will be used.
|
|
|
|
Parameter matching starts from the left to the right.
|
|
Candidate functions will be matched in order of parameter types.
|
|
|
|
Therefore, always leave [`Dynamic`] parameters (up to 16, see below) as far to the right as possible.
|
|
|
|
```rust
|
|
use rhai::{Engine, Dynamic};
|
|
|
|
// Different versions of the same function 'foo'
|
|
// will be matched in the following order.
|
|
|
|
fn foo1(x: i64, y: &str, z: bool) { }
|
|
|
|
fn foo2(x: i64, y: &str, z: Dynamic) { }
|
|
|
|
fn foo3(x: i64, y: Dynamic, z: bool) { }
|
|
|
|
fn foo4(x: i64, y: Dynamic, z: Dynamic) { }
|
|
|
|
fn foo5(x: Dynamic, y: &str, z: bool) { }
|
|
|
|
fn foo6(x: Dynamic, y: &str, z: Dynamic) { }
|
|
|
|
fn foo7(x: Dynamic, y: Dynamic, z: bool) { }
|
|
|
|
fn foo8(x: Dynamic, y: Dynamic, z: Dynamic) { }
|
|
|
|
let mut engine = Engine::new();
|
|
|
|
// Register all functions under the same name (order does not matter)
|
|
|
|
engine.register_fn("foo", foo5)
|
|
.register_fn("foo", foo7)
|
|
.register_fn("foo", foo2)
|
|
.register_fn("foo", foo8)
|
|
.register_fn("foo", foo1)
|
|
.register_fn("foo", foo3)
|
|
.register_fn("foo", foo6)
|
|
.register_fn("foo", foo4);
|
|
```
|
|
|
|
|
|
~~~admonish warning "Only the right-most 16 parameters can be `Dynamic`"
|
|
|
|
The number of parameter permutations goes up exponentially, and therefore there is a realistic limit
|
|
of 16 parameters allowed to be [`Dynamic`], counting from the _right-most side_.
|
|
|
|
For example, Rhai will not find the following function – Oh! and those 16 parameters to the right
|
|
certainly have nothing to do with it!
|
|
|
|
```rust
|
|
// The 'd' parameter counts 17th from the right!
|
|
fn weird(a: i64, d: Dynamic, x1: i64, x2: i64, x3: i64, x4: i64,
|
|
x5: i64, x6: i64, x7: i64, x8: i64,
|
|
x9: i64, x10: i64, x11: i64, x12: i64,
|
|
x13: i64, x14: i64, x15: i64, x16: i64) {
|
|
|
|
// ... do something unspeakably evil with all those parameters ...
|
|
}
|
|
```
|
|
~~~
|
|
|
|
|
|
TL;DR
|
|
-----
|
|
|
|
```admonish question "How is this implemented?"
|
|
|
|
#### Hash lookup
|
|
|
|
Since functions in Rhai can be [overloaded][function overloading], Rhai uses a single _hash_ number
|
|
to quickly lookup the actual function, based on argument types.
|
|
|
|
For each function call, a hash is calculated from:
|
|
|
|
1. the function's [namespace][function namespace], if any,
|
|
2. the function's name,
|
|
3. number of arguments (its _arity_),
|
|
4. unique ID of the type of each argument, if any.
|
|
|
|
The correct function is then obtained via a simple hash lookup.
|
|
|
|
#### Limitations
|
|
|
|
This method is _fast_, but at the expense of flexibility (such as multiple argument types that must
|
|
map to a single version). That is because each type has a different ID, and thus they calculate to
|
|
different hash numbers.
|
|
|
|
This is the reason why [generic functions](generic.md) must be expanded into concrete types.
|
|
|
|
The type ID of [`Dynamic`] is different from any other type, but it must match all types seamlessly.
|
|
Needless to say, this creates a slight problem.
|
|
|
|
#### Trying combinations
|
|
|
|
If the combined hash calculated from the actual argument type ID's is not found, then the [`Engine`]
|
|
calculates hashes for different _combinations_ of argument types and [`Dynamic`], systematically
|
|
replacing different arguments with [`Dynamic`] _starting from the right-most parameter_.
|
|
|
|
Thus, assuming a three-argument function call:
|
|
|
|
~~~rust
|
|
foo(42, "hello", true);
|
|
~~~
|
|
|
|
The following hashes will be calculated, in order.
|
|
They will be _all different_.
|
|
|
|
| Order | Hash calculation method |
|
|
| :---: | --------------------------------------------------- |
|
|
| 1 | `foo` + 3 + `i64` + `string` + `bool` |
|
|
| 2 | `foo` + 3 + `i64` + `string` + [`Dynamic`] |
|
|
| 3 | `foo` + 3 + `i64` + [`Dynamic`] + `bool` |
|
|
| 4 | `foo` + 3 + `i64` + [`Dynamic`] + [`Dynamic`] |
|
|
| 5 | `foo` + 3 + [`Dynamic`] + `string` + `bool` |
|
|
| 6 | `foo` + 3 + [`Dynamic`] + `string` + [`Dynamic`] |
|
|
| 7 | `foo` + 3 + [`Dynamic`] + [`Dynamic`] + `bool` |
|
|
| 8 | `foo` + 3 + [`Dynamic`] + [`Dynamic`] + [`Dynamic`] |
|
|
|
|
Therefore, the version with all the correct parameter types will always be found first if it exists.
|
|
|
|
At soon as a hash is found, the process stops.
|
|
|
|
Otherwise, it goes on for up to 16 arguments, or at most 65,536 tries.
|
|
That's where the 16 parameters limit comes from.
|
|
```
|
|
|
|
```admonish question "What?! It calculates 65,536 hashes for each function call???!!!"
|
|
|
|
Of course not. Don't be silly.
|
|
|
|
#### Not every function has 16 parameters
|
|
|
|
Studies have repeatedly shown that most functions accept few parameters, with the mean between
|
|
2-3 parameters per function. Functions with more than 5 parameters are rare in normal code bases.
|
|
If at all, they are usually [closures] that _capture_ lots of external variables, bumping up the
|
|
parameter count; but [closures] are always script-defined and thus all parameters are already
|
|
[`Dynamic`].
|
|
|
|
In fact, you have a bigger problem if you write such a function that you need to call regularly.
|
|
It would be far more efficient to group those parameters into [object maps].
|
|
|
|
#### Caching to the rescue
|
|
|
|
Function hashes are _cached_, so this process only happens _once_, and only up to the number of
|
|
rounds for the correct function to be found.
|
|
|
|
If not, then yes, it will calculate up to 2<sup>_n_</sup> hashes where _n_ is the number of
|
|
arguments (up to 16). But again, this will only be done _once_ for that particular
|
|
combination of argument types.
|
|
```
|
|
|
|
```admonish danger "But then... beware module functions"
|
|
|
|
The functions resolution _cache_ resides only in the [global namespace][function namespace].
|
|
This is a limitation.
|
|
|
|
Therefore, calls to functions in an [`import`]ed [module] (i.e. _qualified_ with
|
|
a [namespace][function namespace] path) do not have the benefit of a cache.
|
|
|
|
Thus, up to 2<sup>_n_</sup> hashes are calculated during _every_ function call.
|
|
This is unlikely to cause a performance issue since most functions accept only a few parameters.
|
|
```
|