The glsp Crate
Let's take another look at the skeleton project from the Overview chapter:
use glsp::prelude::*; fn main() { let runtime = Runtime::new(); runtime.run(|| { glsp::load("main.glsp")?; Ok(()) }); }
The main player here is the Runtime type.
It owns all of the data required to run a single GameLisp instance: a garbage‑collected heap,
a symbol registry and a call stack, among other things. When the Runtime is dropped, all of that
data is automatically freed.
There can be many simultaneous Runtimes in a program, and a Runtime can be created
on any thread, but Runtimes (and handles to the data inside them) can never be moved from one
thread to another. Although GameLisp can be gracefully integrated into a multithreaded program,
each individual GameLisp runtime is entirely single-threaded.
The Active Runtime
Unusually for a Rust library, the glsp crate uses thread_local storage internally.
This means that there's no need to pass around a context object (aka "God object"), because that
would make the crate much less convenient to use.
Instead, each thread has a hidden thread_local pointer to its active Runtime. The rt.run(...)
method sets rt to be the active Runtime, executes an arbitrary closure, and then restores the
Runtime which was previously active, if any.
The glsp crate contains a large number of free functions, like glsp::sym, which
manipulate the active Runtime via that thread_local pointer. If these functions are called
when no Runtime is active, they panic.
Most of the functions closely imitate an equivalent function which is built in to GameLisp.
For example, glsp::sym is equivalent to the sym function, and glsp::load
is equivalent to load.
Most games will have no need for multiple Runtimes. The simplest way to use GameLisp is to
create a single Runtime at program start, and immediately make it active by calling its
run() method, passing in a closure which lasts for the entire duration of your program's
main function.
use glsp::prelude::*; fn main() { let runtime = Runtime::new(); runtime.run(|| { //...your entire program executes within this scope... Ok(()) }); }
Types
To introduce a few of the crate's most important types:
-
Valis the enum which represents a GameLisp value. Each variant corresponds to one of the sixteen primitive types. -
Symrepresents a GameLisp symbol. It's a smallCopytype which just wraps au32identifier. -
Rootis a smart pointer which refers to something stored on the garbage-collected heap. It points to a struct which represents one of GameLisp's primitive types, such asRoot<Arr>for an array orRoot<Coro>for a coroutine. Such types can only be accessed using aRoot; your program will never take direct ownership of anArror aCoro. -
GFnis the name for GameLisp'sfnprimitive type, to avoid confusion with Rust'sFntrait. Similarly, the GameLisp typeiteris represented by the Rust typeGIter.
Moving Data Between Runtimes
Types which represent GameLisp data, like Root, Val and Sym, are closely linked to the
specific Runtime in which they were constructed. You shouldn't attempt to manipulate GameLisp
data when there's no active Runtime, and you should never move GameLisp data from one Runtime
to another.
For example, if you return a Val from Runtime::run, and then attempt to print it, your
program will panic. If you construct a symbol in one Runtime, and attempt to compare it
to a symbol from a different Runtime, the comparison may return a false positive or false
negative.
Moving data between Runtimes is always memory-safe, but the results are otherwise undefined.
Under some rare circumstances, in order to preserve memory safety, GameLisp may be forced to
abort the process!
Generic Conversions
Functions and methods in the glsp crate tend to be highly generic, to keep manual type
conversions to a minimum.
For example, this is the signature of the glsp::global function, which is designed
to imitate GameLisp's (global) function:
#![allow(unused_variables)] fn main() { pub fn global<S, T>(s: S) -> GResult<T> where S: ToSym, T: FromVal, }
The ToSym trait converts something to a symbol, and the FromVal trait is implemented by
anything which can be fallibly converted from a Val. In practice, this means that the function
can be called like this...
#![allow(unused_variables)] fn main() { let frames: u32 = glsp::global("frames")?; }
...rather than needing a mess of explicit type conversions:
#![allow(unused_variables)] fn main() { let frames = u32::from_val(&glsp::global(glsp::sym("frames")?)?)?; }
The only downside is that with so many generic types, Rust's type inference will sometimes get confused. Rust doesn't yet allow you to put type annotations wherever you please, so under those circumstances, you'll usually need to introduce a temporary local variable with an explicit type.
In particular, for functions like glsp::call which have a generic return value, the type of
the return value must be specified explicitly, even when it's discarded.
#![allow(unused_variables)] fn main() { //an error glsp::call(&my_gfn, &(1, 2, 3))?; //correct let _: Val = glsp::call(&my_gfn, &(1, 2, 3))?; }
Comprehensive Coverage
The glsp crate aims to be comprehensive: if it's possible to achieve something in GameLisp code,
it should also be possible to achieve it in Rust. It's usually possible for any GameLisp code to be
translated into (much uglier) Rust code line-by-line.
(for plant in plants
(when (< [plant 'height] 100)
(.grow plant 10)))
#![allow(unused_variables)] fn main() { let plants: Root<Arr> = glsp::global("plants")?; for val in plants.iter() { let plant = Root::<Obj>::from_val(&val)?; let height: i32 = plant.get("height")?; if height < 100 { let _: Val = plant.call("grow", &(10,))?; } } }
The Prelude
Importing the glsp::prelude::* module will pull in all of the most commonly-used names
from the glsp crate, except for the free functions: glsp::bind_global() doesn't become
bind_global().
I can't overstate how much more convenient this is, compared to manually adding and removing dozens of imports to every file. If name collisions occur, they can be disambiguated via renaming:
#![allow(unused_variables)] fn main() { use glsp::prelude::*; use num::Num as NumTrait; use error_chain::bail as ec_bail; }
If glob imports aren't to your taste, naturally there's nothing stopping you from importing names individually instead.
Sandboxing
Runtime::new() creates a default Runtime. It's also possible to use the
RuntimeBuilder struct to configure a Runtime before creating it.
Currently, the only configuration setting is sandboxed, which defaults to false. A sandboxed
Runtime does not provide any of the built-in GameLisp functions which access the filesystem -
namely load, include and require.
Untrusted GameLisp code can get up to all sorts of mischief even without filesystem access, so
you should still proceed with great caution when running it.
Output Streams
By default, prn will print its output to std::io::Stdout, and
eprn will print its output to std::io::Stderr.
It's possible to replace those streams with an arbitrary std::io::Write type using the
functions glsp::set_pr_writer and glsp::set_epr_writer. For example, to
discard both streams:
#![allow(unused_variables)] fn main() { use std::io::sink; glsp::set_pr_writer(Box::new(sink())); glsp::set_epr_writer(Box::new(sink())); }
Or to send output to a log file and also to the standard output streams:
#![allow(unused_variables)] fn main() { use std::io::{self, stdout, stderr, Write}; struct Tee<A: Write, B: Write>(A, B); impl<A: Write, B: Write> Write for Tee<A, B> { fn write(&mut self, buf: &[u8]) -> io::Result<usize> { self.0.write_all(buf).and_then(|_| self.1.write_all(buf))?; Ok(buf.len()) } fn flush(&mut self) -> io::Result<()> { self.0.flush().and_then(|_| self.1.flush()) } } //defining a cloneable file handle is left as an exercise for the reader let log_file = SharedFile::new("log.txt"); glsp::set_pr_writer(Box::new(Tee(log_file.clone(), stdout()))); glsp::set_epr_writer(Box::new(Tee(log_file.clone(), stderr()))); }
Unless you manually override them, the Rust
macros print!(), println!(), eprint!() and eprintln!() will still print to the standard
output and standard error streams. As an alternative, you can use the macros pr!(), prn!(),
epr!() and eprn!() to print to the active Runtime's pr_writer and epr_writer.