Orbifx's logarion

Storing an OCaml callback function in a C++ object

OCaml global roots can not be stored in OCaml's heap

| | | topics: computing | keywords: c++, callbacks, ocaml
id: b2d13b33-39f5-4e15-8d18-e0100391ceba

OCaml's heap is controlled automatically, by the so-called Garbage Collector (GC). C++ objects can be stored in OCaml's heap as custom or abstract blocks.

Lablqml binds QML signals to OCaml "callback" functions. When a bound property's value changes, the OCaml function is called. The address of the function needs to be known both to Qt (QML) and OCaml. So the address of the function is stored in Qt deriving C++ object.

To preserve the OCaml function from being collected, and for the address to remain valid if it is ever moved, it is registered as a global root, which means the garbage collector will treat is as a value needed forever or until removed from global roots.

These functions can't be stored directly in custom or abstract blocks, because the caml_register_global_root registers an address which expected to not change. If it was an address within a heap block and when the block eventually moved, the global registry would not be revised, leading to corruption if it tried to update the value.

The function value needs to be stored in malloced location and then a pointer to this can safely be stored in OCaml's heap blocks. OCaml's registry updates the malloc-ed address and Qt objects that need to perform callbacks, hold an address to that malloc-ed address too.

In code:

value caml_create_callback(value func) {
    CAMLparam(func);

    value *malloced_func = (value*) malloc(sizeof(func));
    *function_v = func;
    caml_register_global_root(malloced_func);

    ...

    auto cpp_object = new CppObject;
    cpp_object->callback = malloced_func;

    ... = caml_alloc_custom(&cpp_object_ops, sizeof(CppObject*), 0, 1);

Now if the garbage collector moves the block, its pointer will still be valid and OCaml knows where the function value pointer are to update them.

Notes

It's important to unregister the global root when it's no longer necessary, so the object handling the callback should call caml_unregister_global_root when it's destroyed. Also, if the callback function is rarely or never updated, it is worth using caml_register_generational_global_root.

Credits: