Expand description
§Part 4: Invoking OCaml Functions from Rust
The ocaml!
macro is used to declare Rust bindings for OCaml functions.
§4.1 The ocaml!
Macro
This macro defines Rust function bindings for OCaml functions that have been registered on the
OCaml side using Callback.register "ocaml_function_name" ocaml_function_name
.
Example:
Suppose you have the following OCaml code:
(* In your OCaml library, e.g., my_ocaml_lib.ml *)
let greet name = Printf.printf "Hello, %s!\n" name
let add_ints a b = a + b
let () =
Callback.register "ml_greet" greet;
Callback.register "ml_add_ints" add_ints
To call these from Rust, you would declare them using the ocaml!
macro:
mod ocaml_api {
use ocaml_interop::{ocaml, OCamlInt};
ocaml! {
// OCaml: val ml_greet : string -> unit
// Effective Rust signature generated:
// pub fn ml_greet(cr: &mut OCamlRuntime, name: OCamlRef<String>);
pub fn ml_greet(name: String);
// OCaml: val ml_add_ints : int -> int -> int
// Effective Rust signature generated:
// pub fn ml_add_ints(cr: &mut OCamlRuntime, a: OCamlRef<OCamlInt>, b: OCamlRef<OCamlInt>) -> BoxRoot<OCamlInt>;
pub fn ml_add_ints(a: OCamlInt, b: OCamlInt) -> OCamlInt;
}
}
Key points about the ocaml!
macro usage:
- The macro generates Rust functions (e.g.,
ocaml_api::ml_greet(cr, ...)
andocaml_api::ml_add_ints(cr, ...)
). - The first argument to these generated Rust functions is automatically
cr: &mut OCamlRuntime
, even if not explicitly listed in theocaml!{}
block for a given function signature. - Arguments in the
ocaml!{}
block (e.g.,name: String
,a: OCamlInt
) define the types that the generated Rust function will expect after thecr
argument. These are then converted internally toOCamlRef<T>
for the actual FFI call. - Return types specified in
ocaml!{}
(e.g.,-> OCamlInt
) indicate the type contained within theBoxRoot<T>
that the generated Rust function will return.
§4.2 Passing Arguments to OCaml
-
The Rust functions generated by
ocaml!
expect arguments to be passed asOCamlRef<'a, T>
. -
OCamlRef<'a, T>
is a reference to an OCaml value. This reference can be derived from anOCaml<'gc, T>
or aBoxRoot<T>
. -
Role of the Borrow Checker:
ocaml-interop
leverages Rust’s borrow checker. AnOCaml<'gc, T>
value is tied to the lifetime ('gc
) of theOCamlRuntime
reference (cr
) from which it was obtained or with which it is associated. If an operation occurs that mutably borrowscr
(e.g., another call into OCaml, allocation), the borrow checker will prevent the use of pre-existingOCaml<'gc, T>
values that might have been invalidated by potential GC activity. This is a key safety feature. -
When to Use
BoxRoot<T>
for Arguments (and the Borrow Checker): The primary reason for explicitly usingBoxRoot<T>
for arguments when calling OCaml functions (defined viaocaml!
) stems from Rust’s borrow checking rules interacting with the&mut OCamlRuntime
requirement of these functions.-
The
&mut OCamlRuntime
Conflict: Functions generated byocaml!
takecr: &mut OCamlRuntime
as their first, implicit argument. If you create a temporary, unrooted OCaml value likelet ocaml_arg = rust_data.to_ocaml(cr);
, thisocaml_arg
(of typeOCaml<'gc, T>
) holds an immutable borrow oncr
for its lifetime'gc
. When you then attempt to call an OCaml function, e.g.,ocaml_api::some_func(cr, &ocaml_arg)
, a borrow conflict arises:some_func
requires a mutable borrow ofcr
, butocaml_arg
still immutably borrows it. Rust’s borrow checker will prevent this. -
BoxRoot<T>
as the Workaround: Converting the Rust data to aBoxRoot<T>
usinglet ocaml_arg_rooted = rust_data.to_boxroot(cr);
resolves this. ABoxRoot<T>
registers the OCaml value with the GC independently. Its validity is not tied to the specific borrow ofcr
used for its creation in the same way anOCaml<'gc, T>
is. Thus, you can pass&mut OCamlRuntime
to the OCaml function and pass&ocaml_arg_rooted
(which becomes anOCamlRef<T>
) without a borrow checker conflict. -
In Practice: This means that for any non-immediate OCaml value (i.e., values allocated on the OCaml heap like strings, lists, custom blocks, etc.) that you intend to pass as an argument to an OCaml function, it generally needs to be in a
BoxRoot<T>
at the point of the call. The example below illustrates this:val_a_ocaml
andval_b_ocaml
(representing OCaml floats, which are heap-allocated when boxed) are converted toBoxRoot<OCamlFloat>
beforeocaml_api::add_floats
is called.
Immediate OCaml values (like
OCamlInt
,bool
,()
, or unboxedf64
) do not involve OCaml heap pointers in the same way, so they don’t present this specific GC-related borrow checker challenge for their own validity, though theocaml!
macro still ensures they are passed correctly. -
OCamlRuntime::with_domain_lock(|cr| {
let val_a_rust: f64 = 5.5;
let val_b_rust: f64 = 2.3;
// Convert to BoxRoot. This is crucial for two main reasons:
// 1. Borrow Checker: The subsequent call to `ocaml_api::add_floats` requires [`&mut OCamlRuntime`](OCamlRuntime).
// If `val_a_rust.to_ocaml(cr)` (which creates an `OCaml<'gc, OCamlFloat>`) were used directly,
// it would immutably borrow `cr`. This conflicts with the mutable borrow needed by `add_floats`,
// and the borrow checker would prevent the call.
// 2. GC Safety for Heap Values: `OCamlFloat` (when not unboxed) represents a heap-allocated OCaml float.
// Rooting with `to_boxroot(cr)` ensures the OCaml GC doesn't deallocate or move the float value
// prematurely, especially if other OCaml operations were to occur.
// [`BoxRoot<T>`](BoxRoot) makes the OCaml value's lifetime independent of the immediate borrow of `cr` for its creation.
let val_a_ocaml: BoxRoot<OCamlFloat> = val_a_rust.to_boxroot(cr);
let val_b_ocaml: BoxRoot<OCamlFloat> = val_b_rust.to_boxroot(cr);
// Pass references (&) to the BoxRooted values. The ocaml! macro generates
// functions that take OCamlRef<T>, which can be created from &BoxRoot<T>.
let sum_ocaml: BoxRoot<OCamlFloat> = ocaml_api::add_floats(cr, &val_a_ocaml, &val_b_ocaml);
let sum_rust: f64 = sum_ocaml.to_rust(cr);
println!("Sum of floats from OCaml: {}", sum_rust);
});
§4.3 Receiving Return Values from OCaml
- Functions declared using
ocaml!
that return a value will yield aBoxRoot<T>
. - This ensures that the returned OCaml value is immediately rooted and thus safe for use within Rust.
- It can subsequently be converted to a Rust type using
.to_rust(cr)
.
§4.4 Handling OCaml Exceptions from Rust
- If an OCaml function invoked from Rust raises an exception, this will currently manifest as a Rust panic.
- Recommendation: It is advisable to design OCaml functions intended for FFI with Rust to
signal error conditions by returning
option
orresult
types (e.g.,int option
,(string, string) result
), rather than by raising exceptions. These can then be mapped to Rust’sOption<T>
andResult<T, E>
types.