JS Interop

Interfacing with JavaScript from Onyx is easy thanks to the core.js package. It was inspired from syscall/js, made by the wonderful people over at on the Go team.

The core.js package abstracts away the details of managing references to JS values from Onyx, so you are able to write code that uses JS values without caring about all the internal details.

For example, here is a simple program that runs on a web browser. It creates a new button element, add a click event handler that will call an Onyx function, then adds the button to the page.

use core.js

main :: () {
    // Lookup the document object in the global scope (i.e. window).
    document := js.Global->get("document");

    // Call createElement to make a new button, then set the text of the button.
    button := document->call("createElement", "button");
    button->set("textContent", "Click me!");

    // Call addEventListener to handle the `click` event.
    // Use js.func to wrap an Onyx function to be available from JS.
    button->call("addEventListener", "click", js.func((this, args) => {
        js.Global->call("alert", "Hello from Onyx!");

        return js.Undefined;
    }));

    // Call appendChild on the body to insert the button on the page.
    document->get("body")->call("appendChild", button);
}

While compiling this program, be sure to add the -r js flag, as it specifies you are targeting a JS runtime.

onyx build -o app.wasm -r js program.onyx

This will generate two files, app.wasm and app.wasm.js. The .js file exists to allow you to load and call your Onyx code from JS. Here is a simple HTML page that will load the JS and start the program, which will in turn call main.

<html>
    <head>
        <title>Onyx program</title>
        <script type="module">
            import Onyx from "/app.wasm.js"
            let app = await Onyx.load("/app.wasm")
            app.start()  // Bootstrap program and call main
        </script>
    </head>
    <body>
    </body>
</html>

Load this in your favorite web browser from a local web server and you should see a button on the page. Click it to test the program!

Some internal details

There are some nuances that are worth mentioning about how this library is currently setup.

The .start() method does start the program and invoke your main function, but it also does a little more. It also bootstraps the standard library, preparing buffers and allocators used by most Onyx programs. For this reason, even if you are not going to do anything in your main program and solely want to use Onyx as auxiliary to your main code, you still need to call the .start() method; just leave the main procedure empty.

When you want to invoke a specific Onyx function from JS, you have to do two things. First, the procedure you wish to call has to have the following signature: (js.Value, [] js.Value) -> js.Value. The first argument is the this implicit parameter. The second argument is a slice of js.Values that are the actual arguments. Here is a simple add procedure using this signature.

use core.js

add :: (this: js.Value, args: [] js.Value) -> js.Value {
    a := args[0]->as_int() ?? 0;
    b := args[1]->as_int() ?? 0;

    res := js.Value.from(a + b);

    return res;
}

Second, export the procedure from Onyx using the #export directive.

#export "add" add

Then, you can use the .invoke() method to invoke the procedure with an arbitrary number of arguments.

app.invoke("add", 123, 456); // Returns 579

As a slight aid, if you forget to call .start(), .invoke() will automatically call it for you the first time. So, if you use invoke and are wondering why the main of your procedure is executing, you likely forgot to call start.

Understanding the API

The API provided by core.js is a very thin wrapper around normal JS operations. The best way to understand it is to understand what each of the methods does in JS. Once you understand how each JS operation maps to the corresponding Onyx method, it is relatively easy to translate JS code into Onyx.

Value.new_object

Creates a new empty object. Equivalent of writing {} in JS.

Value.new_array

Creates a new empty array. Equivalent of writing [] in JS.

Value.from

Converts an Onyx value into a JS value, if possible.

Value.as_bool, Value.as_float, Value.as_int, Value.as_str

Convert a JS value into an Onyx value, if possible.

Value.type

Returns the type of the JS value. Similar to typeof in JS, but it has sensible semantics.

Value.call

Calls a method on an object, passing the object as the this argument. x->call("y", "z") is equivalent to x.y("z") in JS.

Value.invoke

Invokes a function, passing null as the this argument. x->invoke("y") is equivalent to x("y") in JS.

Value.delete

Invokes the delete operator from JS on the property of the object.

Value.new

Invokes the new operator on the value.

Value.get

x->get("y") is equivalent to writing x.y in JS.

Value.set

x->set("y", 123) is equivalent to writing x["y"] = 123 in JS.

Value.length

Shorthand for x->get("length")->as_int() ?? 0, since this operation is so common.

Value.index

x->index(y) is equivalent to writing x[y] in JS.

Value.instance_of

x->instance_of(y) is equivalent to writing x instanceof y in JS.

Value.equals

Returns true if two values are equal.

Value.is_null

Returns if the value contained is null.

Value.is_undefined

Returns if the value contained is undefined.

Value.is_nan

Returns if the value contained is NaN.

Value.truthy

Return true if the value is considered "truthy" under JS's semantics.

Value.leak

Removes the value from the tracked pool of objects, so it will not automatically be freed.

Value.release

Frees the JS value being stored. After calling this the value should not be used anymore.

Defining your own JS module

This documentation will be coming soon!