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

Running a script

I never take over your program. A script runs when you say so, for as long as you say, and then hands control back.

The short way

script.run_all(10_000)?;

run_all runs the main program and every thread it starts, in turns, on the calling thread, until all of them are done. It’s the right thing for a command-line tool or a test. A program with a screen to draw wants more say than that.

A frame at a time

    let mut main = Some(script.start());
    let mut others: Vec<Thread> = Vec::new();
    while main.is_some() || !others.is_empty() {
        // One frame: every thread gets a slice of 500 loop passes and calls.
        if let Some(thread) = &mut main {
            match thread.run(500)? {
                Status::Finished => main = None,
                // Out of budget, waiting on a host function, or stopped by
                // the debugger: all mean "come back to me next frame".
                Status::Yielded | Status::Waiting | Status::Stopped(_) => {}
            }
        }
        // The others the same way. One that has finished is dropped.
        let mut at = 0;
        while at < others.len() {
            match others[at].run(500)? {
                Status::Finished => drop(others.swap_remove(at)),
                _ => at += 1,
            }
        }
        // Threads that ASYNC calls started during this frame.
        others.extend(script.take_spawned());

        // ...draw the frame, read the controller, play the sound...
    }

script.start() gives you the main program as a thread. A thread of mine is a stack and a place in the code, far lighter than one of the operating system’s, and it does nothing until you call run.

run(budget) runs it until one of these happens:

It returnsBecauseYou
Status::FinishedThe thread reached its endDrop it
Status::YieldedThe budget ran outRun it again next frame
Status::WaitingA host function is waiting for somethingRun it again later; it asks again each time
Status::Stopped(…)The debugger stopped itShow the debugger, then run it again
an errorThe script made a mistakeShow script.report(&error)

The budget counts loop passes and function calls, which are the only ways a script can run for long. A script with an endless loop costs you one budget per frame and nothing more. It can’t hang your program.

Waiting without blocking

A host function must never sit and wait: it would hold up everything else. When the answer isn’t ready, it returns a poll instead.

    // A function that has to wait does not block. It gives a poll, and the
    // thread that called it steps aside until the poll has an answer.
    let door = door_open.clone();
    engine.function("/// Waits until the door is open.\nFUNCTION wait_for_door()", move |_| {
        let door = door.clone();
        Reply::wait(move || door.load(Ordering::Relaxed).then(|| Value::string("open")))
    })?;

The thread that called it steps aside (Status::Waiting), and each time you run it again I ask the poll. None means “not yet”. Some(value) becomes what the function returned, and the script carries on from there.

To the script, it just looks like a function that took a while:

PRINT "the door is", wait_for_door()

This is how you write DELAY, wait for a key, wait for a download, or wait for an animation to finish.

Sometimes blocking is what you have: a call into another part of your program that waits for the next frame, say. Then wrap the part that blocks in call.outside, so the rest of the script isn’t kept waiting on it:

engine.function("FUNCTION next_frame()", |call| {
    call.outside(|| frames.wait_for_next());
    Value::Null
})?;

Threads

Every ASYNC call in a script makes another thread. They pile up until you ask for them with script.take_spawned(), and then they’re yours to run however you like:

  • In turns, on one thread, as the loop above does. Simple, and all a great many programs need.
  • On a pool of real threads. A Thread can be sent to another core and run there. Scripts that share variables are safe to run side by side: every read and write of a shared value is a single step, and ATOMIC variables make a read-change-write one step too.

Nothing about a script says which you chose. The same ASYNC and RESOLVE work either way.

A host function can end the thread that called it by returning Reply::end(): no error, it just stops there. To stop one thread from outside, call thread.cancel(). To stop every thread of a script, as when the main program ends and the rest shouldn’t outlive it, call script.cancel_all(). run_main is run_all with that built in.

In a browser

Built for the web in the ordinary way, I run on the page’s single thread and script threads take turns. This book works that way.

For real parallel threads in a browser, build with WebAssembly threads switched on and run script threads on Web Workers that share memory. I need nothing special for it — I start no threads and never sleep — but keep script threads off the page’s main thread: a script thread may wait briefly for a value another is using, and a browser doesn’t let its main thread wait. The webtest/ folder is a working example that runs four threads on four workers.

Cleaning up

You don’t free anything, and neither do scripts: see Who cleans up?. Values are freed the moment nothing holds them.

The one thing worth knowing as a host: values that hold each other are found and freed in an occasional pass, which pauses the script’s threads for about a millisecond per ten thousand live lists, maps and objects. By default I choose the moment. If you’d rather choose it — between frames, on a loading screen — say so:

script.collect_by_hand(true);
// ...then, when it suits you:
if script.collect_due() {
    script.collect();
}

Anything you are holding is never freed by this, nor anything it leads to. You don’t have to tell me what you’re holding.