ForeignType

Foreign types are a special kind of opaque type which may contain references to managed data, but can't have type parameters. Instead of OpaqueType directly, we'll need to derive ForeignType. The only attibute that still applies is super_type.

Like OpaqueType, a type can only implement ForeignType if it is 'static and implements Send and Sync. This is problematic, since managed data has lifetimes and doesn't implement those traits. To work around the first issue, we need to use Weak data because it allows the 'scope-lifetime to be erased. To work around the second, we'll need to manually implement Send and Sync. As long as the Rust parts of the type implement these traits, it should be fine to assume they can be implemented; all managed data is hidden behind Weak, which lets us ignore the issue until we actually try to access it because these considerations are already a part of the safety contract of accessing weakly-referenced managed data.

Mark functions

Foreign types can reference managed data because they have custom mark functions. When the GC encounters an instance of a foreign type during the mark phase, this custom function is called to allow the GC to find those live references. If the type of a field implements the Mark trait, that field can simply be annotated with #[jlrs((mark)] to include it in the generated mark function. This trait is implemented for all Weak types, Option<Weak>, and arrays and Vecs of types that implement Mark themselves.

If a field has a type that references Julia data but doesn't implement Mark, e.g. HashMap<String, WeakValue>, we'll need to implement a custom marking function manually and annotate the field with #[jlrs(mark_with = custom_mark_fn)]. This custom function for T must have this signature: unsafe fn custom_mark_fn<P: ForeignType>(&T, ptls: PTls, parent: &P) -> usize. This is the same signature as Mark::mark; unlike Mark, custom fuctions can be implemented even for types defined in other crates.

To implement a custom mark function correctly, we must mark every instance of Julia data referenced by that type. In the case of HashMap<String, WeakValue>, this means iterating over all values in the map and calling Mark::mark on every one with the provided ptls and parent, and returning the sum of their return values.

Write barriers

A custom mark function isn't the only thing we need to maintain GC invariants, we'll use the word object to refer to an instance of a managed type. The GC has two generations, young and old. A newly allocated object is young, if it survives a collection cycle it becomes old. The GC can do a full collection cycle and look at both generations, or an incremental one and just look at the young generation. If a young object is only referenced by an old one, we hit a snag: an incremental run only looks at young objects, so it should never see that reference in an old object and free it. To prevent this from happening, we have to insert a write barrier whenever we start referencing an object that might be young. Two cases where a write barrier must be inserted are setting a field to another object, and adding an object to a collection.

use std::collections::HashMap;

use jlrs::{
    data::{managed::value::{typed::{TypedValue, TypedValueRet}, ValueRet}, types::foreign_type::{mark::Mark, ForeignType}}, prelude::*,  weak_handle
};

// We can introduce additional generics as long as they can be inferred.
unsafe fn mark_map<M: Mark, P: ForeignType>(
    data: &HashMap<(), M>,
    ptls: jlrs::memory::PTls,
    parent: &P,
) -> usize {
    data.values().map(|v| unsafe { v.mark(ptls, parent) }).sum()
}

#[derive(ForeignType)]
pub struct ForeignThing {
    #[jlrs(mark)]
    a: WeakValue<'static, 'static>,
    #[jlrs(mark_with = mark_map)]
    b: HashMap<(), WeakValue<'static, 'static>>,
}

unsafe impl Send for ForeignThing {}
unsafe impl Sync for ForeignThing {}

impl ForeignThing {
    pub fn new(value: Value<'_, 'static>) -> TypedValueRet<ForeignThing> {
        match weak_handle!() {
            Ok(handle) => {
                TypedValue::new(
                    handle,
                    ForeignThing {
                        a: value.leak(),
                        b: HashMap::default(),
                    },
                )
                .leak()
            },
            Err(_) => panic!("not called from Julia"),
        }
    }

    pub fn get(&self) -> ValueRet {
        unsafe { self.a.assume_owned().leak() }
    }

    pub fn set(&mut self, value: Value) {
        unsafe {
            self.a = value.assume_owned().leak();
            self.write_barrier(self.a, self);
        }
    }
}

julia_module! {
    become julia_module_tutorial_init_fn;

    struct ForeignThing;
    in ForeignThing fn new(value: Value<'_, 'static>) -> TypedValueRet<ForeignThing> as ForeignThing;
    in ForeignThing fn get(&self) -> ValueRet;
    in ForeignThing fn set(&mut self, value: Value);
}
julia> module JuliaModuleTutorial ... end
Main.JuliaModuleTutorial

julia> v =  JuliaModuleTutorial.ForeignThing(Float32(3.0))
Main.JuliaModuleTutorial.ForeignThing()

julia> JuliaModuleTutorial.get(v)
3.0

julia> JuliaModuleTutorial.set(v, Float32(4.0))

julia> JuliaModuleTutorial.get(v)
4.0