Skip to content
Anil Madhavapeddy edited this page Jul 7, 2020 · 10 revisions

Note: This page is preserved for historical reasons. We no longer require changes to the C API with the "parallel minor collector" that is the default in multicore OCaml now. See this paper for more details about the experiments that lead to this decision. The below text applies only to the old concurrent minor collector that is no longer being actively upstreamed.

While we aim to preserve backwards-compatibility with OCaml code, the multicore OCaml implementation breaks some of the API used to interface C and OCaml code. For background on this API, see Chapter 19 of the OCaml Manual.

Field access

In order to port C bindings to use the multicore API, these replacements need to be made:

Stock OCaml Multicore OCaml
a = Field(x, i); a = Field_imm(x, i); (if known immutable), or
caml_read_field(x, i, &a); (otherwise)
caml_modify(&Field(x, i), a); caml_initialize_field(x, i, a); (if initializing)
caml_modify_field(x, i, a); (if modifying)
caml_initialize(&Field(x, i), a); caml_initialize_field(x, i, a);
Field(x, i) = a; caml_initialize_field(x, i, a);

NB: Unlike stock OCaml's caml_initialize, multicore's caml_initialize_field may call the GC, and so all values must be protected (with CAMLparamN or similar) before using it.

Use of the following convenience functions is not strictly necessary, but they will perform slightly better than the alternative in cases where they apply:

Stock OCaml Multicore OCaml
n = Int_val(Field(x, i)); n = Int_field(x, i); (whether mutable or not)
n = Long_val(Field(x, i)); n = Long_field(x, i); (whether mutable or not)
pair = caml_alloc_small(2, 0);
Field(pair, 0) = a;
Field(pair, 1) = b;
pair = caml_alloc_2(0, a, b);

The convenience functions caml_alloc_<N> are defined for N < 10.

Explanation

The major change to the C API is the removal of Field(x, i).

In stock OCaml, local roots are exposed to the GC (using CAMLlocalN and similar), so that if a minor collection occurs during a call to e.g. caml_alloc, all pointers which need to be moved are visible to the GC. In multicore OCaml, the roots must also be exposed to the GC when reading a mutable field, since the read barrier may need to promote objects.

This presents a problem for the Field(x, i) API, since code like this is currently valid:

foo(Field(x, 0), Field(x, 1));

If both fields are mutable, then the call to Field(x,1) could require that some values be promoted. If Field(x, 0) has already been evaluated into some temporary location, then that location will not be visible to the GC, and will not be relocated should promotion require it.

So, in the multicore API, Field(x, i) is split into two separate operations Field_imm and caml_read_field, depending on whether the field in question is immutable or mutable. For immutable fields, the change is simply to replace Field with Field_imm:

foo(Field_imm(x, 0), Field_imm(x, 1));

For mutable fields, calls to caml_read_field must be inserted. Instead of returning a value, this function returns void but takes a value* into which the result is stored. This style of API forces the user to declare a value variable (using CAMLlocalN), ensuring that the temporary value is exposed to the GC:

CAMLlocal2(a,b);
caml_read_field(x, 0, &a);
caml_read_field(x, 1, &b);
foo(a, b);

If it is unclear in a given context whether a field is mutable, caml_read_field can always be used. Field_imm is an optimised version for fields known to be immutable.

As a convenience for fields known to contain integer values (to which promotion does not apply), the function Int_field(x, i) is provided, which can operate on mutable fields as well as immutable, and is equivalent to Int_val(Field(x,i)) in the stock OCaml API. Long_field works similarly.

Field is also used as an lvalue, when modifying fields:

caml_modify(&Field(x, i), a);

In the multicore API, this instead uses the new function caml_modify_field:

caml_modify_field(x, i, a);

Similarly, caml_initialize becomes caml_initialize_field. Finally, there is one case in which Field is directly assigned to, which is when initialising blocks allocated with the low-level caml_alloc_small function:

pair = caml_alloc_small(2, 0);
Field(pair, 0) = a;
Field(pair, 1) = b;

In the multicore API, such initialisations use caml_initialize_field instead:

pair = caml_alloc_small(2, 0);
caml_initialize_field(pair, 0, a);
caml_initialize_field(pair, 1, a);

For initialising small fixed-size structures like pairs, this is marginally less efficient than in stock OCaml. However, efficient convenience functions are provided for this use, which are also more concise than the stock OCaml API:

pair = caml_alloc_2(0, a, b);

Other changes

There are a smaller number of breaking changes in less-commonly-used parts of the API. The most important of these is changes to how global roots are defined, and how values registered for callbacks are accessed. These changes are still slightly in flux, but you can see the current versions in memory.h and callback.h