Skip to content

Commit

Permalink
Add reactors docs and test (#291)
Browse files Browse the repository at this point in the history
  • Loading branch information
taybenlor committed Jan 22, 2024
1 parent 884c1c0 commit 6a001c7
Show file tree
Hide file tree
Showing 7 changed files with 177 additions and 0 deletions.
44 changes: 44 additions & 0 deletions packages/wasi/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,16 @@ const result = wasi.start(wasm, {
});
```

If you are working with a Reactor instead of a command, you can instead use:

```js
const exports = wasi.initialize(wasm, {
memory: myMemory,
});
```

The returned exports will be the exports from your WebAssembly module.

## Using the WASIWorker

A worker is provided for using the WASI runner outside of the main thread. It
Expand Down Expand Up @@ -162,6 +172,40 @@ Cross-Origin-Embedder-Policy: require-corp
You can test that your page is Cross-Origin Isolated by opening the browser
console and checking `crossOriginIsolated` (see: [mdn docs](https://developer.mozilla.org/en-US/docs/Web/API/crossOriginIsolated)).

## Initializing a WASI Reactor

Reactors are modules that respond to calls, rather than running as a command.

You can initialize a WASI Reactor with `initialize` instead of `start`:

```js
import { WASI } from "@runno/wasi";

//...

const exports = WASI.initialize(fetch("/binary.wasm"), {
args: ["binary-name", "--do-something", "some-file.txt"],
env: { SOME_KEY: "some value" },
stdout: (out) => console.log("stdout", out),
stderr: (err) => console.error("stderr", err),
stdin: () => prompt("stdin:"),
fs: {
"/some-file.txt": {
path: "/some-file.txt",
timestamps: {
access: new Date(),
change: new Date(),
modification: new Date(),
},
mode: "string",
content: "Some content for the file.",
},
},
});
```

The `WASI.initialize` call will return the exports from the WebAssembly module.

## The filesystem

`@runno/wasi` internally emulates a unix-like filesystem (FS) from a flat
Expand Down
2 changes: 2 additions & 0 deletions packages/wasi/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
List dir in arg1 and print on STDOUT
</option>
<option value="/bin/languages/qjs.wasm">QuickJS</option>
<option value="/reactors/return_57.wasi.wasm">Return 57</option>
</select>
</label>
</p>
Expand All @@ -61,6 +62,7 @@
</p>
</form>
<button id="run">Run</button>
<button id="run-reactor">Reactor</button>

<p><strong>Exit code:</strong><code id="exit-code"></code></p>

Expand Down
91 changes: 91 additions & 0 deletions packages/wasi/lib/wasi/wasi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@ function popDebugStrings(): { [key: string]: string }[] {
return current;
}

export class InvalidInstanceError extends Error {}
export class InitializationError extends Error {}

/**
* Implementation of a WASI runner for the browser.
* Explicitly designed for the browser context, where system resources
Expand All @@ -56,7 +59,12 @@ export class WASI implements SnapshotPreview1 {
memory!: WebAssembly.Memory;
context: WASIContext;
drive: WASIDrive;
hasBeenInitialized: boolean = false;

/**
* Start a WASI command.
*
*/
static async start(
wasmSource: Response | PromiseLike<Response>,
context: Partial<WASIContextOptions> = {}
Expand All @@ -69,6 +77,24 @@ export class WASI implements SnapshotPreview1 {
return wasi.start(wasm);
}

/**
* Initialize a WASI reactor.
*
* Returns the WebAssembly instance exports.
*/
static async initialize(
wasmSource: Response | PromiseLike<Response>,
context: Partial<WASIContextOptions> = {}
) {
const wasi = new WASI(context);
const wasm = await WebAssembly.instantiateStreaming(
wasmSource,
wasi.getImportObject()
);
wasi.initialize(wasm);
return wasm.instance.exports;
}

constructor(context: Partial<WASIContextOptions>) {
this.context = new WASIContext(context);
this.drive = new WASIDrive(this.context.fs);
Expand All @@ -81,17 +107,44 @@ export class WASI implements SnapshotPreview1 {
};
}

/**
* Start a WASI command.
*
* See: https://github.com/WebAssembly/WASI/blob/main/legacy/application-abi.md
*/
start(
wasm: WebAssembly.WebAssemblyInstantiatedSource,
options: {
memory?: WebAssembly.Memory;
} = {}
): WASIExecutionResult {
if (this.hasBeenInitialized) {
throw new InitializationError(
"This instance has already been initialized"
);
}

this.hasBeenInitialized = true;
this.instance = wasm.instance;
this.module = wasm.module;
this.memory =
options.memory ?? (this.instance.exports.memory as WebAssembly.Memory);

// If the export contains `_initialize` it's a reactor which
// should be initialized instead
if ("_initialize" in this.instance.exports) {
throw new InvalidInstanceError(
"WebAssembly instance is a reactor and should be started with initialize."
);
}

// If the export doesn't contain `_start` we can't start it
if (!("_start" in this.instance.exports)) {
throw new InvalidInstanceError(
"WebAssembly instance doesn't export _start, it may not be WASI or may be a Reactor."
);
}

const entrypoint = this.instance.exports._start as () => void;
try {
entrypoint();
Expand Down Expand Up @@ -119,6 +172,44 @@ export class WASI implements SnapshotPreview1 {
};
}

/**
* Initialize a WASI Reactor.
*
* See: https://github.com/WebAssembly/WASI/blob/main/legacy/application-abi.md
*/
initialize(
wasm: WebAssembly.WebAssemblyInstantiatedSource,
options: {
memory?: WebAssembly.Memory;
} = {}
) {
if (this.hasBeenInitialized) {
throw new InitializationError(
"This instance has already been initialized"
);
}

this.hasBeenInitialized = true;
this.instance = wasm.instance;
this.module = wasm.module;
this.memory =
options.memory ?? (this.instance.exports.memory as WebAssembly.Memory);

// If the export contains `_start` it's a command which
// should be started instead
if ("_start" in this.instance.exports) {
throw new InvalidInstanceError(
"WebAssembly instance is a command and should be started with start."
);
}

// Optionally initialize if the initialize export is present
if ("_initialize" in this.instance.exports) {
const initialize = this.instance.exports._initialize as () => void;
initialize();
}
}

getImports(
version: "unstable" | "preview1",
debug?: DebugFn
Expand Down
Binary file added packages/wasi/public/reactors/return_57.wasi.wasm
Binary file not shown.
7 changes: 7 additions & 0 deletions packages/wasi/reactors/return_57.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
// Built with
// wasi-sdk-21.0/bin/clang --sysroot=wasi-sdk-21.0/share/wasi-sysroot -mexec-model=reactor -o return_57.wasm return_57.c

__attribute__((export_name("return_57"))) int return_57()
{
return 57;
}
16 changes: 16 additions & 0 deletions packages/wasi/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ import { WASI, WASIContext } from "../lib/main";
const programSelect = document.getElementById("program")! as HTMLSelectElement;
const argsInput = document.getElementById("args")! as HTMLInputElement;
const runButton = document.getElementById("run")! as HTMLButtonElement;
const runReactorButton = document.getElementById(
"run-reactor"
)! as HTMLButtonElement;

const exitCode = document.getElementById("exit-code")! as HTMLElement;
const stdoutPre = document.getElementById("stdout")! as HTMLPreElement;
Expand Down Expand Up @@ -63,3 +66,16 @@ runButton.addEventListener("click", async () => {
);
exitCode.textContent = result.exitCode.toString();
});

runReactorButton.addEventListener("click", async () => {
stdoutPre.textContent = "";
stderrPre.textContent = "";

const url = programSelect.value;
const exports = await WASI.initialize(fetch(url));

// This is very specific to the current reactor
// but I just want a simple e2e test. Write something a bit more general
// when there are more reactor tests.
exitCode.textContent = (exports.return_57 as Function)();
});
17 changes: 17 additions & 0 deletions packages/wasi/tests/reactor.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { test, expect } from "@playwright/test";

test.beforeEach(async ({ page }) => {
await page.goto("http://localhost:5173");
});

test.describe("reactor-return-57", () => {
test.beforeEach(async ({ page }) => {
await page.locator("select").selectOption("/reactors/return_57.wasi.wasm");
});

test("returns 57", async ({ page }) => {
await page.locator("text=Reactor").click();

await expect(page.locator("#exit-code")).toHaveText("57");
});
});

0 comments on commit 6a001c7

Please sign in to comment.