Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Sharing what we know

Scripts and the host live in the same memory. Passing something between us is direct: for most things, we both end up holding the very same value.

Values

Everything a script can hold is a Value.

A script’sIs a Value::You get at it with
Whole numberInteger(i64)the number
Number with a fractionFloat(f64)the number
TextStrto_string()
NULLNull
CoordinateCoordinate(x, y) and friendscomponent(n)
ListArrayread() / write() on what it holds
MapMapread() / write() on what it holds
An object of a TYPEObjectget("field") / set("field", value)
Function or blockFunction, Closurescript.call(&value, args)
ErrorErrorits code and values

Make them with Value::Integer(5), Value::string("hi"), Value::array(vec![…]) and Value::map(…). value.to_string() gives what PRINT would show, and value.type_name() gives words for a message: “an array”, “a string”.

A script’s variables

    // A variable is read and written by name. In a namespace it has its
    // full name: `army.recruits`.
    let score = script.global("score");
    assert!(matches!(score, Some(Value::Integer(7))));
    script.set_global("score", Value::Integer(8));

    // An object's fields are read and written by name too.
    if let Some(Value::Object(hero)) = script.global("ada") {
        let hp = hero.get("hp");
        assert!(matches!(hp, Some(Value::Integer(10))));
        hero.set("hp", Value::Integer(99));
    }

One thing, two holders

This is the important one. Numbers, text and coordinates are copied when they’re passed. Lists, maps and objects are shared. If a script hands you a list and you keep it, you and the script are holding the same list.

    // An array, a map and an object are shared, not copied. Keep the one a
    // script hands over and both sides are looking at the same thing.
    let tracked: Arc<Mutex<Vec<Value>>> = Arc::default();
    let keep = tracked.clone();
    let track = "/// Has the engine move this position every frame.\nFUNCTION track(position)";
    engine.function(track, move |call| {
        keep.lock().unwrap().push(call.args[0].clone());
        Value::Null
    })?;
spot = [0, 0]
track(spot)

From then on, whatever you write into it, the script sees:

    // Each frame, the engine writes where the thing has got to. The script
    // sees it the next time it reads `spot`.
    for position in tracked.lock().unwrap().iter() {
        if let Value::Array(numbers) = position {
            let mut numbers = numbers.write().unwrap();
            numbers[0] = Value::Integer(120);
            numbers[1] = Value::Integer(64);
        }
    }

No message was sent and nothing was copied back. The next time the script reads spot(0), it’s 120.

This is how an engine keeps a script’s view of something up to date without the script asking: a position that physics moves, a list of who’s in a room, a table of high scores. Give the script the list (or let it give you one), keep hold of it, and write into it.

Each read and write is a single safe step, even with script threads running on other cores. Take the lock, do one thing, let it go: don’t hold a write() across a call back into a script.

Calling into a script

    // Any function of the script can be called from outside, by name.
    let report = script.function("report").expect("the script declares it");
    let mut thread = script.call(&report, Vec::new())?;
    while thread.run(10_000)? != Status::Finished {}

script.function finds a function by its full name: update, Army.recruit, Unit::Hit. What you get stays good even if the function is replaced later by live coding: the next call runs the new one. This is how an engine calls a script’s update every frame.

Between two machines

Two copies of me that loaded the same scripts can send values to each other. script.to_wire(&value) gives bytes and script.from_wire(&bytes) turns them back. Carrying the bytes is your business.

Nearly everything crosses, including functions, blocks with the locals they kept, and ASYNC calls that haven’t started — which is how you ask another machine to run something. What arrives is always a copy. Compare script.fingerprint() on both sides when they connect: it only matches if both loaded the same code.