Marshalling types
An important part of embedding Gluon is translating non-primitive types from Gluon types to Rust types and vice versa, allowing you to seamlessly implement rich APIs with complex types. This translation is called marshalling.
Required traits
Gluon provides several traits for safely marshalling types to and from Gluon code:
-
VmType provides a mapping between Rust and Gluon types. It specifies the Gluon type the implementing Rust type represents. All types that want to cross the Gluon/Rust boundary must implement this trait.
-
Getable: Types that implement
Getable
can be marshalled from Gluon to Rust. This means you can use these types anywhere you are receiving values from Gluon, for example as parameters for a function implemented on the Rust side or as return type of a Gluon function you want to call from Rust. -
Pushable is the counterpart to
Getable
. It allows implementing types to be marshalled to Gluon. Values of these types can returned from embedded Rust functions and be used as parameters to Gluon functions. -
Userdata allows a Rust type to be marshalled as completely opaque type. The Gluon code will be able to receive and pass values of this type, but cannot inspect it at all. This is useful for passing handle-like values, that will be mostly used by the Rust code.
Pushable
is automatically implemented for all types that implementUserdata
.Getable
is automatically implemented for&T where T: Userdata
when used as argument to a Rust function, for placesOpaqueValue
can be used as a smart pointer around aUserdata
value or theUserdataValue
extractor can be used to clone the value.
Gluon already provides implementations for the primitive and common standard library types.
Implementing the marshalling traits for your types
You can implement all of the above traits by hand, but for most cases you can also use the derive macros in gluon_codegen.
You will also have to register the correct Gluon type. If you are marshalling Userdata
, you
can use Thread::register_type
, otherwise you will need to provide the complete type definition
in Gluon. When using the serialization
feature, you can automatically generate the source
code using the api::typ::make_source
function.
Using derive macros
Add the gluon_codegen
crate to your Cargo.toml
this lets you import and derive the
VmType
, Getable
, Pushable
and Userdata
traits.
VmType
, Getable
and Pushable
can be implemented on any type which only consists of types which in turn implements
these traits whereas Userdata
can be derived for any type as long as it is Debug + Send + Sync
and has a 'static
lifetime.
Sometimes when deriving VmType
you do not want to define a new type. In this case you can use the vm_type
attribute
to point to another, compatible type. See the marshalling example for the complete source for the examples below.
// Using `vm_type` to point to compatible type defined in gluon
#[derive(Debug, PartialEq, VmType, Getable)]
#[gluon(vm_type = "std.list.List")]
enum List<T> {
Nil,
Cons(T, Box<List<T>>),
}
// Defines an opaque type with Userdata
#[derive(Userdata, Trace, Clone, Debug, VmType)]
// Lets gluon know that the value can be cloned which can be needed when transferring the value between threads
#[gluon_userdata(clone)]
// Refers to the `WindowHandle` type registered on the Rust side
#[gluon(vm_type = "WindowHandle")]
struct WindowHandle {
id: Arc<u64>,
metadata: Arc<str>,
}
Implementing by hand
The following examples will all assume a simple struct User<T>
, which is defined in a different
crate (You can find the full code in the marshalling example). To implement the marshalling traits,
we have to create a wrapper and implement the traits for it.
// defined by a different crate
struct User<T> {
name: String,
age: u32,
data: T,
}
VmType
VmType
requires you to specify the Rust type that maps to the correct Gluon type. You can
simply assign Self
. The heart of the trait is the make_type
function. To get the correct
Gluon type, you will have to look it up from the vm, using the fully qualified type name:
let ty = vm.find_type_info("examples.wrapper.User")
.expect("Could not find type")
.into_type();
If you have a non generic type, this is all you need. In our case, we will have to apply the generic type parameters first:
let mut vec = AppVec::new();
vec.push(T::make_type(vm));
Type::app(ty, vec)
You simply push all parameters to the AppVec
in the order of their declaration, and then
use Type::app
to construct the complete type.
Getable
Getable
only has one function you need to implement, from_value
. It supplies a reference
to the vm and the raw data, from which you have to construct your type. Since we are implementing
Getable
for a complex type, we are only interested in the ValueRef::Data
variant.
let data = match data.as_ref() {
ValueRef::Data(data) => data,
_ => panic!("Value is not a complex type"),
};
From data
we can now extract the individual fields, using lookup_field
for named fields or
get_variant
for unnamed fields (like in tuple structs or variants).
// once we have the field's value, we construct the correct type
// using its Getable implementation
let name = String::from_value(vm, data.lookup_field(vm, "name").unwrap();
In this example we used a struct, but if we wanted to construct an enum, we need to find out
what variant we are dealing with first, using the tag
method:
match data.tag() {
0 => // build first variant
1 => // build second variant
// ...
}
Pushable
To implement Pushable
, we need to interact with Gluon's stack directly. The goal is to create
a Value
that represents our Rust value, and push it on the stack. In order to do that, we need to
push the fields of our type first:
self.inner.name.push(vm, ctx)?
self.inner.age.push(vm, ctx)?;
self.inner.data.push(vm, ctx)?;
The ActiveThread
we get passed has a Context
that allows pushing values, but we can do even better and
use the record!
macro:
(record!{
name => self.inner.name,
age => self.inner.age,
data => self.inner.data,
}).push(ctx)
If we were pushing an enum, we would have to use Context::push_new_data
and manually specify the tag
of the pushed variant as well as its number of fields (zero if it's a variant with no attached data).
let val = match an_enum {
Enum::VariantOne => ctx.context().push_new_data(vm, 0, num_fields_in_variant_one),
Enum::VariantTwo => ctx.context().push_new_data(vm, 1, num_fields_in_variant_two),
}?;
Userdata
Implementing Userdata
is straight forward: we can either derive
the trait or use the default
implementation since there are no required methods. However, Userdata
also requires the type to implement
VmType
and Trace
. We can use the minimal VmType
implementation, it already
provides the correct make_type
function for us:
impl<T> VmType for GluonUser<T>
where
T: 'static + Debug + Sync + Send
{
type Type = Self;
}
The Trace
implementation can be automatically derived in most cases as it will just call it's methods on every field
of the type. However, this means that it expects that every field also implements Trace
, if that is not the case you
can opt out of tracing with the #[gluon_trace(skip)]
attribute. This is fine in many cases but can cause reference
cycles if your userdata stores values managed by Gluon's GC. However if it doesn't it is safe to just use skip
.
// Contains no gluon managed values so skipping the trace causes no issues
#[derive(Trace)]
#[gluon_trace(skip)]
struct SimpleType {
name: String,
slot: std::sync::Mutex<i32>,
}
// Here we store a `OpaqueValue` which is managed by gluon's GC. To avoid a reference cycle we must trace
// the field so gluon can find it. `gc::Mutex` is a drop-in replacement for `std::sync::Mutex` which is GC aware.
#[derive(Trace)]
struct Callback(gluon::vm::gc::Mutex<OpaqueValue<RootedThread, fn (i32) -> String>>);
Passing values to and from Gluon
Once your type implements the required traits, you can simply use it in any function you want to expose to Gluon.
If you want to receive or return types with generic type parameters that are instantiated on the Gluon side, you can use the Opaque type together with the marker types in the generic module:
// we define Either with type parameters, just like in Gluon
#[derive(Getable, Pushable, VmType)]
enum Either<L, R> {
Left(L),
Right(R),
}
// the function takes an Either instantiated with the `Opaque` struct,
// which will handle the generic Gluon values for us
use gluon::vm::api::OpaqueValue;
// The `generic` sub-module provides marker types which mimic generic parameters
use gluon::vm::api::generic::{L, R};
fn flip(
either: Either<OpaqueValue<RootedThread, L>, OpaqueValue<RootedThread, R>>,
) -> Either<OpaqueValue<RootedThread, R>, OpaqueValue<RootedThread, L>> {
match either {
Either::Left(val) => Either::Right(val),
Either::Right(val) => Either::Left(val),
}
}
Now we can pass Either
to our Rust function:
// Either is defined as:
// type Either l r = | Left l | Right r
let either: forall r . Either String r = Left "hello rust!"
// we can pass the generic Either to the Rust function without an issue
do _ =
match flip either with
| Left _ -> error "unreachable!"
| Right val -> io.println ("Right is: " <> val)
// using an Int instead also works
let either: forall r . Either Int r = Left 42
match flip either with
| Left _ -> error "also unreachable!"
| Right 42 -> io.println "this is the right answer"
| Right _ -> error "wrong answer!"