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! variables internally. This means that there's no need to pass around a context object (aka "God object"), which would make the crate much less convenient to use.

Each thread has a private thread_local! variable which refers 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 closure's captures, arguments and return value must all implement the GSend auto trait. Because most of the types in the glsp crate impl !GSend, it's very difficult for any data from a Runtime to be smuggled into a scope where that Runtime is no longer active. (Rest assured that if this does happen, the library has dynamic checks which still guarantee memory‑safety.)


#![allow(unused_variables)]
fn main() {
//error: Sym does not implement GSend
let sym: Sym = runtime.run(|| {
	let sym = glsp::sym("hello")?;
	Ok(sym)
}).unwrap();

//no problem: LenWrapper automatically implements GSend
struct LenWrapper(usize);
let len: LenWrapper = runtime.run(|| {
	let sym = glsp::sym("hello")?;
	let len = sym.name().len();
	Ok(LenWrapper(len))
}).unwrap();
}

The glsp crate contains a large number of free functions, like glsp::sym, which manipulate the active Runtime in some way. If called when there is no Runtime active, they panic.

Most of those 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.

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 small Copy type which just wraps a u32 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 as Root<Arr> for an array or Root<Coro> for a coroutine.

  • GFn is the name for GameLisp's fn primitive type, to avoid confusion with Rust's Fn trait. Similarly, the GameLisp type iter is represented by the Rust type GIter.

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.