Rust Functions

As we mentioned several times throughout Section 1, Rust functions can be bound to GameLisp values. Their primitive type is rfn.

In order to bind a Rust function to a GameLisp global variable, call glsp::bind_rfn:


#![allow(unused_variables)]
fn main() {
use glsp::prelude::*;

fn print_them(arg: u8, tuple: (Sym, Num), st: &str) -> [f32; 3] {
	prn!("{:?} {:?} {:?}", arg, tuple, st);
	[10.5, 21.0, 42.0]
}

glsp::bind_rfn("print-them", rfn!(print_them))?;
}
(let result (print-them 1 '(my-sym 5.0) "hello"))
(prn result) ; prints (10.5 21.0 42.0)

Alternatively, you can use the glsp::rfn function to construct an rfn value directly. They're represented by the RFn struct, which is a small Copy type, similar to Sym.


#![allow(unused_variables)]
fn main() {
let int_printer: RFn = glsp::rfn(rfn!(|n: i32| prn!("{}", n)));
arr.push(int_printer)?;
}

The glsp::rfn and glsp::bind_rfn functions accept an argument of type WrappedFn. This should be constructed using the rfn!() macro, which takes the name of a function, the full path to a method, or a non‑capturing closure, and uses some type-system wizardry to produce a type-erased wrapper function.


#![allow(unused_variables)]
fn main() {
glsp::bind_rfn("swap-bytes", rfn!(i32::swap_bytes))?;
}

Type Conversions

Return types and parameters are automatically converted to and from GameLisp values.

Types which can be produced from GameLisp function arguments implement the MakeArg trait. You're not able to implement this trait directly for your own types, but it has a blanket implementation for any type which implements FromVal.

Built-in MakeArg implementations include:

  • All of Rust's primitive integer and floating-point types.

  • Types which correspond to a GameLisp primitive type: (), bool, char, Sym, RFn, Root<Arr>.

  • Num, Deque, Callable, Iterable.

  • Tuples (A, B), fixed-size vectors [A; n], Vec, VecDeque and SmallVec, which are collected from an array. For tuples and fixed-size vectors, the length of the input array must exactly match the length of the type.

  • HashMap and BTreeMap, which are collected from a table.

  • String, CString, OsString, PathBuf, &str, &Path, all of which are copied from a GameLisp string. Note that types like &str and &Path copy the string into a temporary variable, rather than borrowing its internal storage.

  • References to types which can be pointed to by a Root: &Arr, &Str, &GFn.

Return types which can be converted to a GameLisp value implement the IntoResult trait. This trait is implemented for any T or GResult<T> where T implements ToVal. The built-in ToVal implementations are mostly the same types listed above.

Custom Type Conversions

It's possible to use your own value types (not reference types) as rfn return values or parameters by implementing the ToVal and FromVal traits.


#![allow(unused_variables)]
fn main() {
use glsp::prelude::*;

struct Coords {
	x: i32,
	y: i32
};

//a more efficient implementation is possible, as we'll see later
impl FromVal for Coords {
	fn from_val(val: &Val) -> GResult<Coords> {
		match val {
			Val::Obj(obj) => {
				let class: Root<Class> = glsp::global("Coords")?;
				ensure!(obj.is(&class), "expected Coords, received an obj");
				Ok(Coords {
					x: obj.get("x")?,
					y: obj.get("y")?
				})
			}
			val => bail!("expected Coords, received {}", val.a_type_name())
		}
	}
}

impl ToVal for Coords {
	fn to_val(&self) -> GResult<Val> {
		let class: Root<Class> = glsp::global("Coords")?;
		glsp::call(&class, &(self.x, self.y))
	}
}

fn offset(coords: Coords, dx: i32, dy: i32) -> Coords {
	Coords {
		x: coords.x + dx,
		y: coords.y + dy
	}
}

glsp::bind_rfn("offset", rfn!(offset))?;
}

Optional and Rest Parameters

Parameters of type Option<T> are treated as optional. They must appear together, after any non-optional parameters. When an optional parameter receives an argument, it's set to Some; otherwise, it's set to None.

The final parameter may be a &[T] or &mut [T] slice, in which case it will accept zero or more arguments.


#![allow(unused_variables)]
fn main() {
fn example(non_opt: u8, opt: Option<u8>, rest: &[u8]) {
	prn!("{:?} {:?} {:?}", non_opt, opt, rest);
}

glsp::bind_rfn("example", rfn!(example))?;
}
(example)         ; error: too few arguments
(example 1000)    ; error: type mismatch
(example 1)       ; prints 1 None []
(example 1 2)     ; prints 1 Some(2) []
(example 1 2 3)   ; prints 1 Some(2) [3]
(example 1 2 3 4) ; prints 1 Some(2) [3, 4]

Errors

To return an error from an rfn, you can simply set the function's return type to GResult<T>, which is an alias for Result<T, GError>.

The usual way to trigger a GameLisp error is using the macros bail!() and ensure!(). bail constructs a new GError and returns it. ensure tests a condition and calls bail when the condition is false. (The names of these macros are conventional in Rust error-handling libraries, such as error-chain and failure.)

If you need to create an error manually, you can use the error!() macro, or one of GError's constructor methods. An arbitrary Error type can be reported as the cause of an error using the with_source method.


#![allow(unused_variables)]
fn main() {
fn file_to_nonempty_string(path: &Path) -> GResult<String> {
	match std::fs::read_to_string(path) {
		Ok(st) => {
			ensure!(st.len() > 0, "empty string in file {}", path);
			Ok(st)
		},
		Err(io_error) => {
			let glsp_error = error!("failed to open the file {}", path);
			Err(glsp_error.with_source(io_error))
		}
	}
}
}

If a panic occurs within an rfn's dynamic scope, the panic will be caught by the innermost rfn call and converted into a GResult. The panic will still print a message to stderr when it occurs, including a Rust stack-trace when the RUST_BACKTRACE environment variable is set. If this is undesirable, you can override the default printing behaviour with a custom panic hook.

RFn Macros

Both Rust functions and GameLisp functions can be used as GameLisp macros (although GameLisp functions are usually the much more convenient choice).

Within a Rust function, macro_no_op!() will create and return a special kind of GError which suppresses further expansion of the current macro. (Incidentally, this is also how the macro-no-op built-in function works.)

This means that you can only use macro_no_op!() in a function which returns GResult.