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 returns | Because | You |
|---|---|---|
Status::Finished | The thread reached its end | Drop it |
Status::Yielded | The budget ran out | Run it again next frame |
Status::Waiting | A host function is waiting for something | Run it again later; it asks again each time |
Status::Stopped(…) | The debugger stopped it | Show the debugger, then run it again |
| an error | The script made a mistake | Show 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
Threadcan 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, andATOMICvariables 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.