Skip to content

Internal JavaScript bindings

3 min read

Dream runs in the browser and Node as WebAssembly. Talking to JavaScript uses three pieces:

Piece What it's for Docs
extern fun a typed, fixed-signature function that lives in JS (Math.max, your glue code) this page
js a dynamic handle to any live JS value, used with native syntax The js type
function values passing functions across the boundary in either direction Callbacks
@c(...) binds an extern to a C library in native executables and library outputs C Interop

Js.* is WASM/JS-host only: a native build aborts if guest code tries to call into JavaScript.

This page covers extern functions.

Declaring an extern function

An extern fun has a signature but no body. Call it like any other function:

internal extern fun alert(msg: string): void;

fun main(): void {
    alert("Hello from Dream!");
}

By default it binds to a JS global of the same name (alert). Three things cooperate:

  • extern fun declares the signature on the Dream side.
  • @js("module", "field") optionally remaps which JS object and property it binds to (JS-only).
  • Dream stdlib hosts that exist on native and WASM use @runtime("name") instead of @js.
  • The runtime marshals values, binds externs to JS globals, and bridges Promises for extern async fun.

Restrictions

Extern functions cannot have a body, cannot be generic, and cannot be combined with public.

Remapping the import name

@js(module, name) controls which JS object and property the extern binds to:

@js(
    "dom",
    "setText"
) // binds to importObject["dom"]["setText"]
internal extern fun set_text(value: string): void;

@js(
    "console"
) // module only -> field defaults to the function name
internal extern fun log(msg: string): void;

Dream runtime hosts (@runtime)

Stdlib functions implemented by the Dream runtime (files, process, crypto, …) bind with @runtime("name"). That is not JavaScript-only: WASM imports Dream.name and native dream run calls the C symbol name.

@runtime("fileRead")
static internal extern fun file_read(borrow path: string): string;

@runtime cannot be combined with @js, @c, or @intrinsic on the same extern. @native / @node / @web still restrict which targets may mention the API (@native @node @runtime("processRun")).

Running it from JavaScript

Compile a .dream file, then load the .wasm with the Dream runtime:

import { run } from "./runtime/dream.js";
await run("hello.wasm");   // binds externs, calls main

Optionally emit a smaller sibling *.web.runtime.js / *.node.runtime.js (only the host pieces that program needs) with --runtime plus one or both host flags. Without these flags, compile does not write a selective runtime — use the full shared host instead.

dream --runtime --web hello.dream           # hello.web.runtime.js
dream --runtime --node hello.dream          # hello.node.runtime.js
dream --runtime --web --node hello.dream    # both in one compile
import { run } from "./hello.web.runtime.js";
await run("hello.wasm");

Release builds (--release / -O) also minify the selective runtime and emit pre-compressed siblings — hello.wasm.gz / .br and hello.web.runtime.js.gz / .br. Browsers never compress on their own; serve them with gzip_static / brotli_static (nginx), a CDN, or any static server that negotiates encoding (e.g. npx serve).

Auto-binding to JS globals

For every extern you do not supply explicitly, the runtime resolves it against the JS global scope:

  • The default env module maps to a bare global — internal extern fun alert(...) binds to alert.
  • @js("module", "name") maps to a property — @js("console", "log") binds to console.log, @js("Math", "max") to Math.max.

Built-in browser and Node APIs therefore need no glue. Pass imports only for your own functions, keyed by the Dream function name:

await run("hello.wasm", {
  imports: {
    square: (n) => n * n,
  },
});

If an extern matches no global and you don't provide it, the runtime installs a stub that throws only if actually called — so the module still loads. For full control, use load(source, options) instead of run; it returns the instance without calling main.

Value marshaling

Arguments and returns convert between Dream and JavaScript:

Dream type As argument As return value
int, float, double number number
bool boolean boolean
string string return a string
T[] Array of marshaled elements (pointer)
object, classes, List<T> opaque pointer (number) (pointer)

For reference types, read the underlying data with the instance helpers:

mod.readString(ptr);          // UTF-16 string
mod.readArray(ptr, "int");    // -> number[]
mod.readList(ptr, "string");  // List<string> -> string[]
mod.readStruct(ptr, [         // class by field schema (declaration order)
  { name: "x", type: "int" },
  { name: "y", type: "int" },
]);

To hand a string back to Dream, call mod.writeString(str) (the runtime also does this for you when a return value is a string).

Beyond fixed signatures

extern fun is ideal for known signatures. For open-ended JS values (a DOM node, a fetch Response, a RegExp) you want to read and call natively, use the dynamic js type:

let el = js.global.document.getElementById("app");
el.textContent = "hello";

Functions cross the boundary in both directions too — see Callbacks.

Built on interop

  • Regex — Regex in system.text (not a JS RegExp).

Host feature matrix

Most stdlib packages behave identically on native, browser, and Node hosts. Check each retained package's platform notes for file access and process support. Unicode helpers and console input work on native and JS hosts; browser console input uses a prompt because there is no stdin.

Guides in this section