Skip to main content

Foreign Function Interface(FFI)

You can use foreign function in MoonBit through FFI to interact with the hosting runtime when embedded inside the browser or command line applications through Wasmtime or similar projects.

⚠ Warning: MoonBit is still in early stage, so the content may be outdated.

FFI

Declare Foreign Reference

You can declare a foreign reference type like this:

type Canvas_ctx

This will be a type that represents a reference to a foreign object, a CanvasRenderingContext2D object held by the hosting JavaScript runtime in this example.

Declare Foreign Function

You can declare a foreign function like this:

fn cos(d : Double) -> Double = "Math" "cos"

It's similar to a normal function definition except that the function body is replaced with two strings.

For WasmGC backend, these two strings are used to identify the specific function from a Wasm import object, the first string is the module name, and the second string is the function name. For JS backend, these two strings are used to call a static function in the global namespace. The example above becomes similar to const cos = (d) => Math.cos(d).

You can also declare inline functions where the function body is replaced with one string.

For WasmGC backend, you may declare it as a Wasm function without name (which will be generated afterwards):

extern "wasm" fn abs(d : Double) -> Double =
  #|(func (param f64) (result f64))

and for JS backend, you may declare it as a lambda expression:

extern "js" fn abs(d : Double) -> Double =
  #|(d) => Math.abs(d)

After declaration, you can use foreign functions like regular functions.

For multi-backend project, you may implement backend specific code in the files that ends with .wasm.mbt .wasm-gc.mbt and .js.mbt.

You may also declare a foreign function that will be invoked upon a foreign object by using the foreign reference type like this:

fn begin_path(self: Canvas_ctx) = "canvas" "begin_path"

and apply it to a previously owned reference normally such as context.begin_path().

Exported functions

Functions that are not methods nor polymorphic functions can be exported if they are public and if the link configuration appears in the moon.pkg.json of the package:

{
  "link": {
    "wasm": {
      "exports": [
        "add",
        "fib:test"
      ]
    },
    "wasm-gc": {
      "exports": [
        "add",
        "fib:test"
      ]
    },
    "js": {
      "exports": [
        "add",
        "fib:test"
      ],
      "format": "esm"
    }
  }
}

Each backend has a separate definition. For JS backend, a format option is used to specify whether the generated JavaScript file should be released as an ES Module (esm), a CommonJS module (cjs), or an immediately invoked function expression (iife).

The example above will export function add and fib, and the function fib will be exported with the name of test.

For WasmGC backend, the _start function should always be called to initialize all the global instances defined in MoonBit program.

Use compiled Wasm

To use the compiled Wasm, you need to initialize the Wasm module with the host functions so as to meet the needs of the foreign functions, and then use the exported functions provided by the Wasm module.

Provide host functions

To use the compiled Wasm, you must provide All declared foreign functions in Wasm import object.

For example, to use wasm compiled from above code snippet in JavaScript:

WebAssembly.instantiateStreaming(fetch("xxx.wasm"), {
  Math: {
    cos: (d) => Math.cos(d),
  },
});

Check out the documentation such as MDN or the manual of runtime that you're using to embed the Wasm.

Example: Smiling face

Let's walk through a full example to draw a smiling face using Canvas API in MoonBit. Suppose you created a new project with moon new draw

// We first declare a type representing the context of canvas
type Canvas_ctx

// We then declare the foreign function interfaces
fn begin_path(self : Canvas_ctx) = "canvas" "beginPath"
fn arc(self : Canvas_ctx, x : Int, y : Int, radius : Int, start_angle : Double,
    end_angle : Double, counterclockwise : Bool) = "canvas" "arc"
fn move_to(self : Canvas_ctx, x : Int, y : Int) = "canvas" "moveTo"
fn stroke(self : Canvas_ctx) = "canvas" "stroke"

fn get_pi() -> Double = "math" "PI"
let pi : Double = get_pi()

// We then apply these functions to define the drawing function upon the context
pub fn draw(self : Canvas_ctx) -> Unit {
  self.begin_path()
  self.arc(75, 75, 50, 0.0, pi * 2.0, true) // Outer circle
  self.move_to(110, 75)
  self.arc(75, 75, 35, 0.0, pi, false) // Mouth (clockwise)
  self.move_to(65, 65)
  self.arc(60, 65, 5, 0.0, pi * 2.0, true) // Left eye
  self.move_to(95, 65)
  self.arc(90, 65, 5, 0.0, pi * 2.0, true) // Right eye
  self.stroke()
}

// We also demonstrate the `println` functionality here
pub fn display_pi() -> Unit {
  println("PI: \{pi}")
}
{
  "link": {
    "wasm": {
      "exports": ["draw", "display_pi"]
    },
    "wasm-gc": {
      "exports": ["draw", "display_pi"]
    }
  }
}

Build the project using moon build. We recommend using Wasm with GC integration whenever possible (which is the default). If the environment does not support the GC feature, use the --target wasm option instead.

We now can use it from JavaScript.

<html lang="en">
  <body>
    <canvas id="canvas" width="150" height="150"></canvas>
  </body>
  <script>
    // import object for defining the FFI
    const importObject = {
      // TODO
    }

    const canvas = document.getElementById("canvas");
    if (canvas.getContext) {
      const ctx = canvas.getContext("2d");
      WebAssembly.instantiateStreaming(fetch("target/wasm-gc/release/build/lib/lib.wasm"), importObject).then(
        (obj) => {
          // Always call _start to initialize the environment
          obj.instance.exports._start();
          // Pass the JS object as parameter to draw the smiling face
          obj.instance.exports["draw"](ctx);
          // Display the value of PI
          obj.instance.exports["display_pi"]();
        }
      );
    }
  </script>
</html>

For import object, we need to provide all the FFI used in the previously defined program: the canvas rendering API, the math API and finally, an API for printing to the console used by the println or print function.

As of the canvas rendering API and the math API, we can use the following code to convert the methods of objects into function calls that accept the object as the first parameter, and convert the constant properties of objects into functions that returns the value:

function prototype_to_ffi(prototype) {
  return Object.fromEntries(
    Object.entries(Object.getOwnPropertyDescriptors(prototype))
      .filter(([_key, value]) => value.value)
      .map(([key, value]) => {
        if (typeof value.value == 'function')
          return [key, Function.prototype.call.bind(value.value)]
        // TODO: it is also possible to convert properties into getters and setters
        else
          return [key, () => value.value]
      })
  );
}

const importObject = {
  canvas: prototype_to_ffi(CanvasRenderingContext2D.prototype),
  math: prototype_to_ffi(Math),
  // ...
}

As of the printing service, we can provide the following closure so that it buffers the bytes of string until it needs to be logged to the console:

const [log, flush] = (() => {
  var buffer = [];
  function flush() {
    if (buffer.length > 0) {
      console.log(new TextDecoder("utf-16").decode(new Uint16Array(buffer).valueOf()));
      buffer = [];
    }
  }
  function log(ch) {
    if (ch == '\n'.charCodeAt(0)) { flush(); }
    else if (ch == '\r'.charCodeAt(0)) { /* noop */ }
    else { buffer.push(ch); }
  }
  return [log, flush]
})();

const importObject = {
  // ...
  spectest: {
    print_char: log
  },
}

// ...
WebAssembly.instantiateStreaming(fetch("target/wasm-gc/release/build/lib/lib.wasm"), importObject).then(
  (obj) => {
    obj.instance.exports._start();
    // ...
    flush()
  }
);

Now, we put them together, so this is our final complete index.html:

<!DOCTYPE html>
<html>

<head></head>

<body>
  <canvas id="canvas" width="150" height="150"></canvas>
  <script>
    function prototype_to_ffi(prototype) {
      return Object.fromEntries(
        Object.entries(Object.getOwnPropertyDescriptors(prototype))
          .filter(([_key, value]) => value.value)
          .map(([key, value]) => {
            if (typeof value.value == 'function')
              return [key, Function.prototype.call.bind(value.value)]
            else
              return [key, () => value.value]
          })
      );
    }

    const [log, flush] = (() => {
      var buffer = [];
      function flush() {
        if (buffer.length > 0) {
          console.log(new TextDecoder("utf-16").decode(new Uint16Array(buffer).valueOf()));
          buffer = [];
        }
      }
      function log(ch) {
        if (ch == '\n'.charCodeAt(0)) { flush(); }
        else if (ch == '\r'.charCodeAt(0)) { /* noop */ }
        else { buffer.push(ch); }
      }
      return [log, flush]
    })();



    const importObject = {
      canvas: prototype_to_ffi(CanvasRenderingContext2D.prototype),
      math: prototype_to_ffi(Math),
      spectest: {
        print_char: log
      },
    }

    const canvas = document.getElementById("canvas");
    if (canvas.getContext) {
      const ctx = canvas.getContext("2d");
      WebAssembly.instantiateStreaming(fetch("target/wasm-gc/release/build/lib/lib.wasm"), importObject).then(
        (obj) => {
          obj.instance.exports._start();
          obj.instance.exports["draw"](ctx);
          obj.instance.exports["display_pi"]();
          flush()
        }
      );
    }
  </script>
</body>

</html>

Make sure that draw.wasm and index.html are in the same folder, then start a http server at this folder. For example, using Python:

python3 -m http.server 8080

Goto http://localhost:8080 in your browser, there should be a smile face on the screen and an output on the console:

A smile face webpage with browser devtools open