This repository has been archived on 2025-08-04. You can view files and clone it, but cannot push or open issues or pull requests.
rhaj/rhai_engine/rhaibook/rust/indexers.md
2025-04-03 09:18:05 +02:00

7.7 KiB

Custom Type Indexers

{{#include ../links.md}}

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/set a property value at a particular index:

object [ index ]

object [ index ] = value ;

The [Elvis notation][elvis] is similar except that it returns [()] if the object itself is [()].

// returns () if object is ()
object ?[ index ]

// no action if object is ()
object ?[ index ] = value ;

Like property [getters/setters], indexers take a &mut reference to the first parameter.

They also take an additional parameter of any type that serves as the index within brackets.

Indexers are disabled when the [no_index] and [no_object] features are used together.

Engine API Function signature(s)
(T: Clone = custom type,
X: Clone = index type,
V: Clone = data type)
Can mutate T?
register_indexer_get Fn(&mut T, X) -> V yes, but not advised
register_indexer_set Fn(&mut T, X, V) yes
register_indexer_get_set getter: Fn(&mut T, X) -> V
setter: Fn(&mut T, X, V)
yes, but not advised in getter

Rhai does NOT support normal references (i.e. `&T`) as parameters.
All references must be mutable (i.e. `&mut T`).

By convention, index getters are not supposed to mutate the [custom type],
although there is nothing that prevents this mutation.

For [fallible][fallible function] indexers, it is customary to return
`EvalAltResult::ErrorIndexNotFound` when called with an invalid index value.

Cannot Override Arrays, BLOB's, Object Maps, Strings and Integers


They can be defined in a [plugin module], but will be ignored.

For efficiency reasons, indexers cannot be used to overload (i.e. override) built-in indexing operations for [arrays], [object maps], [strings] and integers (acting as [bit-field] operation).

The following types have built-in indexer implementations that are fast and efficient.

Type Index type Return type Description
[Array] INT [Dynamic] access a particular element inside the [array]
[Blob] INT INT access a particular byte value inside the [BLOB]
[Map] [ImmutableString],
String, &str
[Dynamic] access a particular property inside the [object map]
[ImmutableString],
String, &str
INT [character] access a particular [character] inside the [string]
INT INT boolean access a particular bit inside the integer number as a [bit-field]
INT [range] INT access a particular range of bits inside the integer number as a [bit-field]

In general, it is a bad idea to overload indexers for any of the [standard types] supported
internally by Rhai, since built-in indexers may be added in future versions.

Examples

#[derive(Debug, Clone)]
struct TestStruct {
    fields: Vec<i64>
}

impl TestStruct {
    // Remember &mut must be used even for getters
    fn get_field(&mut self, index: String) -> i64 {
        self.fields[index.len()]
    }
    fn set_field(&mut self, index: String, value: i64) {
        self.fields[index.len()] = value
    }

    fn new() -> Self {
        Self { fields: vec![1, 2, 3, 4, 5] }
    }
}

let mut engine = Engine::new();

engine.register_type::<TestStruct>()
      .register_fn("new_ts", TestStruct::new)
      // Short-hand: .register_indexer_get_set(TestStruct::get_field, TestStruct::set_field);
      .register_indexer_get(TestStruct::get_field)
      .register_indexer_set(TestStruct::set_field);

let result = engine.eval::<i64>(
r#"
    let a = new_ts();
    a["xyz"] = 42;                  // these indexers use strings
    a["xyz"]                        // as the index type
"#)?;

println!("Answer: {result}");       // prints 42

Convention for Negative Index

If the indexer takes a signed integer as an index (e.g. the standard INT type), care should be taken to handle negative values passed as the index.

It is a standard API convention for Rhai to assume that an index position counts backwards from the end if it is negative.

-1 as an index usually refers to the last item, -2 the second to last item, and so on.

Therefore, negative index values go from -1 (last item) to -length (first item).

A typical implementation for negative index values is:

// The following assumes:
//   'index' is 'INT', 'items: usize' is the number of elements
let actual_index = if index < 0 {
    index.checked_abs().map_or(0, |n| items - (n as usize).min(items))
} else {
    index as usize
};

The end of a data type can be interpreted creatively. For example, in an integer used as a [bit-field], the start is the least-significant-bit (LSB) while the end is the most-significant-bit (MSB).

Convention for Range Index


By convention, negative values are _not_ interpreted specially in indexers for [ranges].

It is very common for [ranges] to be used as indexer parameters via the types std::ops::Range<INT> (exclusive) and std::ops::RangeInclusive<INT> (inclusive).

One complication is that two versions of the same indexer must be defined to support exclusive and inclusive [ranges] respectively.

use std::ops::{Range, RangeInclusive};

let mut engine = Engine::new();

engine
    /// Version of indexer that accepts an exclusive range
    .register_indexer_get_set(
        |obj: &mut TestStruct, range: Range<i64>| -> bool { ... },
        |obj: &mut TestStruct, range: Range<i64>, value: bool| { ... },
    )
    /// Version of indexer that accepts an inclusive range
    .register_indexer_get_set(
        |obj: &mut TestStruct, range: RangeInclusive<i64>| -> bool { ... },
        |obj: &mut TestStruct, range: RangeInclusive<i64>, value: bool| { ... },
    );

engine.run(
"
    let obj = new_ts();

    let x = obj[0..12];             // use exclusive range

    obj[0..=11] = !x;               // use inclusive range
")?;