I will try to describe the differences by reading from Goswin’s code, however I might understand some things incorrectly.
First, here is how Cuite works:
- a small runtime library binds each Qt concept once and for all
- the bulk of the library is automatically generated from a synthetic description (see QtWidgets for instance), building on the primitive concepts from the runtime library
The goal is to have as few work as possible on OCaml side (a “low-level” binding). If you want to build nicer abstractions, you can, but this foundation will be needed anyway. Qt code is exposed almost directly.
The primitive stuff that is taken care of includes:
- exposing Qt basic types, variants, enumerations and flags
- exposing Qt object model and inheritance, with signals and slots
- fine-grained GC interaction, such that manipulating Qt objects is as natural and safe as possible, allowing to mix automatic and explicit memory management for deterministic resource release.
The library avoids inheritance (if you have to inherit a class to expose it to OCaml, then you won’t be able to expose objects that have been instantiated under the hood by Qt). Users of the library should rely only on composition.
One exception is for data models (abstract classes on C++ side). They are implemented by inheriting once, proxying each abstract method to an OCaml function (see Cuite__model).
For the user of the library
No C++ code has to be written at all. I also tried to make the design dead-code elimination friendly to produce small binaries (linking to a shared
libcuite.so) when few features are used (still work-in-progress).
The module system is used to structure the code. Each Qt class maps to one module. All code on OCaml side is parametric: ad-hoc behaviors (same function having different effects on different objects) occur only on C++ side. Single inheritance subtyping is encoded with polymorphic variants used in a phantom type.
It gives a procedural feeling that should lead to predictable code for OCaml users (see this PoC example translated from Qt).
OCaml-like type safety is present: if you have an instance of an object, you should be able to use it safely. Places where nulls are accepted are represented as an option type. There are two exceptions to the rules (which should be clearly identifiable from the context):
- for deterministic memory management, explicit deletion is allowed. In this case, you might get “use after free” OCaml exceptions if you reuse a value, but safety is preserved.
- some low-level resources cannot benefit of managed memory (for instance iterator invalidation cannot tracked in general).
In that case, you are on your own and Cuite exposes the low-level primitives only. The design is such that it should be possible to isolate the unsafe part in a safe to use OCaml module, after identifying common use cases (which requires manual curation and can be done after, at a higher-level than automatic generation).
My small experience (though I haven’t done any serious project) is that code ends up being shorter and more structured than the equivalent in C++. However, it is more explicit about recursion which sometimes require rethinking the design (“tying the knot” is not implicit as it is in C++).
Other information are available in the following presentation that was given at an OCaml meetup.
Side question: why not automatically parse headers?
Headers contain a lot of noise that is not relevant to OCaml code.
They also don’t say anything about certain semantic aspects that are relevant to OCaml side (will this parameter live longer than the lexical scope of the method? is this pointer used as input or output? are these references “real” or just a disguised returned tuple?).
For these two reasons, I preferred to resort to a synthetic description, described as an OCaml value (OCaml is actually good for manipulating symbolic representations :)), that exposes what is really necessary for generating the bindings. The original description was automatically extracted from Qt documentation sources and manually curated.
In terms of the amount of work involved:
- mapping concepts is
O(number of concepts) with a big constant factor as we have to write C++ and OCaml code, and deal with tricky control and data flow interactions, this is done
- mapping the object hierarchy is still
O(number of classes and methods) but with a very small constant factor, as it is just one entry in the description (as in QtWidget)
- mapping models is still quite involved (manual C++ and OCaml code for each model), however there are very few models and code reuse is possible, following existing examples.
I think this is the best trade-off given the requirements I have chosen for the binding. Further cost optimisations are possible by automating description update (this time parsing the headers, or using whatever metadata can be extracted from Qt), but code review will still be necessary and I would not trust 100% automated extraction.
It seems to me that ocaml-qt5 tries to expose Qt at a higher-level. The library doesn’t show Qt concepts directly but extend classes with OCaml specific counterparts (OWidget for instance), which leads me to worry that it will not scale (as a lot of manual work is involved), and that the resulting api might differ significantly from the original Qt one.
The implementation also deals directly with OCaml objects (using an abstract
type only to encode a nominal hierarchy on top of OCaml structural ones) which, in my opinion, results in unnecessary complication (the C code has to know about OCaml object system, while a proper separation in theory allows to do all OO stuff purely on ML side, but I might be wrong).
On the plus side, OCaml-qt5 allows method redefinitions on OCaml side (hence the inheritance-based approach).
However Qt is designed to achieve as many stuff as possible through object composition so it is rarely a problem, and nothing prevents to do the same on Cuite side (as is done with models) when it proves necessary. It seems to be the exception rather than the rule.
In the absolute, these are not wrong design decisions however they seem less practical for the goal I am trying to achieve with Cuite.
Comparing Cuite, OCaml-qt5 and Lablgtk designs:
- Cuite implements only a procedural API
- OCaml-qt5 implements an OO-API directly in the bindings
- Lablgtk implements a procedural API in the bindings, then exposes both the
procedural API and an OO wrapper in ML
I feel that it is unecessary to expose an OO-API, I don’t appreciate this style and it leads to confusion.
GObject or C++ object systems are too different from OCaml one (a class in OCaml has nothing to do with a class in C++), there is nothing to gain semantically in trying to encode one in another, so it is purely a matter of syntax and I am happy writing
But nothing prevents implementing it or even deriving it automatically from the description.