Hi!
I'm trying to build an interop layer between asynchronous Rust and asynchronous OCaml with primary goal to be able to use asynchronous Rust libraries from OCaml application.
I've got it in a somewhat usable state, and even have it open-sourced on Github [1]. The project is built on top of excellent ocaml-rs
and ocaml-interop
crates.
I'm running into a weird segmentation fault that I can't really understand. OCaml GC is a moving one, and most likely this is the cause as Rust is not quite prepared to the fact that some things are relocating behind it's back.
I've came up with a smart pointer for OCaml referenced objects, called CamlRef
(full sources):
pub struct CamlRef<'a, T>
where
T: Debug,
{
dynbox_root: ocaml::root::Root,
ptr: *const T,
marker: PhantomData<&'a T>,
}
impl<'a, T> CamlRef<'a, T>
where
T: Debug + 'static,
{
pub fn new(dynbox: OCaml<DynBox<T>>) -> Self {
let dynbox_root = unsafe { ocaml::root::Root::new(dynbox.get_raw()) };
let ptr: &T = dynbox.borrow();
Self {
dynbox_root,
ptr: ptr as *const T,
marker: PhantomData,
}
}
}
impl<'a, T> Deref for CamlRef<'a, T>
where
T: Debug + Sized,
{
type Target = T;
fn deref(&self) -> &'a Self::Target {
unsafe { &*self.ptr }
}
}
Basically it roots the OCaml value upon creation (rooting stops the value from being garbage-collected by OCaml GC), and dereferences it via Deref
trait.
I was very happy that Rust refused to move executor
into async in the following example:
#[ocaml::func]
#[ocaml::sig("executor -> (int -> promise) -> unit")]
pub fn lwti_executor_test(executor: CamlRef<Executor>, _f: ocaml::Value) {
eprintln!("executor at #1: {:?}", executor);
let task = executor.spawn(async move {
eprintln!("executor at #2: {:?}", executor);
executor.spawn(async {}).detach();
});
task.detach();
}
But if I change the async
to not be a move
one, it happily compiles, and crashes as well. Debug prints will print the following lines:
executor at #1: CamlRef { dynbox_root: Root(0x7f3d6d150028), ptr: 0x556cc720f600, marker: PhantomData<&ocaml_lwt_interop::local_executor::LocalExecutor> }
executor at #2: CamlRef { dynbox_root: Root(0x7f3d6d93b3c0), ptr: 0x556cc6006682, marker: PhantomData<&ocaml_lwt_interop::local_executor::LocalExecutor> }
Segmentation fault
Somehow CamlRef
contents got corrupted, leading to segfault.
I'll try to figure out what's happening with GC interaction (although some ideas are welcome if someone knowledgeable about OCaml integration comes by!), but I thought that maybe it makes sense to just prevent this from happening using type system capabilities somehow. If I clone my executor before sending it to async, everything works just fine. Ideally I want my CamlRef
to not leak into an async that outlives the current function, and I want Rust compiler to enforce that.
I've added explicit lifetime parameter to my CamlRef
implementation, and assumed that it worked with async move
, but looks like it's not enough. Some recommendations on how this can be tightened up are welcome!
Thanks in advance!