ocaml-rs is a Rust crate for interacting with the OCaml runtime. It allows you to write functions in Rust that can be called from OCaml and vice-versa. ocaml-rs also does automatic conversion between OCaml and Rust representations. There are several crates that make this possible:

  • ocaml-sys - Low level bindings to the OCaml runtime
  • ocaml-boxroot-sys - Bindings to ocaml-boxroot, which handles safe allocation of OCaml values
  • ocaml-derive - Procedural macros: ocaml::func, ocaml::sig, derive(FromValue), derive(ToValue)
  • ocaml-build - Generate OCaml interfaces from ocaml::sig definitions
  • ocaml - Higher level bindings built using the crates listed above

Before going any further, it may be helpful to read through the Interfacing C with OCaml from the OCaml handbook if you haven't already!

Initial setup

This section will cover how to set up a Rust crate that is linked into an OCaml program. If you're interested in calling into an OCaml library from Rust instead, see Linking an OCaml library into a Rust program.

Add the following to your Cargo.toml:

[lib]
crate-type = ["staticlib"] # You can also use cdylib, depending on your project

[dependencies]
ocaml = "*"

Additionally, on macOS you may need to add a .cargo/config with the following:

[build]
rustflags = ["-C", "link-args=-Wl,-undefined,dynamic_lookup"]

This is because macOS doesn't allow undefined symbols in dynamic libraries by default.

If you plan on using ocaml-build:

[build-dependencies]
ocaml-build = "*"

And add a build.rs:

extern crate ocaml_build;
pub fn main() -> std::io::Result<()> {
  ocaml_build::Sigs::new("src/rust.ml").generate()
}

This build script will look for usages of #[ocaml::sig(...)] to generate OCaml bindings.

Next you will need to add setup a dune project to handle compilation of your OCaml code. Here is an example dune file that will link your Rust project, in this case the Rust crate is named example:

(rule
 (targets libexample.a)
 (deps (glob_files *.rs))
 (action
  (progn
   (run cargo build --target-dir %{project_root}/../../target --release)
   (run mv %{project_root}/../../target/release/libexample.a libexample.a))))

(library
 (name example)
 (public_name example)
 (foreign_archives example)
 (c_library_flags
  (-lpthread -lc -lm)))

You should also add the following stanza to a dune file at the root of your project to ignore the target directory:

(dirs :standard \ target)

It can take a little trial and error to get this right depending on the specifics of your project!

Additionally, if you plan on releasing to opam, you will need to vendor your Rust dependencies to avoid making network requests during the build phase, since reaching out to crates.io/github will be blocked by the opam sandbox. To do this you should run:

cargo vendor

then follow the instructions for editing .cargo/config

To simplify the full setup process, take a look at ocaml-rust-starter.

Build options

By default, building ocaml-sys will invoke the ocamlopt command to figure out the version and location of the OCaml compiler. There are a few environment variables to control this.

  • OCAMLOPT (default: ocamlopt) is the command that will invoke ocamlopt
  • OCAML_VERSION (default: result of $OCAMLOPT -version) is the target runtime OCaml version.
  • OCAML_WHERE_PATH (default: result of $OCAMLOPT -where) is the path of the OCaml standard library.
  • OCAML_INTEROP_NO_CAML_STARTUP (default: unset) can be set when loading an ocaml-rs library into an OCaml bytecode runtime (such as utop) to avoid linking issues with caml_startup

If both OCAML_VERSION and OCAML_WHERE_PATH are present, their values are used without invoking ocamlopt. If any of those two env variables is undefined, then ocamlopt will be invoked to obtain both values.

Defining the OCAML_VERSION and OCAML_WHERE_PATH variables is useful for saving time in CI environments where an OCaml install is not really required (to run clippy for example).

Features

  • derive
    • enabled by default, adds #[ocaml::func] and friends and derive implementations for FromValue and ToValue
  • link
    • link the native OCaml runtime, this should only be used when no OCaml code will be linked statically
  • no-std
    • Allows ocaml to be used in #![no_std] environments like MirageOS

Writing your first ocaml::func

ocaml::func is the highest-level macro that can be used to generate OCaml functions. It's built on ocaml::native_func which only works on Value parameters and ocaml::bytecode_func which is used for generating bytecode functions. ocaml::func will take care of generating bytecode bindings for functions with more than five parameters as required by the OCaml runtime. ocaml::func handles using CAMLparam/CAMLlocal/CAMLreturn correctly for you, often making it much easier to write bindings than using the C API directly, particularly for those who haven't used the OCaml C API before.

All ocaml::func's have an implicit gc variable which is used to access the OCaml runtime. To pick another name you can provide it as an argument to the ocaml::func macro:

#[ocaml::func(my_gc_name)]
...

The following example will read a file and return the contents, we will ignore error handling for now since that will be covered later - however, one thing worth mentioning is that Rust panics will be converted into OCaml exceptions.

#![allow(unused)]
fn main() {
extern crate ocaml;

#[ocaml::func]   // This is needed to make the function compatible with OCaml
#[ocaml::sig("string -> string")] /// This is used to generate the OCaml bindings
pub unsafe fn read_file(filename: String) -> String {
  std::fs::read_to_string(filename).unwrap()
}
}

In the above example, automatic conversion is performed between OCaml strings and Rust strings. The next section will provide a table of valid conversions before getting into more details about writing functions and calling OCaml functions from Rust.

Type conversion

As mentioned in the previous section, ocaml-rs automates the conversion between Rust and OCaml representations for many types. This is done using two traits: ToValue, which is implemented for types that can be converted to an OCaml value and FromValue for types that can be converted from an OCaml value.

Below is a list of types that implement these traits in ocaml-rs and their corresponding OCaml type:

Rust typeOCaml type
()unit
isizeint
usizeint
i8int
u8int
i16int
u16int
i32int32
u32int32
i64int64
u64int64
f32float
f64float
strstring
[u8]bytes
Stringstring
Option<A>'a option
Result<A, ocaml::Error>'a or exception
Result<A, B>('a, 'b) Result.t
(A, B, C)'a * 'b * 'c
&[Value]'a array (no copy)
Vec<A>, &[A]'a array
BTreeMap<A, B>('a, 'b) list
LinkedList<A>'a list
Seq<A>'a Seq.t

NOTE: Even though &[Value] is specifically marked as no copy, any type like Option<Value> would also qualify since the inner value is not converted to a Rust type. However, Option<String> will do full unmarshaling into Rust types. Another thing to note: FromValue for str and &[u8] is zero-copy, however ToValue for str and &[u8] creates a new value - this is necessary to ensure the string is registered with the OCaml runtime.

If you're concerned with minimizing allocations/conversions you should use Value type directly.

Implementing ToValue and FromValue

The ToValue trait has a single function, to_value which takes a reference to self, a reference to [Runtime] and returns a Value and FromValue has from_value, which takes a Value and returns Self:

#![allow(unused)]
fn main() {
extern crate ocaml;

pub struct MyType(i32);

unsafe impl ocaml::ToValue for MyType {
  fn to_value(&self, _gc: &ocaml::Runtime) -> ocaml::Value {
    unsafe { ocaml::Value::int32(self.0) }
  }
}

unsafe impl ocaml::FromValue for MyType {
  fn from_value(value: ocaml::Value) -> MyType {
    unsafe { MyType(value.int32_val()) }
  }
}
}

This can also be accomplished using the derive macros:

#![allow(unused)]
fn main() {
extern crate ocaml;

#[derive(ocaml::ToValue, ocaml::FromValue)]
pub struct MyType(i32);
}

derive(ToValue, FromValue) will work on any struct or enum that are comprised of types that also implement ToValue and FromValue

Types that work directly on OCaml values

There are several types that work directly on OCaml values, these don't perform any copies when converting to and from Value.

Rust typeOCaml type
ocaml::Array<T>'a array
ocaml::List<T>'a list
ocaml::Seq<T>'a Seq.t
ocaml::bigarray::Array1<T>('a, 'b, c_layout) Bigarray.Array1.t
ocaml::bigarray::Array2<T>('a, 'b, c_layout) Bigarray.Array2.t
ocaml::bigarray::Array3<T>('a, 'b, c_layout) Bigarray.Array3.t

Wrapping Rust values

Rust values can be used as opaque values that can be shared with OCaml using ocaml::Pointer. The Pointer type allows for Rust values to be allocated using the OCaml runtime, this means their lifetime will be handled by the garbage collector. Pointer::alloc_final is used to move an existing Rust type into an OCaml allocated pointer, but even better is the option to implement the Custom trait for your type.

Implementing Custom allows you to define equality/comparison, finalization, hashing and serialization functions for your type that will be used by OCaml. When allocating custom values you should use Pointer::from or Pointer::alloc_custom.

In either case you will need to write the allocation function in Rust because OCaml doesn't know the specifics about the layout or contents of these types, unlike when using FromValue or ToValue. Pointer should primarily be used on Rust values that cannot be converted directly to OCaml types.

#![allow(unused)]
fn main() {
extern crate ocaml;

#[ocaml::sig]   // Creates an opaque type on the OCaml side
pub struct MyType {
  a: i32,
  b: f64,
  c: std::fs::File, // This can't be converted to an OCaml value
}

ocaml::custom!(MyType);

#[ocaml::func]
#[ocaml::sig("my_type -> float")]
pub unsafe fn my_type_add_a_b(t: &MyType) -> f64 {
  t.a as f64 + t.b
}
}

NOTE: In this example the Rust type MyType has automatically been renamed to my_type in OCaml.

Also in this example, the default finalizer is used - this will call Pointer::drop_in_place to call drop on the Rust side before freeing the memory on the OCaml heap. If you add your own finalize implementation you should make sure to call Pointer::drop_in_place any time the underlying Rust value contains dynamically allocated values, like std::fs::File in the example above.

Now that you have some insight into how type conversion is handled, the next section will cover more details about writing OCaml functions in Rust.

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]

Linking an OCaml library into a Rust program

The section will cover how to create a program in Rust that calls functions from an OCaml library.

Example project layout:

  • Cargo.toml
  • build.rs
  • src: contains Rust code
  • lib: contains OCaml code and dune file
  • dune-project
  • example.opam

Add the following to your Cargo.toml:

[dependencies]
ocaml = "*"

[build-dependencies]
ocaml-build = {version = "*", features=["dune"]}

And add a build.rs, this example assumes your OCaml library and the following dune file are in lib at the root of your Rust project:

extern crate ocaml_build;
pub fn main() {
  ocaml_build::Dune::new("lib").build()
}

If the dune root is not the root of your project you can use Dune::with_root to set the correct path.

Next you will need to add setup a dune project to handle compilation of your OCaml code. Here is an example dune file that will generate object files that can be linked into your Rust program, in this case the OCaml library is named example:

(executable
 (name example)
 (public_name example)
 (modes exe object))

NOTE: The OCaml code needs to be built with dune in object mode

In lib/example.ml

let hello_world () = "Hello, world"
let () = Callback.register "hello_world" hello_world

In lib/main.rs:

extern crate ocaml;

ocaml::import! {
  fn hello_world() -> String;
}

pub fn main() {
  let gc = ocaml::init(); // Initialize OCaml runtime

  let s = unsafe { hello_world(&gc).unwrap() };
  println!("{s}");
}

NOTE: ocaml::init needs to be called before attempting to access any OCaml functions.

To simplify the full setup process, take a look at rust-ocaml-starter.