Brr FFI manual

This manual describes how OCaml and JavaScript values are represented by js_of_ocaml and Brr. The companion FFI cookbook has a few tips and off-the-shelf design answers for common JavaScript bindings scenarios.

An OCaml value compiled by the js_of_ocaml is encoded as a JavaScript value (see this paper for outdated yet interesting details). JavaScript does not understand this encoding, conversly OCaml does not understand JavaScript values directly. The foreign function interface (FFI) reconciles these views.

Foreword

The official js_of_ocaml JavaScript foreign function interface encodes and structurally types JavaScript objects as OCaml object phantom types. This is a very neat trick and topped with the custom syntax offered by js_of_ocaml-ppx this means that you can write perfectly typed and idiomatic JavaScript code in OCaml with unscrutable type error messages.

We are not keen to program in JavaCamlScript. We are not keen to have to use the round corner of the OCaml language. We are not keen to use an ad-hoc syntax powered by the horrific and brittle ppx system. We want to tap into the browser APIs for the functionality they provide, not for the programming idioms they propose.

In particular we are not attached to the JavaScript object system and do not feel the need to model it into OCaml types. In Brr we simply hide JavaScript objects behind abstract OCaml types which are acted upon using regular OCaml functions. JavaScript being quite sane about its object-orientation, the occasional mixin or inheritance relationship can be handled with explicit coercions functions.

This approach exposes the browser APIs in a simple way for both newcomers and working OCaml programmers who can harness the power of the excellent work that went into the js_of_ocaml compiler and its runtime without the need to submit to ppx and less travelled parts of the language.

JavaScript values

JavaScript values are represented in OCaml programs by the Jv.t type. A value of this type represents any JavaScript value: null, undefined, a boolean, a number, a string, an array, an object, a function, etc.

Except for JavaScript strings which are represented by values of type Jstr.t nothing is done to type JavaScript values beyond this universal type. JavaScript bindings glue is in charge of manipulating Jv.t values of specific object types and expose them as type safe interfaces by using OCaml abstract types.

The Jv.repr function returns the JavaScript representation of any OCaml value, it is the moral equivalent of Obj.repr for the JavaScript encoding of OCaml values made by js_of_ocaml compilation.

Equality

The Jv module provides access to JavaScript equality operators on Jv.t values:

The OCaml ( = ) structural equality is implemented as per OCaml semantics on OCaml values. What it does on JavaScript values does not seem to be properly documented (get in touch if you know any better) – you will have to make sense of the source.

The OCaml ( == ) physical equality is compiled to JavaScript's strict equality ===.

To sum up we have:

OCaml             Compiled JavaScript
---------------------------------------
Jv.equal          ==
Jv.strict_equal   ===
( = )             caml_equal
( == )            ===

Null and undefined

Values of type Jv.t can always be null or undefined. In what follows we call safe a value that is guaranteed not be null or undefined and unsafe one that may be.

OCaml got rid of null pointer errors so don't let these values go back haunt your stack traces. Make sure to always handle them immediately in the context where they occur – otherwise they will propagate and blow up in your face at unrelated points in your code.

null is represented by Jv.null and undefined by Jv.undefined. You can test for them with the Jv.is_null and Jv.is_undefined predicates which make sure to use the correct JavaScript equality function.

let is_null = Jv.is_null jv            (* true iff [jv] is null *)
let is_undefined = Jv.is_undefined jv  (* true iff [jv] is undefined *)

In general it's a good idea to defensively test for both; the functions Jv.is_none and Jv.is_some do that directly.

let is_none = Jv.is_none jv  (* true iff null or undefined *)
let is_some = Jv.is_some jv  (* false iff null or undefined *)

For APIs that use these values to denote absence of values, use the Jv.to_option function when you convert Jv.t values to OCaml types. It handles null and undefined by mapping them to None.

let safe_int : int option = Jv.to_option Jv.to_int jv
let safe_jv : Jv.t option = Jv.to_option Fun.id jv

If you are on the way from OCaml to JavaScript you have to choose to map None values to one of null, undefined or something else. The none argument of Jv.of_option specifies this:

let jv = Jv.of_option ~none:Jv.null Fun.id v         (* None is null *)
let jv = Jv.of_option ~none:Jv.undefined Fun.id v    (* None is undefined *)
let jv = Jv.of_option ~none:Jstr.empty Jv.of_jstr v  (* None is empty *)

Booleans

Values of type Jv.t can represent JavaScript booleans.

Jv.of_bool and Jv.to_bool convert them with OCaml bools. Jv.to_bool is unsafe, it does not check for null or undefined. If needed combine with Jv.is_none:

let safe_bool : bool = if Jv.is_none jv then false else Jv.to_bool jv

OCaml bool values are not represented by JavaScript booleans. Make sure not to directly give a JavaScript boolean to an OCaml function expecting a bool value and vice-versa; always go through the conversion functions.

numbers

Values of type Jv.t can represent JavaScript numbers.

Jv.of_int and Jv.to_int convert them with OCaml ints. Jv.of_float and Jv.to_float convert them with OCaml floats. Both Jv.to_int and Jv.to_float are unsafe, they do not check for null or undefined. If needed combine them with Jv.of_option:

let i : int option = Jv.to_option Jv.to_int jv

The conversions are lossless, except if you convert a non-integral JavaScript number with Jv.to_int.

OCaml int and float values are directly represented by JavaScript numbers. This means the conversion are nops. Nevertheless use the conversion functions to insulate yourself of changes js_of_ocaml might make in the future.

Strings

Values of type Jv.t can represent JavaScript strings.

JavaScript strings are immutable sequences of UTF-16 encoded Unicode text. OCaml strings are immutable sequences of bytes and nowadays assumed to be UTF-8 encoded text when interpreted as textual content.

Because of this difference we use a dedicated data type Jstr.t for JavaScript strings. Values of this type directly represent a JavaScript String object. Use Jstr.t values to represent the strings returned to you by JavaScript APIs, not OCaml strings. This avoids constantly converting representations between UTF-16 and UTF-8.

The Jstr.v function takes an UTF-8 encoded OCaml string and translates it to an UTF-16 encoded JavaScript string. By UTF-8 encoding OCaml sources this almosts gives us a literal notation for JavaScript strings:

let s : Jstr.t = Jstr.v "A JavaScript string"

If the OCaml string is only made of US-ASCII characters like above the js_of_ocaml compiler compiles the call and OCaml string literal directly to a JavaScript string literal. However if the literal has non US-ASCII Unicode characters, a runtime conversion occurs for now (see this issue):

let s : Jstr.t = Jstr.v "🐫" (* UTF-8 to UTF-16 conversion at runtime *)

To convert a JavaScript string to an UTF-8 encoded OCaml string use Jstr.to_string.

Conversion between Jv.t values and Jstr.t are nops, but do use the Jv.of_jstr and Jv.to_jstr functions.

Arrays

Values of type Jv.t can represent JavaScript arrays.

The Jv.Jarray module provides functions to directly manipulate them. In general you will want to convert them to OCaml arrays or lists, the functions Jv.to_array, Jv.of_array, Jv.to_list, Jv.of_list do this aswell as a few specialized conversion functions.

JavaScript arrays and OCaml lists and arrays are represented differently, these conversions are not free.

Objects

Values of type Jv.t can represent JavaScript objects.

The global object

The global object is represented by the Jv.global value. This object is used to access the global scope in window and non-window contexts. For example to look for functions, object constructors or global values.

Properties

Functions Jv.find, Jv.get, Jv.set and Jv.delete operate on object properties of Jv.t values. Jv.get returns undefined if the property is undefined and null if it is defined but null; use Jv.find to safely map these cases to None.

let get_prop o = Jv.get o "prop"
let set_prop o v = Jv.set o "prop" v
let delete_prop o = Jv.delete o "prop"
let find_prop o = Jv.find o "prop" (* handles [null] and [undefined] *)

These property functions return and take Jv.t values. In practice you have to further convert these values to the types they represent. For example for an int property:

let length o = Jv.to_int (Jv.get o "length")
let set_length o l = Jv.set o "length" (Jv.of_int l)

To make these conversions more streamlined for basic types, Jv provides the Jv.Bool, Jv.Int, Jv.Float and Jv.Jstr submodules which have property functions converting directly with the corresponding OCaml types. Using Jv.Int the example above rewrites to:

let length o = Jv.Int.get o "length"
let set_length l = Jv.Int.set o "length" l

An few other example:

let name o = Jv.Jstr.get o "name"
let set_name o s = Jv.Jstr.set o "name" s
let pi = Jv.Float.get (Jv.get Jv.global "Math") "PI"

Unicode property names

Most object property names in APIs are made only of US-ASCII characters. For these properties the functions seen so far work perfectly.

However if you do hit property names that have arbitrary Unicode characters you cannot use these. You need to use these primed primitives: Jv.get', Jv.set' and Jv.delete'. These functions take a Jstr.t for the property name:

let pi2_prop = Jstr.v "π²" (* make sure we don't convert on each call *)
let pi2 o = Jv.to_float (Jv.get' o pi2_prop)

Creating

A new object can be created via Jv.obj which simply takes an array of name/value pairs:

let o = Jv.obj Jv.[| "length", of_int 3; "name", of_jstr (Jstr.v "Ha!") |]

If you need to handle full Unicode names use Jv.obj':

let pi2_prop = Jstr.v "π²" (* make sure we don't convert on each call *)
let o = Jv.obj' Jv.[| pi2_prop, of_float (pi *. pi) |]

Creating with constructors

A new object is created with a constructor by first looking the constructor function in the global object and then call it with Jv.new':

let date = Jv.get Jv.global "Date"
let date_of_ptime_ms ms = Jv.new' date [| Jv.of_float ms |]

Calling methods

To call a method on a object, construct an OCaml array of Jv.t values representing the method arguments and use Jv.call on the object

let to_jstr o = Jv.to_jstr (Jv.call o "toString" [||])

Functions

Values of type Jv.t can represent JavaScript functions and closures.

To call a function, look it up in the global object, construct an OCaml array of Jv.t values representing the arguments and invoke Jv.apply.

let atob = Jv.get Jv.global "atob"
let base64 s = Jv.to_jstr @@ Jv.apply atob Jv.[| of_jstr s |]

For information about calling back from JavaScript to OCaml see the cookbook.

Errors and exceptions

Values of type Jv.t can represent JavaScript Error objects.

The Jv.Error module has a dedicated type and functions to handle them. Use the Jv.of_error and Jv.to_error functions to convert them with Jv.t objects.

JavaScript exceptions are thrown in your face as the OCaml Jv.Error which holds a Jv.Error.t value. So handling JavaScript exceptions is just a matter of catching that exception:

let result_of_raising f v = match f v with
| exception (Jv.Error e) -> Error (Jv.Error.message e)
| v -> Ok v

If you want to throw a JavaScript exception yourself from OCaml code use Jv.throw.

Promises

Values of type Jv.t can represent JavaScript promise objects.

The Jv.Promise module has a type and few functions to handle them directly. However Brr uses Fut values to safely type them. This is the module you should use to interact with JavaScript promises, see the cookbook for explanations.