Toplevel and Dynlink

This page explains how to build an OCaml toplevel (REPL) that runs in the browser, and how to use dynamic linking to load bytecode at runtime.

Both js_of_ocaml and wasm_of_ocaml support these features.

Building a toplevel

A toplevel is an interactive OCaml environment (like ocaml or utop). Js_of_ocaml and wasm_of_ocaml can compile a toplevel to JavaScript or WebAssembly, allowing it to run in the browser. See the live demo.

With js_of_ocaml

  1. Initialize the toplevel in your OCaml code using Js_of_ocaml_toplevel.Direct.initialize
  1. Build your bytecode with debug info and linkall:

    ocamlfind ocamlc -g -linkall -package js_of_ocaml-toplevel \
        -linkpkg toplevel.ml -o toplevel.byte
  1. Compile to JavaScript with the --toplevel flag:

    js_of_ocaml --toplevel toplevel.byte -o toplevel.js

With wasm_of_ocaml

  1. Initialize the toplevel in your OCaml code using Js_of_ocaml_toplevel.Direct.initialize
  1. Build your bytecode with debug info and linkall, linking wasm_of_ocaml-compiler.dynlink and js_of_ocaml-toplevel.common:

    ocamlfind ocamlc -g -linkall -package wasm_of_ocaml-compiler.dynlink \
        -package js_of_ocaml-toplevel.common -package compiler-libs.toplevel \
        -linkpkg toplevel.ml -o toplevel.byte
  1. Compile to WebAssembly with the --toplevel flag:

    wasm_of_ocaml --toplevel toplevel.byte -o toplevel.js

Running the toplevel in a Web Worker

The toplevel can run in a Web Worker so that parsing, compiling and evaluating code does not block the page: the page ships source to the worker and renders the results asynchronously.

This splits into two programs:

The worker's entry point hands control to the generic driver:

(* worker.ml *)
let () = Js_of_ocaml_toplevel_worker.start ()

Build it like a toplevel. Passing --no-cmis keeps the .cmi files out of worker.js (smaller output); the cmis are then provided as a bundle that the worker fetches at startup:

ocamlfind ocamlc -g -linkall \
    -package js_of_ocaml-toplevel -package js_of_ocaml-toplevel.worker \
    -linkpkg worker.ml -o worker.byte
js_of_ocaml --toplevel --no-cmis worker.byte -o worker.js

Produce that bundle with jsoo_mkcmis; it must be named stdlib.cmis.js. Only unwrapped libraries are resolvable from the toplevel this way (for example stdlib and lwt, not the wrapped js_of_ocaml):

jsoo_mkcmis -o stdlib.cmis.js stdlib

Both js_of_ocaml and jsoo_mkcmis place cmis under /static/cmis, so the worker only fetches the bundle when the cmis are not already there: drop --no-cmis to embed them in worker.js instead, and no bundle is needed.

The host page drives the worker through Js_of_ocaml_toplevel_worker_lwt_client (create, execute, reset, and interrupt to stop a hung computation). It depends only on js_of_ocaml-toplevel.worker_lwt_client and the dependency-free js_of_ocaml-toplevel.protocol (for the result type), so it links no toplevel runtime:

ocamlfind ocamlc -g \
    -package js_of_ocaml-lwt \
    -package js_of_ocaml-toplevel.worker_lwt_client \
    -package js_of_ocaml-toplevel.protocol \
    -linkpkg main.ml -o main.byte
js_of_ocaml main.byte -o main.js

The toplevel/examples/lwt_toplevel_worker sources are a complete example, with a banner, input history, an examples sidebar, and a Cancel button that interrupts a stuck computation.

Limiting available modules

By default, all linked modules are available in the toplevel. To limit this, use --export FILE where FILE lists compilation unit names (one per line).

The jsoo_listunits tool generates this list from findlib libraries:

jsoo_listunits -o units.txt stdlib str
js_of_ocaml --toplevel --export units.txt toplevel.byte

The --export flag works the same way with wasm_of_ocaml.

OCaml's Dynlink module lets you load bytecode files at runtime. This works in both js_of_ocaml and wasm_of_ocaml with some setup.

With js_of_ocaml

  1. Link js_of_ocaml-compiler.dynlink in your program (initializes dynlink support)
  1. Build your bytecode with debug info and linkall:

    ocamlfind ocamlc -g -linkall -package dynlink \
        -package js_of_ocaml-compiler.dynlink -linkpkg main.ml -o main.byte
  1. Compile to JavaScript with the --dynlink flag:

    js_of_ocaml --dynlink main.byte -o main.js

With wasm_of_ocaml

  1. Link wasm_of_ocaml-compiler.dynlink in your program (initializes dynlink support)
  1. Build your bytecode with debug info and linkall:

    ocamlfind ocamlc -g -linkall -package dynlink \
        -package wasm_of_ocaml-compiler.dynlink -linkpkg main.ml -o main.byte
  1. Compile to WebAssembly with the --dynlink flag:

    wasm_of_ocaml --dynlink main.byte -o main.js

Plugin .cmo files are compiled on the fly to WebAssembly and instantiated at runtime.

Example

(* main.ml *)
let () = Dynlink.loadfile "./plugin.cmo"

With js_of_ocaml:

# Compile main program
ocamlfind ocamlc -g -linkall -package dynlink \
    -package js_of_ocaml-compiler.dynlink -linkpkg main.ml -o main.byte
js_of_ocaml --dynlink main.byte -o main.js

# Compile plugin
ocamlfind ocamlc -c plugin.ml

# Run
node ./main.js

With wasm_of_ocaml:

# Compile main program
ocamlfind ocamlc -g -linkall -package dynlink \
    -package wasm_of_ocaml-compiler.dynlink -linkpkg main.ml -o main.byte
wasm_of_ocaml --dynlink main.byte -o main.js

# Compile plugin
ocamlfind ocamlc -c plugin.ml

# Run
node ./main.js

Loading precompiled files

By default, Dynlink.loadfile compiles .cmo and .cma files on the fly. You can also precompile plugins ahead of time for faster loading.

With js_of_ocaml

Use js_of_ocaml to compile a .cmo or .cma file to JavaScript:

js_of_ocaml plugin.cmo -o plugin.js
js_of_ocaml plugin2.cma -o plugin2.js

Then load the resulting JavaScript file at runtime. In Node.js, use require:

let require s =
  Js_of_ocaml.Js.Unsafe.fun_call
    (Js_of_ocaml.Js.Unsafe.js_expr "require")
    [| Js_of_ocaml.Js.Unsafe.inject (Js_of_ocaml.Js.string s) |]

let () = require "./plugin.js"

In a browser, use a <script> tag:

<script src="plugin.js"></script>

With wasm_of_ocaml

Use wasm_of_ocaml compile to compile a .cmo to a .wasmo file, or a .cma to a .wasma file:

wasm_of_ocaml compile plugin.cmo -o plugin.wasmo
wasm_of_ocaml compile plugin2.cma -o plugin2.wasma

Then load the precompiled file using Wasm_of_ocaml_compiler_dynlink.loadfile:

let () = Wasm_of_ocaml_compiler_dynlink.loadfile "./plugin.wasmo"

This skips the on-the-fly bytecode-to-Wasm compilation step, which can significantly reduce loading time for large plugins.

See also