Native and JS stubs with js_of_ocaml

Some of the functions and modules I’m using in my native executable, are not able to compile to JS using js_of_ocaml. For example, Nocrypto cannot compile to JS, and some of the Core.Time functions are not in Core_kernel.Time. However, there are straightforward JS equivalents I could use.

How can I write these stubs so that the same library can compile both JS and native? For example, I have a library that could call either Nocrypto.Hash.SHA284.digest or a JS file called crypto.js with sha384_digest, depending on the compilation target. How would I set jbuilder up to compile that?

Related question: if I want a type that is one thing when doing our JS compilation (perhaps a JS object) and another thing (probably a Core.Time.t) for native compilation, how do I do that. I guess I want to create a MyTime.t that abstracts over those two types, but I don’t know where to get started with this.

Thanks!

Learnings since posting:

I think found how to create stubs in js_of_ocaml: https://github.com/janestreet/core_kernel/search?q=core_kernel_time_ns_format&unscoped_q=core_kernel_time_ns_format

I do not know if what you want to do can be achieved with js_of_ocaml but you might be interested in checking Bucklescript ( https://bucklescript.github.io/ ) which enables linking to JS functions as exernals. I do not know how it works with jbuilder though, it seems to have its own build system.

As for your second question, no idea, but there is, linked to Bucklescript again, bsb-native that enables compiling to ocaml instead of JS ( https://github.com/bsansouci/bsb-native ) I’d expect that you can give alternatives depending on the backend.

There is work-in progress to support that out of the box in jbuilder/dune, it’s called Library Variants. The idea is to define a commun signature for various implementations, depending on the backends you want to use. Tip: we are very interested to have this for MirageOS :slight_smile:

That feature is not planned for dune 1.0, but you can already achieve something similar by tweaking a bit your build system: see https://github.com/diml/variants-workaround for a template. The trick is to compile the common mli with -no-keep-locs on 4.06.1 and to choose the right implementation at link time.

5 Likes

There’s also gen_js_api for generating ocaml bindings for javascript

1 Like

Thanks, this is super helpful! I haven’t quite got the JS side working, but the ocaml side and structure works well.

For anyone else whose doing a similar thing, here’s how it played out:

We had a simple layout:

  • lib: almost all the code
  • main: a few binaries, mostly 6-7 lines, using lib
  • test: tested lib code

We now have:

  • libtarget: target-specific functions. Has no implementation
  • libtarget.ocaml: implementation for libtarget for the backend
  • libtarget.js: implementation for libtarget for the frontend
  • libbackend: all server code, depends on libtarget and libtarget.ocaml
  • libexecution: the code we wanted to share between client and server. Depends on libtarget
  • libfrontend: all client code. Depends on libtarget and libtarget.js
  • main: makes binaries from libbackend
  • js: makes js from libfrontend
  • test: tests js code

Quick question: why is --no-keep-locs important?

4 Likes

That’s great! We are in the process of chaning some of the mirage libraries to use that scheme too, to have both a portable OCaml and an efficient C implementation for checksums and hashes. We should add a JS backend too at one point too.

Using --keep-locs allows to get better error messages. It adds locations to the cmi files, which make them non relocatable. See https://github.com/ocaml/ocaml/pull/1219 for the rational of turning that on per default and the related discussion.

1 Like

Note: I think it may be possible to get good error messages without using --keep-locs to compile .cmi files, but that would require work. The idea is to define a notion of “paths to a definition within an interface” (today we identify those definitions just by their source location, which is less abstract), and use these paths to query locations in a separate .cmti file. I am not fully sure that it would work, but it can be tried; we haven’t because it was argued that --keep-locs is a low-effort way to get the nice errors without much downsides. If people re-evaluatate the downsides, it could motivate pouring work into alternatives.