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 rfn
s 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(); }