RData
So far, so good! With the information from the previous chapter, we can handle most Rust functions which deal with primitive types, standard-library types and built-in GameLisp types, binding those functions to GameLisp with very little boilerplate.
We've also learned how to provide automatic argument and return-value conversions for our own Rust types - perhaps representing a tuple struct as an array, or representing an enum as a symbol.
The next step is to start dealing with Rust types which can't be represented as a GameLisp
primitive. Many Rust types, such as the standard File struct, can't really be converted into
any of the GameLisp primitive types. Some other types would be too expensive to convert back and
forth on every function call - for example, Rust's standard IpAddr struct could be represented
as a GameLisp string, but it wouldn't be a very efficient choice.
In those cases, we can use the rdata primitive type. rdata stands for "Rust data".
An rdata is a reference to an arbitrary Rust value which has been moved onto the GameLisp heap.
It's represented in the Rust API using the Val::RData enum variant and the RData struct.
There are no special conditions for constructing an rdata; GameLisp can take ownership
of any 'static type. Simply pass your Rust value to the glsp::rdata function, which will
consume the value, wrap it in an RData, move it onto the garbage-collected heap, and
return a Root<RData> which points to its new memory location.
#![allow(unused_variables)] fn main() { let file: File = File::open("example.txt")?; let rdata: Root<RData> = glsp::rdata(file); }
The RData wrapper is dynamically typed, like Any, and dynamically borrowed, like
RefCell. You can query an RData's type by calling is(), and you can dynamically borrow
its payload by calling borrow() or borrow_mut(). If you get the type wrong, or dynamically
borrow the payload in a way that violates Rust's aliasing rules, those methods will gracefully
fail.
#![allow(unused_variables)] fn main() { ensure!(rdata.is::<File>()); let file = rdata.borrow::<File>(); prn!("{} bytes", file.metadata()?.len()); let wrong_type = rdata.borrow::<PathBuf>(); //an error let aliasing_mut = rdata.borrow_mut::<File>(); //an error }
When you call glsp::rdata, you're transferring ownership of your Rust data to the garbage
collector. If the rdata becomes unreachable for any reason, it will automatically be deallocated.
This will drop the rdata's payload, invoking its destructor.
Being at the mercy of the garbage collector isn't always desirable, so you can manually take
back ownership of an RData's payload using the RData::take method. If you attempt to
borrow an RData after calling its take() method, the borrow() call will panic.
#![allow(unused_variables)] fn main() { let file: File = rdata.take::<File>()?; let already_taken = rdata.borrow::<File>(); //an error }
RData as Return Values
In the previous chapter, we described how rfns can return any Rust type which implements the
IntoVal trait. That trait is used to automatically convert the function's return value
into a GameLisp value.
We provide a default implementation of IntoVal for any 'static type. This implementation
simply passes its self argument to the glsp::rdata function, converting it into an rdata.
This means that if you return one of your own types from an rfn, it will "just work", even if
you haven't provided an explicit implementation of IntoVal.
#![allow(unused_variables)] fn main() { struct Sprite { //... } impl Sprite { fn new(path: &str) -> Sprite { //... } } glsp::bind_rfn("Sprite", &Sprite::new)?; }
(let goblin (Sprite "goblin.png"))
(prn (rdata? goblin)) ; prints #t
RData as Arguments
To receive an rdata as an argument to an rfn, you could use the type Root<RData> and
borrow it manually.
#![allow(unused_variables)] fn main() { fn sprite_size(rdata: Root<RData>) -> GResult<(u32, u32)> { let sprite = rdata.try_borrow::<Sprite>()?; Ok((sprite.width, sprite.height)) } }
However, this isn't very convenient. We provide a better alternative. When processing rfn
parameters, if GameLisp encounters a reference to an unknown 'static type, it will accept
an rdata argument and attempt to borrow it as that type for the duration of the function call.
In other words, GameLisp will automatically write the above code on your behalf!
#![allow(unused_variables)] fn main() { fn sprite_size(sprite: &Sprite) -> (u32, u32) { (sprite.width, sprite.height) } }
This means that you can write a normal Rust method with a &self or &mut self parameter, and
then bind it as an rfn without needing to modify it at all.
#![allow(unused_variables)] fn main() { impl Sprite { pub fn size(&self) -> (u32, u32) { (self.width, self.height) } } glsp::bind_rfn("sprite-size", &Sprite::size)?; }
As you might expect, a &mut reference will attempt to mutably borrow an rdata, obeying
Rust's usual aliasing rules.
#![allow(unused_variables)] fn main() { fn copy_pixels(dst: &mut Sprite, src: &Sprite, x: u32, y: u32) { //... } glsp::bind_rfn("copy-pixels", ©_pixels)?; }
(let goblin (Sprite "goblin.png"))
(let changeling (Sprite "human.png"))
; this call succeeds
(copy-pixels changeling goblin 0 0)
; this call fails - the Sprite can't be both mutably and
; immutably borrowed at the same time
(copy-pixels goblin goblin 0 0)
By default, it's not possible for an arbitrary Rust type to be passed into an rfn by value;
only shared and mutable references are supported. You can override this default by implementing
FromVal for your type. This usually works best for Copy types.
#![allow(unused_variables)] fn main() { impl FromVal for Point { fn from_val(val: &Val) -> GResult<Point> { match val { Val::RData(rdata) if rdata.is::<Point>() => { Ok(*rdata.try_borrow::<Point>()?) } val => bail!("expected a Point, received {}", val) } } } }
RRoot
You'll sometimes need to store references to RData. For example, you might need to build
a hash table which you can use to look up Sprites by name.
You could consider storing Root<RData> in the hash table, like so:
#![allow(unused_variables)] fn main() { struct Sprites { by_name: HashMap<String, Root<RData>> } let rdata = sprites.by_name.get("angry-sun").unwrap(); let sprite = rdata.borrow::<Sprite>(); }
However, Root<RData> can be a little awkward to use, because it's dynamically typed. Every
time you call is(), take(), borrow() or borrow_mut(), you'll need to specify that
you're dealing with a Sprite, rather than a non-specific RData. You might also accidentally
store a non-Sprite in the hash table.
Instead, consider using the RRoot smart pointer, which behaves like a Root<RData> but
doesn't erase the RData's actual type:
#![allow(unused_variables)] fn main() { struct Sprites { by_name: HashMap<String, RRoot<Sprite>> } let rdata = sprites.by_name.get("angry-sun").unwrap(); let sprite = rdata.borrow(); }