RClass
By default, from the perspective of GameLisp scripts, each rdata
is a black box. GameLisp code
can query whether a particular value is an rdata
, receive rdata
from function calls,
pass rdata
as function arguments... and that's about it!
If you prefer your scripting APIs to be a little more object-oriented, you can register an
RClass
("Rust class") for any Rust type. When an rdata
has an associated RClass
,
this will cause the rdata
to behave like a GameLisp object.
It can have methods and properties; its type can be queried using the
is?
function; and it has full support for operator
overloading.
To register an RClass
, use the RClassBuilder
struct. The builder's api is designed to
vaguely resemble a defclass
form. We saw this API used earlier,
for our Texture
struct:
#![allow(unused_variables)] fn main() { RClassBuilder::<Texture>::new() .met("width", &Texture::width) .met("height", &Texture::height) .build(); }
You can register an RClass
for any 'static
Rust type, including types defined by external
crates. The only restriction is that each RClass
must have a unique name. If you have two
types, one named audio::Clip
and one named video::Clip
, they can't both be named Clip
.
#![allow(unused_variables)] fn main() { glsp::bind_rfn("Command", &Command::new::<String>)?; RClassBuilder::<Command>::new() .met("args", &|command: &mut Command, rest: Rest<String>| { command.args(rest); }) .met("status", &Command::status) .build(); }
(let command (Command "ls"))
(ensure (is? command 'Command))
(.args command "-l" "-a")
; spawn the process and wait for it to complete
(.status command)
Weak Roots
Generally speaking, it's fine to move any 'static
type onto the GameLisp heap. However,
there's one exception.
When a Root
smart pointer is pointing to an object, it will entirely prevent that object from
being deallocated. This means that if any Root
pointers are moved onto the garbage-collected heap
(for example, by storing a Root
in a struct which is passed to glsp::rdata
), memory leaks
will occur. The Val
and RRoot
types may contain Roots
, so those types should also be kept
off the heap.
If you need a pointer from one heap allocation to another, you should use Gc
instead.
Gc
is a weak reference; it doesn't prevent its pointee from being deallocated, and so it
won't cause any memory leaks.
Unfortunately, Gc
does require a little bit of supervision. Because Gc
pointers are
weakly-rooted, this means that the object they're pointing to could be deallocated at any
time! To prevent this from happening, you'll need to:
-
Use
RClassBuilder::trace
to specify how the garbage collector should visit each of theGc
pointers owned by your Rust type. -
Call
glsp::write_barrier
when an instance of your Rust type has been mutated, so that the garbage collector can check whether any newGc
pointers have been added to it.
#![allow(unused_variables)] fn main() { //don't do this! struct Colliders { array: Root<Arr> } impl Colliders { fn get(&self) -> Root<Arr> { Root::clone(&self.array) } fn replace(&mut self, new_array: Root<Arr>) { self.array = new_array; } } RClassBuilder::<Colliders>::new() .met("get", &Colliders::get) .met("replace!", &Colliders::replace) .build(); }
#![allow(unused_variables)] fn main() { //instead, do this... struct Colliders { array: Gc<Arr> } impl Colliders { fn get(&self) -> Root<Arr> { self.array.upgrade().unwrap() } fn replace(&mut self, new_array: Root<Arr>) { self.array = new_array.downgrade(); } fn trace(&self, visitor: &mut GcVisitor) { visitor.visit(&self.array); } } RClassBuilder::<Colliders>::new() .met("get", &Colliders::get) .met( "replace!", &|colliders: RRoot<Colliders>, new_array: Root<Arr>| { colliders.borrow_mut().replace(new_array); glsp::write_barrier(&colliders.into_root()); } ) .trace(Colliders::trace) .build(); }
We also provide GcVal
as a weakly-rooted alternative to Val
, and RGc
as a
weakly-rooted alternative to RRoot
.
Make sure you don't get caught out when constructing an rfn
! If your rfn
is a Rust closure
which captures a Root
, Val
or RRoot
, then you're effectively moving a Root
onto the GameLisp heap, which will cause the captured data to leak. (Of course, for an rfn
which is bound to a global variable, a memory leak might be acceptable.)
#![allow(unused_variables)] fn main() { let primes: Root<Arr> = arr![2, 3, 5, 7, 11]; primes.freeze(); let closure = move || { Root::clone(&primes) }; glsp::bind_rfn("primes", Box::new(closure))?; }