1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158
|
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`:
```console
$ 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).
```{code-block} ocaml
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).
```{code-block} ocaml
: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`:
```{code-block} 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
:::
::::
|