Writing ReasonML bindings for JavaScript APIs

If you haven't heard about Reason, you should probably look into what it is first. However, if you have a JavaScript background and have just recently heard about Reason, the rest of the article should make sense for you.

If you're looking to write ReasonML bindings for an existing JavaScript library, you'll find some documentation on how the process works (both in the Reason and Bucklescript docs). There's also plenty of bindings that you can use for some popular libraries (moment.js, React, etc.).

For this blog post, I wanted to write a set of bindings for a JavaScript library for the sole purpose of documenting the process so other people can learn more about it.

I picked Web Workers as they have a pretty simple API and so it shouldn't take us very long to be able to use it with ReasonML. I also couldn't find any existing bindings in either Reason or Bucklescript, so I thought it'd make sense to take the opportunity to contribute something new.

For the purpose of this blog post, we'll only be covering the basic Web Workers API. This includes the Worker constructor, the postMessage method and the onMessage event handlers.

You can find the finished repository with the bindings and an example here.


Before you read any further

These bindings were a Reason learning experiment and so a couple of things in this article might not be written in "the Reason way".


Our final goal (except in Reason)

index.js

var myWorker = new Worker("worker.js");

myWorker.onmessage = function(e) {
  console.log("Message received from worker");
}

myWorker.postMessage({"text": "Hello world"});

worker.js

onMessage = function(e) {
  console.log(e.data);
  postMessage("Message received from main thread");
}

In order to write the above example in Reason, we'll have to create a set of Reason bindings for the JavaScript API. The first thing that you should absolutely read before writing any such bindings is the Bucklescript documentation on Interop.

On the Main Thread

Let's start with the constructor — new Worker(string). This is a method that takes in a string and returns a Worker. So, we should start by creating a type for Web Workers and declaring the binding for that constructor using Bucklescript's interop syntax.

type webWorker;

[@bs.new] external create_webworker : string => webWorker = "Worker";

There's already a few key things to note:

  1. The Web Worker type has no definition yet (it just exists). This is because we should write the bindings such that they work first, and worry about the details later.
  2. We called our constructor create_webworker as this name adheres to the Reason naming standard.
  3. We bind create_webworker to new Worker() using @bs.new.

So, how would we use this file from our main file? (we'll call it demo.re)

let myWorker = WebWorkers.create_webworker("worker.js");

Now, we need to create an external binding for the postMessage method. This is a method on the Worker type that takes in "something" and returns nothing.

[@bs.send] external postMessage : (webWorker, 'a) => unit = "postMessage";
  1. It's a @bs.send which means that Reason will apply the method on the first argument, of type webWorker.
  2. We use 'a as a "for all types".
  3. It returns nothing, so unit.

We can use this method like this (in demo.re):

let msg = {"text": "Hello world"};

WebWorkers.postMessage(myWorker, msg);

Now, we need to define the onmessage handler for when the worker uses its postMessage function. This time, we need to use @bs.set to define a method on the webWorker type. This method takes in a function that receives a MessageEvent and returns nothing. The method itself returns nothing as well.

module MessageEvent = {
  type t;
  [@bs.get] external data : t => 'a = "";
};


[@bs.set] external setOnMessage : (webWorker, MessageEvent.t => unit) => unit = "onmessage";
  1. The MessageEvent is a submodule with a type that represents the actual MessageEvent type.
  2. This submodule then needs a .data() method (that maps to the read-only property .data) which returns the actual data stored (which can be of any type, in true JavaScript spirit).
  3. We use empty "" because the JavaScript property name is data so Reason can infer that mapping for us.
  4. Finally, we create a setOnMessage function that sets (using @bs.set for mapping) the onMessage property of a webWorker. The function that is passed in has to be of type MessageEvent.t => unit.

Let's take a look at our final demo.re file which uses the API we have built so far.

let worker = WebWorkers.create_webworker("worker.js");

let msg = {"text": "Hello world"};

WebWorkers.postMessage(worker, msg);

let msgBackHandler = (e: WebWorkers.MessageEvent.t) => {
  Js.log("I am the main thread and I have received a message back from the worker:");
  Js.log(WebWorkers.MessageEvent.data(e))
};

WebWorkers.setOnMessage(worker, msgBackHandler);

The Worker Script

The worker script was fairly more complicated. Here's a refresher of what our goal for this file is:

worker.js

onMessage = function(e) {
  console.log(e.data);
  postMessage("Message received from main thread");
}

Here's what I came up with for setting the onMessage property of self.

type window;
[@bs.val] external self : window = "";
[@bs.set] external setWorkerOnMessage : (window, MessageEvent.t => unit) => unit = "onmessage";

[@bs.val] external postMessageFromWorker : ('a) => unit = "postMessage";
  1. We declare the bland type window (which could be also be JS.Dom.window).
  2. We map WebWorkers.self to window.self using @bs.val.
  3. We declare WebWorkers.onmessage which takes in a window and an onmessage handler and maps to onmessage.
  4. We created WebWorkers.postMessageFromWorker which takes in anything and returns nothing.

The Final Result

Let's take a look at the final Reason result.

demo.re

let worker = WebWorkers.create_webworker("worker.js");

let msg = {"text": "Hello world"};

WebWorkers.postMessage(worker, msg);

let msgBackHandler = (e: WebWorkers.MessageEvent.t) => {
  Js.log("I am the main thread and I have received a message back from the worker:");
  Js.log(WebWorkers.MessageEvent.data(e))
};

WebWorkers.onMessage(worker, msgBackHandler);

worker.re

WebWorkers.onmessage(
    WebWorkers.window,
    (e: WebWorkers.MessageEvent.t) => {
        Js.log("I am the Web Worker and I have received a message:");
        Js.log(WebWorkers.MessageEvent.data(e));
        WebWorkers.postMessageFromWorker("my result");
    }
);

Note how on the worker's side, we can also make use of the WebWorkers.MessageEvent.data getter util to parse the MessageEvent passed to the onmessage handler.

Next steps

This API could be polished to be more strongly typed and to cover more of the Web Workers API.

Additionally, I will try to submit my bindings to the bs-webapi-incubator project, which holds Reason bindings for diverse Web APIs. For now, the work I did for this blog post lives here.

If you want to look at the JavaScript output (it's very interesting, I promise), feel free to clone it and run it.

Conclusion

The Bucklescript/Reason interop (FFI) system is very powerful and lets us add strong typing to previously existing JavaScript APIs. This is just a breath of fresh air after working with a weakly typed language (JavaScript) for so long.

However, the FFI also lets us "add types as we go". You can start by mapping just the bare basics of your API to Reason without strong typing and then, as you go along, you can incrementally polish your FFI.