Skip to content

Commit 304bf66

Browse files
feat(examples): add fs write file example
1 parent 8e5a806 commit 304bf66

12 files changed

Lines changed: 997 additions & 1 deletion

File tree

examples/components/README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ A brief description of the examples contained in this folder:
99
|------------------------------------------------------------|--------------------------------------------------------------------------------------------------|
1010
| [`add`](./add) | `export`s basic functionality with simple types with a bare function export (deprectaed) |
1111
| [`adder`](./adder) | `export`s basic functionality with simple types with an interface (recommended) |
12+
| [`fs-write-file`](./fs-write-file) | Example of using `wasi:filesystem` (p2) to write a file to disk |
1213
| [`host-logging`](./host-logging) | Showcases the use of the `wasi:logging` interface with a custom embedder ("host") function |
1314
| [`http-hello-world`](./http-hello-world) | HTTP server using the [`wasi:http/incoming-handler`][wasi-http], the hard way. |
1415
| [`http-server-fetch-handler`](./http-server-fetch-handler) | HTTP server using standards-forward `fetch()` event handling built into [StarlingMonkey][sm] |
@@ -24,7 +25,8 @@ A brief description of the examples contained in this folder:
2425
[hono]: https://hono.dev
2526
[nodejs]: https://nodejs.org
2627
[sm]: https://github.com/bytecodealliance/StarlingMonkey
27-
[wasi-http]: https://github.com/WebAssembly/wasi-http
28+
[wasi-http]: https://github.com/WebAssembly/WASI/tree/main/proposals/http
2829
[wasi-logging]: https://github.com/WebAssembly/wasi-logging
30+
[wasi-fs]: https://github.com/WebAssembly/WASI/tree/main/proposals/filesystem
2931
[webidl2wit]: https://github.com/wasi-gfx/webidl2wit
3032
[webidl]: https://developer.mozilla.org/en-US/docs/Glossary/WebIDL
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
node_modules
2+
dist
3+
*.wasm
4+
pnpm-lock.yaml
5+
output.txt
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
v22.5.1
Lines changed: 216 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,216 @@
1+
# WASI `fs-write-file` in JavaScript
2+
3+
This folder contains a WebAssembly Javascript component that uses [`wasi:filesystem`][wasi-fs] to write
4+
a file to a pre-opened directory.
5+
6+
It uses [`jco`][jco] to:
7+
8+
- Generate a WebAssembly component (via `jco componentize`) that can be executed by a WebAssembly runtime (ex. [`wasmtime serve`][wasmtime])
9+
- Transpile the component (via `jco transpile`) into one that can run on NodeJS
10+
11+
[nodejs]: https://nodejs.org
12+
[jco]: https://bytecodealliance.github.io/jco/
13+
[wasi-fs]: https://github.com/WebAssembly/WASI/tree/main/proposals/filesystem
14+
[wasmtime]: https://github.com/bytecodealliance/wasmtime
15+
16+
# Quickstart
17+
18+
## Dependencies
19+
20+
First, install required dependencies:
21+
22+
```console
23+
npm install
24+
```
25+
26+
> [!NOTE]
27+
> As this is a regular NodeJS project, you can use your package manager of choice (e.g. `yarn`, `pnpm`)
28+
29+
At this point, since this project is *just* NodeJS, you could use the module from any NodeJS project or browser project where appropriate.
30+
31+
That said, we'll be focusing on building the JS code we've written so far into a WebAssembly binary, which can run *anywhere*
32+
WebAssembly runtimes are supported, including in other languages, and the browser (experimental support).
33+
34+
## Building the WebAssembly component
35+
36+
To turn our JS into a WebAssembly component, we can use `jco componentize`:
37+
38+
```console
39+
jco componentize component.js --wit wit --world-name component --out dist/component.wasm
40+
```
41+
42+
> [!NOTE]
43+
> For ease, you can do all of this with `pnpm build` or `npm run build`, or your npm-compatible build tool of choice.
44+
45+
You should see output like the following:
46+
47+
```
48+
pnpm build
49+
50+
> build
51+
> jco componentize component.js --wit wit --world-name component --out dist/component.wasm
52+
53+
OK Successfully written dist/component.wasm.
54+
```
55+
56+
Now that your component has been built, we can do *alot* of things to inspect it.
57+
58+
You can recognize a WebAssemblyc omponent versus a module via the magic strings recognized by tools like `file`:
59+
60+
```
61+
$ file dist/component.wasm
62+
dist/component.wasm: WebAssembly (wasm) binary version 0x1000d (component)
63+
```
64+
65+
66+
We can do all this quickly with the `build` script:
67+
68+
```console
69+
npm run build
70+
```
71+
72+
## Transpiling the WebAssembly component
73+
74+
To convert the component we built to a form in which it is runnable by any JS runtime with `WebAssembly` support, we use
75+
`jco transpile`:
76+
77+
```console
78+
jco transpile dist/component.wasm --instantiation=async -o dist/transpiled
79+
```
80+
81+
This command takes `dist/component.wasm` and converts it into a folder with multiple files:
82+
83+
```
84+
dist/transpiled
85+
├── component.core2.wasm
86+
├── component.core3.wasm
87+
├── component.core4.wasm
88+
├── component.core.wasm
89+
├── component.d.ts
90+
├── component.js
91+
└── interfaces
92+
├── jco-test-test.d.ts
93+
├── wasi-cli-stderr.d.ts
94+
├── wasi-cli-stdin.d.ts
95+
├── wasi-cli-stdout.d.ts
96+
├── wasi-cli-terminal-input.d.ts
97+
├── wasi-cli-terminal-output.d.ts
98+
├── wasi-cli-terminal-stderr.d.ts
99+
├── wasi-cli-terminal-stdin.d.ts
100+
├── wasi-cli-terminal-stdout.d.ts
101+
├── wasi-clocks-monotonic-clock.d.ts
102+
├── wasi-clocks-wall-clock.d.ts
103+
├── wasi-filesystem-preopens.d.ts
104+
├── wasi-filesystem-types.d.ts
105+
├── wasi-http-outgoing-handler.d.ts
106+
├── wasi-http-types.d.ts
107+
├── wasi-io-error.d.ts
108+
├── wasi-io-poll.d.ts
109+
├── wasi-io-streams.d.ts
110+
└── wasi-random-random.d.ts
111+
112+
2 directories, 25 files
113+
```
114+
115+
The files in this folder contain WebAssembly modules (i.e. what is supported by `WebAssembly` out of the box today), along with
116+
glue code, and typescript necessary to interact with the interfaces that are used by your component.
117+
118+
We can do all of this quickly with the `transpile` script:
119+
120+
```console
121+
npm run transpile
122+
```
123+
124+
## Running the transpiled WebAssembly component
125+
126+
As we have now transpiled our WebAssembly Component into it's constituent modules and glue code, we can instantiate it
127+
and use it from JS.
128+
129+
See the code in `run-transpiled.js`, importantly the following lines:
130+
131+
```js
132+
import { instantiate } from "./dist/transpiled/component.js";
133+
134+
// ...
135+
136+
const instance = await instantiate(...);
137+
```
138+
139+
Another set of important lines are the import and usage of a WASI Shim, which controls the sandbox in which
140+
your WebAssembly component will execute:
141+
142+
```js
143+
import { WASIShim } from "@bytecodealliance/preview2-shim/instantiation";
144+
145+
// ...
146+
147+
const shim = new WASIShim({
148+
sandbox: {
149+
preopens: {
150+
'/': CURRENT_PATH,
151+
},
152+
}
153+
});
154+
155+
const instance = await instantiate(undefined, shim.getImportObject());
156+
```
157+
158+
As WebAssembly has a capability-driven security model, all access to the filesytem must be provided explicitly.
159+
160+
To run the demo all at once:
161+
162+
```
163+
npm run transpiled-js
164+
```
165+
166+
# How it works
167+
168+
## Exploring this Component WIT
169+
170+
As WebAssembly components are powered by a [WebAssembly Interface Types ("WIT")][wit]-first workflow, making
171+
a HTTP handler component in WebAssembly requires creating a WIT contract to represent that component.
172+
173+
This folder contains a `wit` directory (by convention) with the following content in `component.wit`:
174+
175+
```wit
176+
package jco:test;
177+
178+
interface test {
179+
run: func() -> string;
180+
}
181+
182+
world component {
183+
import wasi:filesystem/types@0.2.8;
184+
import wasi:filesystem/preopens@0.2.8;
185+
186+
export test;
187+
}
188+
```
189+
190+
> [!NOTE]
191+
> See [`wit/component.wit`](./wit/component.wit)
192+
>
193+
> For more information on the WIT syntax, see [WIT design docs][wit]
194+
195+
We make use of the [WebAssembly System Interface ("WASI") for filesystems][wasi-fs] interface here, pulling in
196+
pre-established interfaces interfaces for getting a list of pre-opened directories.
197+
198+
[wasi-http]: https://github.com/WebAssembly/wasi-http
199+
[wit]: https://github.com/WebAssembly/component-model/blob/main/design/mvp/WIT.md
200+
201+
## Resolving references WebAssembly types
202+
203+
As we intend to use the WASI FS interface, we need to pull in WIT interface(s) and types that are referred to by
204+
the `wasi:filesystem/preopens` interface.
205+
206+
One way fo doing this is *downloading* the WIT from Bytecode Alliance repositories, using [`wkg`, from the `bytecodealliance/wasm-pkg-tools`][wkg].
207+
208+
With a top level world in `wit/component.wit`, we can easily fetch all automatically-resolvable interfaces:
209+
210+
```console
211+
wkg wit fetch
212+
```
213+
214+
> [!NOTE]
215+
> Standard interfaces are automatically resolvable, but if custom interfaces are used, you'll need to configure `wkg`
216+
> so it knows where to find the relevant WIT information.
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
// For more information on how to use WASI filesystem interfaces
2+
//
3+
// see: https://github.com/WebAssembly/WASI/tree/main/proposals/filesystem/wit
4+
import { getDirectories } from "wasi:filesystem/preopens@0.2.8";
5+
6+
// This text encoder will be created at compile & JS VM snapshot time,
7+
// it will not be re-created on every call of the `run` export.
8+
const TEXT_ENCODER = new TextEncoder();
9+
10+
const FILENAME = "output.txt";
11+
12+
export const test = {
13+
run() {
14+
const preopens = getDirectories();
15+
if (preopens.length === 0) { throw "ERROR: no preopens"; }
16+
17+
// We expect that we have at least one preopen, and take the first one,
18+
// in which we will write the output file
19+
const dirDescriptor = preopens[0][0];
20+
21+
// NOTE: fs operations (open, write, sync) will throw if they fail
22+
const f = dirDescriptor.openAt(
23+
{symlinkFollow: false},
24+
FILENAME,
25+
{ create: true },
26+
{ write: true },
27+
);
28+
29+
f.write(TEXT_ENCODER.encode("Hello world, from component!"), 0);
30+
31+
f.sync();
32+
33+
return "SUCCESS: wrote file";
34+
}
35+
};
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
{
2+
"name": "fs-write-file",
3+
"description": "Codebase showcasing a simple use of wasi:filesystem (p2)",
4+
"type": "module",
5+
"scripts": {
6+
"build": "jco componentize component.js --wit wit --world-name component --out dist/component.wasm",
7+
"transpile": "jco transpile dist/component.wasm --instantiation=async -o dist/transpiled",
8+
"transpiled-js": "node run-transpiled.js",
9+
"all": "npm run build; npm run transpile; npm run transpiled-js"
10+
},
11+
"devDependencies": {
12+
"@bytecodealliance/jco": "^1.17.6"
13+
},
14+
"dependencies": {
15+
"@bytecodealliance/preview2-shim": "^0.17.8"
16+
}
17+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import { readFile, stat, rm } from "node:fs/promises";
2+
import { join } from "node:path";
3+
import { URL, fileURLToPath } from "node:url";
4+
import assert from "node:assert/strict";
5+
6+
import { WASIShim } from "@bytecodealliance/preview2-shim/instantiation";
7+
8+
// If this import listed below is missing, please run `npm run transpile`
9+
import { instantiate } from "./dist/transpiled/component.js";
10+
11+
const CURRENT_PATH = fileURLToPath(new URL("./", import.meta.url));
12+
13+
async function fileExists(p) {
14+
return stat(p).then(f => f.isFile()).catch(() => false);
15+
}
16+
17+
async function main() {
18+
let expectedPath = join(CURRENT_PATH, "output.txt");
19+
if (await fileExists(expectedPath)) {
20+
console.log("detected existing file at path, removing...");
21+
await rm(expectedPath);
22+
}
23+
24+
// Create the environment the transpiled component will use
25+
const shim = new WASIShim({
26+
sandbox: {
27+
preopens: {
28+
'/': CURRENT_PATH,
29+
},
30+
}
31+
});
32+
33+
// Create an instance of the transpiled component, with environment
34+
const instance = await instantiate(undefined, shim.getImportObject());
35+
36+
// Run the actual component function
37+
const res = instance.test.run();
38+
assert.strictEqual(res, "SUCCESS: wrote file");
39+
40+
assert(await fileExists(expectedPath), "expected output file exists");
41+
42+
const contents = await readFile(expectedPath);
43+
assert.strictEqual(contents.toString(), "Hello world, from component!", "expected output file exists");
44+
45+
console.log("successfully wrote output file");
46+
}
47+
48+
await main();
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package jco:test;
2+
3+
interface test {
4+
run: func() -> string;
5+
}
6+
7+
world component {
8+
import wasi:filesystem/types@0.2.8;
9+
import wasi:filesystem/preopens@0.2.8;
10+
11+
export test;
12+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package wasi:clocks@0.2.8;
2+
3+
interface wall-clock {
4+
record datetime {
5+
seconds: u64,
6+
nanoseconds: u32,
7+
}
8+
9+
now: func() -> datetime;
10+
11+
resolution: func() -> datetime;
12+
}
13+

0 commit comments

Comments
 (0)