mte/unikernel/duniverse/dune_/doc/tutorials/developing-with-dune/interfacing-with-c.md
2025-11-11 02:07:51 +01:00

3.8 KiB

Interfacing with C

In this chapter, we're going to extend our calculator with a new function. The difference with sin is that we're going to use a C implementation of the function because it is not available in Stdlib. To do so, we're going to use {doc}(foreign_stubs) </reference/foreign-stubs> to implement the function in C.

Create a test

Add a new test in test/calc.t:

  $ calc -e 'log10(123456)'

Run dune runtest and see the failure. Run dune promote to add the failure to the test file. Our goal in the rest of the chapter is to change the output of this test.

Lexing

We have a tiny change to make in lib/lexer.mll: extend function names so that they can contain numbers (but not at the beginning).

let ident = letter (letter | digit)+

Evaluation

Let's extend our eval function in lib/cli.ml to handle a log10 function. Instead of implementing the function in OCaml, we declare it as an external (with its type).

:emphasize-lines: 1,12
external log10_c : float -> float = "calc_log10"

let rec eval = function
  | Ast.Int n -> VInt n
  | Float f -> VFloat f
  | Ident "pi" -> VFloat (2. *. Stdlib.acos 0.)
  | Ident _ -> failwith "unknown ident"
  | Op (Add, a, b) -> eval_number_op ( + ) ( +. ) (eval a) (eval b)
  | Op (Mul, a, b) -> eval_number_op ( * ) ( *. ) (eval a) (eval b)
  | Op (Div, a, b) -> eval_number_op ( / ) ( /. ) (eval a) (eval b)
  | Call ("sin", e) -> VFloat (Stdlib.sin (as_float (eval e)))
  | Call ("log10", e) -> VFloat (log10_c (as_float (eval e)))
  | Call _ -> failwith "unknown function"

Create Foreign Stubs

The final part is to implement calc_log10 as a C function and link it with our library.

Let's create a file lib/calc_stubs.c:

:::{literalinclude} interfacing-with-c/lib/calc_stubs.c :language: c :::

:::{note}

The interface between C and OCaml code uses a C type called value.

Values of this type can be converted from and to double using Double_val and caml_copy_double.

They need to be registered with the garbage collector using the CAMLparam1 and CAMLreturn macros. :::

And we finally we specify to Dune that this file is part of the library in lib/dune:

:emphasize-lines: 4-6
(library
 (name calc)
 (libraries cmdliner)
 (foreign_stubs
  (language c)
  (names calc_stubs)))

Run the tests again with dune runtest. At that point, the output should be correct. Call dune promote to update the expected output.

Conclusion

In this chapter, we've extended our library with some C code. The mechanism to do so is called {doc}foreign stubs </reference/foreign-stubs>.

::::{dropdown} Checkpoint :icon: location

This is how the project looks like at the end of this chapter.

:::{literalinclude} introduction/dune-project :caption: dune-project (unchanged) :language: dune :::

:::{literalinclude} structure/bin/dune :caption: bin/dune (unchanged) :language: dune :::

:::{literalinclude} structure/bin/calc.ml :caption: bin/calc.ml (unchanged) :language: ocaml :::

:::{literalinclude} interfacing-with-c/lib/dune :caption: lib/dune :language: dune :::

:::{literalinclude} development-cycle/lib/ast.ml :caption: lib/ast.ml (unchanged) :language: ocaml :::

:::{literalinclude} interfacing-with-c/lib/calc_stubs.c :caption: lib/calc_stubs.c :language: c :::

:::{literalinclude} interfacing-with-c/lib/cli.ml :caption: lib/cli.ml :language: ocaml :::

:::{literalinclude} interfacing-with-c/lib/lexer.mll :caption: lib/lexer.mll :language: ocaml :::

:::{literalinclude} development-cycle/lib/parser.mly :caption: lib/parser.mly (unchanged) :language: ocaml :::

:::{literalinclude} structure/test/dune :caption: test/dune (unchanged) :language: dune :::

:::{literalinclude} interfacing-with-c/test/calc.t :caption: test/calc.t :language: cram :::

::::