Ctypes enum, how to make it work

I try to bind C enums with Ctypes but I am not able to make it work. Here is an example :

In the GLib-2 library, threre is this enumeration :

typedef enum
{
  G_BOOKMARK_FILE_ERROR_INVALID_URI,
  G_BOOKMARK_FILE_ERROR_INVALID_VALUE,
  G_BOOKMARK_FILE_ERROR_APP_NOT_REGISTERED,
  G_BOOKMARK_FILE_ERROR_URI_NOT_FOUND,
  G_BOOKMARK_FILE_ERROR_READ,
  G_BOOKMARK_FILE_ERROR_UNKNOWN_ENCODING,
  G_BOOKMARK_FILE_ERROR_WRITE,
  G_BOOKMARK_FILE_ERROR_FILE_NOT_FOUND
} GBookmarkFileError;

So I create in a directory named enum_ctypes the following files :

enum_ctypes/
|__   _oasis
|__   lib/
        |__ MyEnum.ml

The content of the _oasis file :

OASISFormat: 0.4
Name:        enum_ctypes
Version:     0.0.0
Synopsis:    test enum ctypes
Authors:     cedlemo
License:     GPL-3
Plugins:     META (0.4), StdFiles (0.4), DevFiles (0.4)
BuildTools: ocamlbuild

Library "MyEnum"
  Path: lib
  Modules: MyEnum

  BuildDepends: ctypes, ctypes.foreign
  CCLib: -Wl,-no-as-needed -lgirepository-1.0 -lgobject-2.0 -lglib-2.0
  CCOpt: -O2 -Wall -Wextra -Wno-unused-parameter -pthread
         -I/usr/include/gobject-introspection-1.0
         -I/usr/lib/libffi-3.2.1/include
         -I/usr/include/glib-2.0
         -I/usr/lib/glib-2.0/include

And the content of the MyEnum.ml file :

open Ctypes
open Foreign

let _invalid_uri = constant "G_BOOKMARKFILEERROR_INVALID_URI" uint32_t
and _invalid_value = constant "G_BOOKMARKFILEERROR_INVALID_VALUE" uint32_t
and _app_not_registered = constant "G_BOOKMARKFILEERROR_APP_NOT_REGISTERED" uint32_t
and _uri_not_found = constant "G_BOOKMARKFILEERROR_URI_NOT_FOUND" uint32_t
and _read = constant "G_BOOKMARKFILEERROR_READ" uint32_t
and _unknown_encoding = constant "G_BOOKMARKFILEERROR_UNKNOWN_ENCODING" uint32_t
and _write = constant "G_BOOKMARKFILEERROR_WRITE" uint32_t
and _file_not_found = constant "G_BOOKMARKFILEERROR_FILE_NOT_FOUND" uint32_t
let bookmarkfileerror : [`Invalid_uri|`Invalid_value|`App_not_registered|`Uri_not_found|`Read|`Unknown_encoding|`Write|`File_not_found] typ = enum "bookmarkfileerror" [
`Invalid_uri, _invalid_uri;
`Invalid_value, _invalid_value;
`App_not_registered, _app_not_registered;
`Uri_not_found, _uri_not_found;
`Read, _read;
`Unknown_encoding, _unknown_encoding;
`Write, _write;
`File_not_found, _file_not_found
] ~unexpected:(fun i -> `Unexpected i)

When I try to build this with the following commands in the enum_ctypes directory :

oasis setup -setup-update dynamic
./configure
make

I have the following error message :

File "lib/MyEnum.ml", line 4, characters 19-27:
Error: Unbound value constant

How can I solve this problem ?

2 Likes

What guide or documentation did you follow for this?

That is a real problem, the documentation is scarse, I was only able to find this : https://github.com/ocamllabs/ocaml-ctypes/pull/245 .

The function constant and enum are part of the cstub interface of Ctypes and they are not available in the dynamic mode of Ctypes.

To bind enums in the dynamic mode, the easiest (if unsafe) way is to write an integer view:

type abc = A | B | C
let of_int = function 0 -> A | 1 -> B | 2 -> C | _ -> raise (Invalid_argument "Unexpected C enum")
let to_int = function A -> 0 | B -> 1 | C -> 2
let abc = Ctypes.view ~read:of_int ~write:to_int Ctypes.int

Then the view abc can be used like any other Ctype's view. For instance, binding a function f:abc -> unit can be done with

let f = Foreign.foreign "f" Ctypes.( abc @-> returning void)

If you want to use Ctypes cstubs API, the process is a little more involved.
First, you need to write an ocaml-side type binding generator:

type abc = A | B | C

module Types(T:Cstubs.Types.TYPE) = struct
        let a = T.constant "A" T.int64_t
        let b = T.constant "B" T.int64_t
        let c = T.constant "C" T.int64_t

        let abc = T.enum "letter" [A, a; B, b; C, c]
            ~unexpected:(fun x -> assert false)
end

let () = (* generate the c-side type bindings : *)
  let f = Format.formatter_of_out_channel @@ open_out "types_gen.c" in
  Format.fprintf f {|#include "enum.h"@.|};
  Cstubs.Types.write_c f (module Types)

Executing this ocaml-side generator will generate a C-side type binding generaror (here types_gen.c). This C-side generator can then be executed to generate ocaml bindings to the C type definitions and constants.
At this point, it is possible to use this type bindings to write ctype cstub generator. For instance,
if the generated type bindings was mapped to types_with_abc:

module Tb = Type_bindings
module T = Tb.Types(Types_with_abc)

module P(F:Cstubs.FOREIGN) = struct
  open F
  let f = foreign "f" T.(abc @-> returning Ctypes.void)
end

let () = (* the generation itself is done here: *)
  let cstub = Format.formatter_of_out_channel @@  open_out "cstub_abc.c" in
  let bindings = Format.formatter_of_out_channel @@ open_out "abc_bindings.ml" in
  Cstubs.write_c cstub "abc" (module P);
  Cstubs.write_ml bindings "abc" (module P)

Once executed, this generator will create both cstubs and a new foreign module that can be used to instance the bindings P on the OCaml side:

module Abc = Binding.P(Abc_bindings)
let () =
  Abc.f A
5 Likes

Thanks, your answer is really helpfull. Why do you consider the first solution “unsafe” ?

Since it does not use enum.h, there is no guarantee that of_int and to_int correspond to the C definitions. This is particularly an issue if the C library adds or removes cases, or even renumbers them.

It seems that there are no garantees that this code still works neither

Unless you recompile it every time you launch the program that uses it.

The definitions usually need to be compatible with functions defined in a corresponding C library. If your build context is correct and the shared library is versioned, then you should get a linker error if you runtime-link against an incompatible version. Also must Linux distributions I’ve used are conservative about upgrading libraries to incompatible versions.