Casting, unboxing and accessing managed data
So far, the only Julia function we've called is println
, which isn't particularly interesting because it returns nothing
. In practice, we often don't just want to call Julia functions for their side-effects, we also want to use their results in Rust.
A Value
is an instance of some Julia type, managed by the GC. If there's a more specific managed type for that Julia type, we can convert the Value
by casting it with Value::cast
.
use jlrs::prelude::*;
fn main() {
let handle = Builder::new().start_local().expect("cannot init Julia");
handle.local_scope::<_, 1>(|mut frame| {
let s = JuliaString::new(&mut frame, "Hello, World!").as_value();
assert!(s.cast::<JuliaString>().is_ok());
let module = Module::main(&frame).as_value();
assert!(module.cast::<Module>().is_ok());
assert!(module.cast::<JuliaString>().is_err());
});
}
Managed types aren't the only types that map between Rust and Julia. There are many types where the layout in Rust matches the layout of the managed data, including most primitive types. These types implement the Unbox
trait which lets us extract the data from the Value
with Value::unbox
.
use jlrs::prelude::*;
fn main() {
let handle = Builder::new().start_local().expect("cannot init Julia");
handle.local_scope::<_, 1>(|mut frame| {
let one = Value::new(&mut frame, 1usize);
let unboxed = one.unbox::<usize>().expect("cannot be unboxed as usize");
assert_eq!(unboxed, 1);
});
}
If there's no appropriate type that implements Unbox
or Managed
, we can access the fields of a Value
manually.
use jlrs::prelude::*;
fn main() {
let handle = Builder::new().start_local().expect("cannot init Julia");
handle.local_scope::<_, 4>(|mut frame| {
// Normally, this custom type would have been defined in some module.
// Safety: Defining a new type is safe.
let custom_type = unsafe {
Value::eval_string(
&mut frame,
"struct CustomType
a::UInt8
b::Bool
CustomType() = new(0x1, false)
end
CustomType",
)
.expect("cannot create CustomType")
};
// Safety: the constructor of CustomType is safe to call
let inst = unsafe {
custom_type
.call0(&mut frame)
.expect("cannot call constructor of CustomType")
};
let a = inst.get_field(&mut frame, "a")
.expect("no field named a")
.unbox::<u8>()
.expect("cannot unbox as u8");
assert_eq!(a, 1);
let b = inst.get_field(&mut frame, "b")
.expect("no field named b")
.unbox::<Bool>()
.expect("cannot unbox as Bool")
.as_bool();
assert_eq!(b, false);
});
}
There's a lot going on in this example, but a lot of it is just setup code. We first evaluate some Julia code that defines CustomType
. Constructors in Julia are just functions linked to a type, so we can call CustomType
's constructor by calling the result of the code we've evaluated. Finally, we get to the point and use Value::get_field
to access the fields before unboxing their content.1 The second field is unboxed as a Bool
, not a bool
. The Julia Char
type similarly maps to jlrs's Char
type. These types exist to avoid any potential mismatches between Rust and Julia.
Value::get_field
accesses a field by name, if we had wanted to access a field of a tuple we would have needed to do so by index with Value::get_nth_field
. Indexing starts at 0.