Writing OCaml functions in Rust
This section requires the derive
feature, which is enabled in ocaml-rs
by default. This exposes ocaml::func, which is the recommended way to create an OCaml function in Rust. Below are some examples using ocaml::func
- Hello world
- Structs and enums
- Calling an OCaml function
- Opaque types
- Raising an exception
- Returning OCaml result
- Using
Value
directly - Unboxed arguments
Hello world
This example returns a string from Rust to OCaml
#![allow(unused)] fn main() { extern crate ocaml; #[ocaml::func] #[ocaml::sig("unit -> string")] pub fn hello_world() -> &'static str { "Hello, world!" } }
Structs and enums
The example uses derive(ToValue)
and derive(FromValue)
to create an enum and struct that can be used as parameters to ocaml::func
s. Their names will be converted to snake case for OCaml, so the Rust type BinOp
will become bin_op
and Expr
will become expr
.
#![allow(unused)] fn main() { extern crate ocaml; #[derive(ocaml::FromValue, ocaml::ToValue, Clone, Copy)] #[ocaml::sig("Add | Sub | Mul | Div")] pub enum BinOp { Add, Sub, Mul, Div } #[derive(ocaml::FromValue, ocaml::ToValue)] #[ocaml::sig("lhs: float; rhs: float; op: bin_op")] pub struct Expr { lhs: f64, rhs: f64, op: BinOp, } #[ocaml::func] #[ocaml::sig("expr -> float")] pub fn expr_eval(expr: Expr) -> f64 { match expr.op { BinOp::Add => expr.lhs + expr.rhs, BinOp::Sub => expr.lhs - expr.rhs, BinOp::Mul => expr.lhs * expr.rhs, BinOp::Div => expr.lhs / expr.rhs } } }
Calling an OCaml function
This example shows how to call an OCaml function from Rust - the OCaml function must be registered using Callback.register. In this case we're calling the OCaml function my_incr
, which looks like this:
let my_incr x = x + 1
let () = Callback.register "my_incr" my_incr
#![allow(unused)] fn main() { extern crate ocaml; ocaml::import! { fn my_incr(x: ocaml::Int) -> ocaml::Int; } #[ocaml::func] #[ocaml::sig("int -> int")] pub unsafe fn call_my_incr(x: ocaml::Int) -> Result<ocaml::Int, ocaml::Error> { my_incr(gc, x) } }
A few things to note:
- When calling the import!ed function you will need to pass the OCaml runtime handle as the first parameter
- The return value of the function will be wrapped in
Result<T, ocaml::Error>
because the function may raise an exception
For functions that aren't registered using Callback.register
you can use the ocaml::function!
macro to convert them into a typed closure:
#![allow(unused)] fn main() { extern crate ocaml; #[ocaml::func] #[ocaml::sig("(int -> int) -> int -> int")] pub unsafe fn call_incr(incr: ocaml::Value, a: ocaml::Int) -> Result<ocaml::Int, ocaml::Error> { let incr = ocaml::function!(incr, (a: ocaml::Int) -> ocaml::Int); incr(gc, &a) } }
Opaque types
This example shows how to wrap a Rust type using the Custom trait and ocaml::Pointer
#![allow(unused)] fn main() { extern crate ocaml; use std::io::Read; #[ocaml::sig] // Creates an opaque type on the OCaml side struct File(std::fs::File); ocaml::custom!(File); #[ocaml::func] #[ocaml::sig("string -> file")] pub fn file_open(filename: &str) -> Result<ocaml::Pointer<File>, ocaml::Error> { let f = std::fs::File::open(filename)?; Ok(File(f).into()) } #[ocaml::func] #[ocaml::sig("file -> string")] pub fn file_read(file : &mut File) -> Result<String, ocaml::Error> { let mut s = String::new(); file.0.read_to_string(&mut s)?; Ok(s) } }
Once this value is garbage collected, the default finalizer will call Pointer::drop_in_place
to run drop
and clean up resources on the Rust side, if you write a custom finalizer make sure to include a call to Pointer::drop_in_place
.
Raising an exception
Raising an exception is accomplished by panicking:
#![allow(unused)] fn main() { extern crate ocaml; #[ocaml::func] #[ocaml::sig("int -> unit")] pub unsafe fn fail_if_even_panic(i: ocaml::Int) { if i % 2 == 0 { panic!("even") } } }
or returning a Result<_, ocaml::Error>
value:
#![allow(unused)] fn main() { extern crate ocaml; #[ocaml::func] #[ocaml::sig("int -> unit")] pub unsafe fn fail_if_even_result(i: ocaml::Int) -> Result<(), ocaml::Error> { if i % 2 == 0 { return Err(ocaml::CamlError::Failure("even").into()) } Ok(()) } }
Returning OCaml result
In the previous example Result<_, ocaml::Error>
was used to raise an exception, however Result<A, B>
where A
and B
both implement ToValue
will create an OCaml ('a, 'b) Result.t
:
#![allow(unused)] fn main() { extern crate ocaml; use ocaml::{ToValue}; #[ocaml::func] #[ocaml::sig("string -> (int, [`Msg of string]) result")] pub unsafe fn try_int_of_string(s: &str) -> Result<ocaml::Int, ocaml::Value> { match s.parse::<isize>() { Ok(i) => Ok(i), Err(e) => { let s = format!("{e:?}"); let err = ocaml::Value::hash_variant(gc, "Msg", Some(s.to_value(gc))); Err(err) } } } }
Using Value
directly
It is also possible to use ocaml::Value
to avoid any conversion or copying, however this can be more error prone.
#![allow(unused)] fn main() { extern crate ocaml; #[ocaml::func] #[ocaml::sig("string array -> int -> string -> unit")] pub unsafe fn array_set(mut array: ocaml::Value, index: ocaml::Value, s: ocaml::Value) { array.store_field(gc, index.int_val() as usize, s) } }
Unboxed arguments
Unfortunately ocaml::func
doesn't support unboxed/noalloc functions, however it is still possible to create them using ocaml-rs
:
#![allow(unused)] fn main() { extern crate ocaml; #[no_mangle] pub extern "C" fn unboxed_float_avg(a: f64, b: f64) -> f64 { (a + b) / 2.0 } #[ocaml::bytecode_func] pub fn unboxed_float_avg_bytecode(a: f64, b: f64) -> f64 { unboxed_float_avg(a, b) } }
In this case you will also need to write the signature manually:
external unboxed_float_avg: float -> float -> float = "unboxed_float_avg_bytecode" "unboxed_float_avg" [@@unboxed] [@@noalloc]