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:
-
Val
is the enum which represents a GameLisp value. Each variant corresponds to one of the sixteen primitive types. -
Sym
represents a GameLisp symbol. It's a smallCopy
type which just wraps au32
identifier. -
Root
is 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 anArr
or aCoro
. -
GFn
is the name for GameLisp'sfn
primitive type, to avoid confusion with Rust'sFn
trait. Similarly, the GameLisp typeiter
is 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
.