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: