Hello everyone!
It's quite popular nowadays to extend OCaml in Rust instead of C. It works really well up to the point when you have several binding libraries (i.e. mixed OCaml/Rust libraries) that form a complex dependency graph and you want to somehow distribute them.
An example that I have at hand is my ocaml-lwt-interop library that bridges Rust async code to OCaml async code (using Lwt library). I'm building bindings to hyper on top of that ocaml-lwt-interop
lib. Dependency graph looks something like this (Rust libs in red, OCaml libs in yellow):
Dune is a modern OCaml build system. OCaml has well-defined C ABI, so Rust extension is just a shared library exporting a bunch of "extern" C
symbols that OCaml can call into. Those symbols are also defined in OCaml sources, and there's no checking that number or types of arguments actually match with the actual shared library, so such extension library is best compiled along with OCaml sources from the same source tree.
A bunch of "extern" C
symbols is quite low level and to provide more ideomatic OCaml API, one needs to wrap those with higher level API for more convenience and type-safety. In case of ocaml-lwt-interop
- there is a Rust crate ocaml-lwt-interop
with bits that are required by other binding libraries (i.e. ocaml-hyper
), and also there is Dune library rust-async
that wraps ocaml-lwt-interop
with additional ocaml-lwt-interop-stubs
library that defines those C symbols. Hypothetical rust-hyper
Dune library will depend on rust-async
Dune library, and also it will depend on ocaml-hyper-stubs
lib which defines C API to work with ocaml-hyper
crate.
I've labelled *-stubs
crates as "private" to Dune libraries because they are tightly coupled and can't be meaningfully distributed separately. ocaml-lwt-interop
and ocaml-hyper
could probably be distributed via crates.io
without much issues as long as "private" crates define proper version constraints.
OCaml projects typically are built within so called "switches", which are managed by OCaml package manager - the opam. A switch is a directory, which contains sources and compiled artifacts for all the installed OCaml packages, usable by the project. When Rust sources are embedded in an OCaml package, they just end lying inside some opam switch subdirectory where package sources reside, and compiled artifacts are installed into another opam switch subdirectory.
Important property about opam switch is that it contains proper versions of both OCaml and Rust dependencies of our project, i.e. all Rust extension library sources inside the opam switch do match corresponding (compiled) OCaml libraries in that switch.
Opam itself can be queried about the full list of dependencies of current project, some metadata can be added to opam package definitions telling us that this package has some private Rust crates inside, so we can compose a list of private crates that we need to pull as Rust deps of current project.
I was thinking about some tool that could build some local repository for Cargo out of a list of those private Rust crates inside opam switch, and that local repository creation could be somehow integrated as Dune build rule so that it creates/updates that repository before invoking cargo to actually build the Rust bits. Looks like debian is doing something similar, but they are probably working only with crates that are published on crates.io
, and I want to build some local repository with unpublished crates found inside the opam switch directory, and somehow tell cargo to use it from filesystem so that my Rust dependencies are properly resolved. Would be great to get a piece of advise here from Rust/Cargo experts!
I'm linking some relevant discussions in the OCaml community for reference.