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 implement Userdata. Getable is automatically implemented for &T where T: Userdata when used as argument to a Rust function, for places OpaqueValue can be used as a smart pointer around a Userdata value or the UserdataValue 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!"