Skip to content

zshipko/ocaml-rs

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

ocaml-rs - OCaml extensions in Rust

ocaml-rs allows for OCaml extensions to be written directly in Rust with no C stubs. It was originally forked from raml, but has been almost entirely re-written thanks to support from the OCaml Software Foundation.

Works with OCaml versions 4.06.0 and up

Please report any issues on github

NOTE: While ocaml-rs can be used safely, it does not prevent a wide range of potential errors or mistakes. It should be thought of as a Rust implementation of the existing C API. ocaml-interop can be used to perform safe OCaml/Rust interop. The latest version of ocaml-rs actually uses ocaml-interop behind the scenes to interact with the garbage collector. ocaml-rs also exports an interop module, which is an alias for ocaml_interop and the two interfaces can be combined if desired.

Getting started

Take a look at the ocaml-rust-starter project for a basic example to help get started with ocaml-rs.

On the Rust side, you will need to add the following to your Cargo.toml:

ocaml = "*"

or

ocaml = {git = "https://github.com/zshipko/ocaml-rs"}

For macOS you will need also to add the following to your project's .cargo/config file:

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

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

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

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 IntoValue
  • 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

Documentation

https://docs.rs/ocaml

Examples

// Automatically derive `IntoValue` and `FromValue`
#[derive(ocaml::IntoValue, ocaml::FromValue)]
#[ocaml::sig("{name: string; i: int}")]
struct Example<'a> {
    name: &'a str,
    i: ocaml::Int,
}


#[ocaml::func]
#[ocaml::sig("example -> example")]
pub fn incr_example(mut e: Example) -> Example {
    e.i += 1;
    e
}

#[ocaml::func]
#[ocaml::sig("int -> int * int * int")]
pub fn build_tuple(i: ocaml::Int) -> (ocaml::Int, ocaml::Int, ocaml::Int) {
    (i + 1, i + 2, i + 3)
}

#[ocaml::func]
#[ocaml::sig("float array -> f64")]
pub fn average(arr: ocaml::Array<f64>) -> Result<f64, ocaml::Error> {
    let mut sum = 0f64;

    for i in 0..arr.len() {
        sum += arr.get_f64(i)?;
    }

    Ok(sum / arr.len() as f64)
}

// A `native_func` must take `ocaml::Value` for every argument and return an `ocaml::Value`
// these functions have minimal overhead compared to wrapping with `func`
#[ocaml::native_func]
pub fn incr(value: ocaml::Value) -> ocaml::Value {
    let i = value.int_val();
    ocaml::Value::int(i + 1)
}

// This is equivalent to:
#[no_mangle]
pub extern "C" fn incr2(value: ocaml::Value) -> ocaml::Value {
    ocaml::body!(gc: (value) {
        let i = value.int_val();
        ocaml::Value::int( i + 1)
    })
}

// `ocaml::native_func` is responsible for:
// - Ensures that #[no_mangle] and extern "C" are added, in addition to wrapping
// - Wraps the function body using `ocaml::body!`

// Finally, if your function is marked [@@unboxed] and [@@noalloc] in OCaml then you can avoid
// boxing altogether for f64 arguments using a plain C function and a bytecode function
// definition:
#[no_mangle]
pub extern "C" fn incrf(input: f64) -> f64 {
    input + 1.0
}

#[cfg(feature = "derive")]
#[ocaml::bytecode_func]
pub fn incrf_bytecode(input: f64) -> f64 {
    incrf(input)
}

Note: By default the func macro will create a bytecode wrapper (using bytecode_func) for functions with more than 5 arguments.

The OCaml stubs would look like this:

type example = {
    name: string;
    i: int;
}

external incr_example: example -> example = "incr_example"
external build_tuple: int -> int * int * int = "build_tuple"
external average: float array -> float = "average"
external incr: int -> int = "incr"
external incr2: int -> int = "incr2"
external incrf: float -> float = "incrf_bytecode" "incrf" [@@unboxed] [@@noalloc]

Excluding the incrf example, these can also be generated using ocaml-build

For more examples see test/src or ocaml-vec.

Type conversion

This chart contains the mapping between Rust and OCaml types used by ocaml::func

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, B> exception
(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

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 IntoValue 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.

Pointers to Rust values on the OCaml heap

Pointer<T> can be used to create and access Rust types on the OCaml heap.

For example, for a type that implements Custom:

use ocaml::FromValue;

struct MyType;

unsafe extern "C" fn mytype_finalizer(v: ocaml::Raw) {
    let ptr = v.as_pointer::<MyType>();
    ptr.drop_in_place()
}

ocaml::custom_finalize!(MyType, mytype_finalizer);

#[ocaml::func]
pub fn new_my_type() -> ocaml::Pointer<MyType> {
    ocaml::Pointer::alloc_custom(gc, MyType)
    // ocaml::Pointer::alloc_final(gc, MyType, finalizer, None) can also be used
    // if you don't intend to implement `Custom`
}

#[ocaml::func]
pub fn my_type_example(t: ocaml::Pointer<MyType>) {
    let my_type = t.as_mut();
    // MyType has no fields, but normally you
    // would do something with MyType here
}

Custom exception type

When a Rust panic or Err is encountered it will be raised as a Failure on the OCaml side, to configure a custom exception type you can register it with the OCaml runtime using the name Rust_exception:

exception Rust

let () = Callback.register_exception "Rust_error" (Rust "")

It must take a single string argument.

Using import! macro

The ocaml::import! macro can be used to call OCaml functions that have been registered using Callback.register from Rust:

ocaml::import! {
  fn my_function(i: ocaml::Int) -> ocaml::Int;
}

Which can then be called directly:

my_function(&rt, 123)

ocaml::sig macro

The ocaml::sig macro in combination with ocaml-build can be used to write signatures for OCaml functions and types in-line.

See build/README.md