OCaml for building shared libraries: how are the ergonomics and performance?

Greetings. I appreciate that the topic sounds too subjective or worse: an attempt at trolling. That is certainly not my attention.

I have been following OCaml with great interest, due to my love of its distant cousin who works at a corporate shop: F# :slight_smile:

I have a quite specific and probably niche requirement at hand, which Rust serves rather well: building shared libraries (and static ones) to be called as native libraries from various languages, Python, C#, you name it. I’ve been considering experimenting with OCaml, but I though I may attempt to rely on your generosity to save some precious research/poc time.

Can members of the community who use OCaml in this fashion comment on their experience? I don’t think it is a common scenario to build shared libraries with OCaml, or that’s the impression I got from my initial google searches. Am I right? I’m curious about best practices to deal with the requirement to initialise OCaml runtime when calling it from C, which is something of an inconvenience in my use cases, when I’m building a library in language X which in turn uses a native library, I’d like to contain this requirement so that my own library functions don’t have to pass this requirement on, or have to handle it themselves for every api call they expose (i.e., let’s check if we initialised the OCaml runtime…)

Other things that would take me a long time to find out are: what is the language barrier overhead like? Is it expensive to call OCaml code from other languages compared to overhead of calling a library written in C? (unfair, I know, assume I said Rust)

Finally, do you find it convenient ? I know I’m asking for a subjective view of a niche use-case, but if there’s some support for it (library? ppx?) that I’m not aware of, I’d love hear that. Or if you think it is painful, I’d love to hear that too.

2 Likes

At LexiFi we do exactly this to build our main native Windows application. All the applicative code is written in OCaml, compiled to a shared library. The executable itself is a small C# wrapper that does some initialization and then jumps into the OCaml code (which itself calls back to .NET to handle the native UI). [We use GitHub - LexiFi/csml: High-level bindings between .Net and OCaml to handle the OCaml<->.NET calls, but this is unrelated to your question.]

It depends on your requirements, but the performance of the OCaml/.NET bridge was never an issue for us (granted, most of our application is in OCaml anyway, so essentially we only cross the bridge when interacting with the UI layer which is typically not performance-sensitive).

Very. It has worked very well for us.

Cheers,
Nicolas

10 Likes

Completely agree with @nojb’s responses.

In my world we have an app that has both a Qt/C (Windows and Linux) desktop UI and a conventional Android/Java mobile UI. Essentially the same OCaml shared library is used (modulo some stripping and FFI wrapping) for both UIs. Both UIs have the conventional mainloop threads (Qt or Android) that can’t be blocked, and a thread dedicated to OCaml. Messages are posted between the UI threads and the OCaml thread; the messaging uses ordinary event plumbing that is available in most modern UI frameworks.

5 Likes

I have a relevant PoC in progress: GitHub - jyssh/poc-ocaml-logic-native-ui

I will only try to cover the ergonomics because my PoC is too trivial to worry about performance.

The PoC compiles a Swift executable which also bundles in an OCaml library.

Right now, all it does is generate a Swift CLI app which passes a protobuf message to OCaml, which in turn returns a protobuf response, which is printed to the terminal.

My vision is that Swift and OCaml talk to each other asynchronously through message passing via a protobuf bridge. This approach should keep the C interop code to a minimum of a couple of functions - one that sends messages from Swift to OCaml, another that handles the other direction.

So far, I have only managed to implement a synchronous mode of communication, i.e., a function that Swift calls and gets a return value from the same call. This might prove expensive if OCaml were to do some complex operation. I believe an asynchronous approach is the better way, but I am yet to get there.

All of the above assumes that the Swift side is the main driver of the whole setup, the OCaml library being the passive side. This seems contrary to @nojb’s solution.

Another reason to adopt the message passing apporach is so that the Swift side has the option to carry out all the I/O operations - UI, network calls, may be file or DB ops too. This flexibility could mean that on a resource-sensitive platform like iOS or Android, native I/O operations could be used, which are often optimised to a maximum w.r.t resource usage. Though there is also the question that what the hell is left for the OCaml library to do in this scenario. I will think about it when I get there.

Over all, the viability of this approach depends on how thick the OCaml core can be. Thinner it is, less it justifies all the gymnastics.

2 Likes

Thanks for all the helpful responses. I think there is some first hand feedback that justifies positive expectations about the ergonomics. I need to look into how to solve the initialisation requirement though. I wonder if there’s a way to use a C wrapper to use some OS specific “on load” handler/hook for libraries? I’d be fine with a *nix only solution too. If I could tell the OS to run this piece of code the first time the library is loaded into a process, to start the OCaml runtime, then languages calling into the exposed OCaml API would not have to worry about it at all. I should do some research on that.

Once again, thanks!