OCaml Module 13 – Runtime, performance and interoperability
Interfacing with C
How OCaml calls C: external declarations, C stubs that read and allocate OCaml values under the garbage collector, dune foreign_stubs, and ctypes.
What you will learn
- Declare external functions and write C stubs with CAMLparam, CAMLlocal and CAMLreturn
- Follow the garbage-collector rules for allocating OCaml values inside a stub
- Build stubs with dune's foreign_stubs, raise OCaml exceptions from C, and avoid boxing
- Compare hand-written stubs with ctypes, and read an OCaml 5.5 external type
Before you start
What you’ll learn · Before you start
What you will learn
- Declare external functions and write C stubs with CAMLparam, CAMLlocal and CAMLreturn
- Follow the garbage-collector rules for allocating OCaml values inside a stub
- Build stubs with dune's foreign_stubs, raise OCaml exceptions from C, and avoid boxing
- Compare hand-written stubs with ctypes, and read an OCaml 5.5 external type
Before you start
On this page
Sometimes the routine you need is already written in C: a checksum, a numeric kernel, a system call, or a whole
library. OCaml reaches it through a foreign function interface: A way for code in one language to call functions written in another. In OCaml, you declare a C function with external and link a small C stub that bridges the calling conventions.: you
declare the function with external, write a small C stub: A small C function that connects OCaml to a C routine: it reads its OCaml arguments with the runtime's macros, does the work in C, and returns an OCaml value, cooperating with the garbage collector when it allocates. that connects OCaml’s calling
conventions to C’s, and let the build compile and link it. The reward is a C function that looks like any other OCaml
function; the price is that the stub steps outside the type checker and the garbage collector, so a mistake there is a
memory bug, not a type error. This lesson writes three stubs that run, and draws the rules that keep them safe.
An external declaration names a C function
An external declaration binds an OCaml name to a C function. It gives the OCaml type and, as a string, the C
function’s name:
adler32/main.ml
(* The external declaration links the OCaml name adler32 to the C
function tv_adler32. From here on adler32 is an ordinary function
of type string -> int. *)
external adler32 : string -> int = "tv_adler32"
let () =
let show s = Printf.printf "adler32 %-11S = 0x%08x\n" s (adler32 s) in
show "Wikipedia";
show "";
show "a";
show "abc" adler32/adler32_stub.c
/* A C stub for OCaml: the Adler-32 checksum of a string.
It reads the OCaml string and returns a plain OCaml int, so it
allocates nothing on the OCaml heap and needs no GC registration. */
#include <caml/mlvalues.h>
CAMLprim value tv_adler32(value s)
{
const unsigned char *p = (const unsigned char *) String_val(s);
mlsize_t len = caml_string_length(s);
unsigned long a = 1, b = 0;
for (mlsize_t i = 0; i < len; i++) {
a = (a + p[i]) % 65521;
b = (b + a) % 65521;
}
return Val_long((b << 16) | a);
} adler32/dune
(executable
(name main)
(foreign_stubs
(language c)
(names adler32_stub))) From the call’s point of view, adler32 is an ordinary function of type string -> int. The compiler emits a call
to tv_adler32; it never sees the C body, so the declared type is a promise it trusts rather than checks.
The stub reads the string with String_val and caml_string_length, runs an ordinary C loop, and returns the result
tagged as an OCaml int with Val_long. It allocates nothing on the OCaml heap, so the garbage collector cannot
interfere. Step through one call as it crosses into C and back:
The drawing shows the whole scene; its 4 steps are listed below.
Every step as text
- The call crosses over. main.ml calls adler32 "Wikipedia". Because adler32 was declared external, the compiled call goes to the C function tv_adler32, with the OCaml string handed over as the one argument.
- Read the OCaml string. The stub cannot touch the bytes directly. String_val gives a pointer to them and caml_string_length gives the length, so C can read the nine characters of the string.
- Work in plain C. With the bytes in hand the stub runs the Adler-32 loop over two running sums in ordinary C. It allocates nothing on the OCaml heap, so the garbage collector cannot interfere here.
- Return an OCaml int. The result is a small integer, so Val_long turns the C number into an OCaml int and the stub returns it. Back in OCaml, adler32 "Wikipedia" is 0x11e60398.
Build it with dune and run it: the checksum of "Wikipedia" is 0x11e60398, and the empty string gives
0x00000001, because RFC 1950, which defines Adler-32, starts its two sums at 1 and 0.
from session import files, run
# adler32/: a dune executable whose only C file, adler32_stub.c, holds one
# stub, tv_adler32. main.ml declares `external adler32 : string -> int =
# "tv_adler32"` and prints the checksum of four strings. (foreign_stubs
# (language c) (names adler32_stub)) in the dune file compiles and links the
# C file. The checksum of "Wikipedia" is the value printed in the Adler-32
# article, 0x11e60398.
files(
"adler32/dune-project=dune-project",
"adler32/dune=dune",
"adler32/adler32_stub.c=adler32_stub.c",
"adler32/main.ml=main.ml",
)
run("""
ls
dune build 2>&1
dune exec ./main.exe
""") Output
$ ls adler32_stub.c dune dune-project main.ml $ dune build 2>&1 $ dune exec ./main.exe adler32 "Wikipedia" = 0x11e60398 adler32 "" = 0x00000001 adler32 "a" = 0x00620062 adler32 "abc" = 0x024d0127
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 adler32_session.py
Key idea
An external declaration is a promise, not a check: the compiler trusts the OCaml type you give and calls the named C function directly, so the type must match the C function exactly.
Checkpoint
Check: Declaring an external function
A stub reads OCaml values and returns one
Every argument a stub receives is an OCaml value: one machine word that is either an immediate (an int, a char,
a constant constructor, with its low bit set to 1) or a pointer to a heap block (a string, an array, a record, a
tuple). A stub never touches that word directly; it uses the runtime’s macros, which the manual’s chapter on
interfacing C with OCaml defines. The common ones:
| To do this | Use | Notes |
|---|---|---|
Read an int argument |
Long_val(v) |
the reverse of Val_long |
Return an int |
Val_long(n) |
tags a C integer as an immediate |
Read a string’s bytes |
String_val(v) |
a pointer into the heap block |
| Its length | caml_string_length(v) |
counts null bytes too, unlike strlen |
Read a float from a float array |
Double_field(v, i) |
the array is unboxed |
Return a boxed float |
caml_copy_double(x) |
allocates on the heap |
The Adler-32 stub used only the first rows: it read a string and returned an immediate. Because it allocated nothing and held no OCaml value across an allocation, it needed no further ceremony. The moment a stub allocates, a second set of rules begins, and that is where most stub bugs live.
A value is not a C pointer
Reading a string argument with strlen or casting it to char * and keeping it is wrong twice over: an OCaml
string may contain null bytes, so strlen gives the wrong length, and the garbage collector may move the block, so
a saved C pointer can dangle. Use String_val and caml_string_length each time you need them.
Checkpoint
Check: Reading and returning OCaml values
Practise: break the stub on purpose
Work with the adler32/ example, and predict each result before you build:
- Add
show "a\000b"tomain.ml, a three-byte string whose middle byte is null. The checksum of those bytes is0x018800c4. Then replacecaml_string_length(s)in the stub bystrlen((const char *) String_val(s))and predict the new value:strlenstops at the null byte, so only the byteais summed, and the checksum of"a"is0x00620062. - Change the OCaml type of
adler32tobytes -> intand say whetherdune buildfails, and in which file. The compiler checks every use ofadler32inmain.mlagainst the declared type, so the string passed byshowis refused there, while the C file is never consulted.
Then test yourself on the declaration and on reading values:
Practice that adapts to you
Practise this lesson
4 questions about 5 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.
Interview questions
These questions come up when a team adds its first C binding. Work out your own answer, then open the model answer to compare the reasoning and the fix.
-
Mid to senior
A hand-written C stub returns a record built with caml_alloc. Under load it sometimes returns garbage or crashes, but never in small tests. What rule is most likely broken, and how would you find and fix it?
-
Mid to senior
A colleague marks a numeric stub [@@noalloc] to speed it up, but the stub calls caml_copy_double to return a float. The program passes every test, then crashes in production. Explain the connection.
-
Mid to staff
Your team must bind a C library of about forty functions, mostly taking ints, doubles and pointers, to OCaml. Would you write hand stubs or use ctypes, and what decides it?
The garbage-collector rules for a stub that allocates, and how to raise an OCaml exception from C Pro: opens with a Pro, Premium or Ultimate pass
A stub that allocates on the OCaml heap has to cooperate with the garbage collector, because allocation is exactly what can start a collection, and a minor collection moves the young values the stub is still holding. The rules are few and mechanical, and once followed they make an allocating stub as safe as a pure-OCaml function.
In this part:
- Registering values, and raising from a stub
1 diagram · 2 runnable examples (Python, main.ml, min_max_stub.c) · about 6 minutes
Opening this part with your pass…
This part opens with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
Building stubs with dune's foreign_stubs, and the attributes that avoid boxing on numeric calls Pro: opens with a Pro, Premium or Ultimate pass
Writing the stub is half the job; the build has to compile the C file with the OCaml runtime’s headers on its include path and link it into the executable or library. dune does this with one stanza, and OCaml has three attributes that make a numeric stub avoid boxing its arguments and its result.
In this part:
- Building and linking stubs with dune
2 runnable examples (Python, dune, main.ml, popcount_stub.c) · about 4 minutes
Opening this part with your pass…Pro part. It opens with the same pass as the part above.
Binding C without writing stubs: ctypes, and reading an OCaml 5.5 external type Pro: opens with a Pro, Premium or Ultimate pass
Hand-written stubs are not the only way to reach C. The ctypes library describes a C function’s signature in OCaml
and calls it with no C file at all, which changes the trade-off for larger bindings. OCaml 5.5 also added a small
type-system feature, external types, that names the foreign-defined types such a binding works with.
In this part:
- Binding C without writing the stubs
about 7 minutes
Opening this part with your pass…Pro part. It opens with the same pass as the part above.
Flashcards · 4 cards
Recall it, then check
Say the answer to yourself before you look: recalling it is what makes it stick.
-
How does OCaml call a routine that is already written in C?
Through its foreign function interface: declare the function with external, write a small C stub that connects OCaml's calling conventions to C's, and let the build compile and link it.
-
What does the compiler check about an external declaration's type?
Nothing: it trusts the type you give and calls the C function directly, so a wrong type is a memory bug, not a type error.
-
How does a C stub read its arguments and return an integer?
Its arguments are OCaml values read through macros such as String_val and Long_val, and it returns an immediate with Val_long.
-
Why must a stub not read an OCaml string with strlen, or keep its char *?
An OCaml string may contain null bytes, so strlen can give the wrong length, and the collector may move the block, so a saved pointer can dangle. Use String_val and caml_string_length each time.
Key takeaways
- An
externalbinds an OCaml name to a C function and gives a type the compiler trusts without checking, so a wrong type is a memory bug, not a type error. - A stub reads its arguments as OCaml
values through macros likeString_valandLong_valand returns an immediate withVal_long; a stub that only reads and returns needs no further ceremony. - A
stringargument is read withString_valandcaml_string_length, never withstrlen, because an OCaml string may contain null bytes and the block may move.
Terms in this lesson
- C stub
- A small C function that connects OCaml to a C routine: it reads its OCaml arguments with the runtime's macros, does the work in C, and returns an OCaml value, cooperating with the garbage collector when it allocates.
- foreign function interface
- A way for code in one language to call functions written in another. In OCaml, you declare a C function with external and link a small C stub that bridges the calling conventions.
References
- The OCaml Manual: Interfacing C with OCaml (Inria and the OCaml developers)
- The OCaml Manual: External types (Inria and the OCaml developers)
- dune documentation: Dealing with foreign libraries (The dune developers)
- dune documentation: foreign_stubs reference (The dune developers)
- RFC 1950: ZLIB Compressed Data Format Specification (Adler-32) (IETF)
- ctypes-foreign package (OCaml.org)
- ocaml-ctypes: library documentation (Jeremy Yallop and contributors)
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress