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

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::funcs. 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]