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-interop - Interactions with the OCaml runtime
  • 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.