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 invokeocamlopt
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 anocaml-rs
library into an OCaml bytecode runtime (such asutop
) to avoid linking issues withcaml_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 andderive
implementations forFromValue
andToValue
- enabled by default, adds
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
- Allows
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 type | OCaml type |
---|---|
() | unit |
isize | int |
usize | int |
i8 | int |
u8 | int |
i16 | int |
u16 | int |
i32 | int32 |
u32 | int32 |
i64 | int64 |
u64 | int64 |
f32 | float |
f64 | float |
str | string |
[u8] | bytes |
String | string |
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 type | OCaml 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::PointerPointer
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
- 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]
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 codelib
: contains OCaml code anddune
filedune-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.