A working, zero-dependency implementation of WASI preview1, with lots of filesystem options. It will work in places other than a browser, but that is the primary target. It's easy, light, simple, effortless to modify/extend, and should work with any WASI-enabled wasm and any modern js runtime.
If you need framebuffer/audio/input WASI devices, check out web-zen-dev.
Install into your bundled project with npm i easywasi
. You can also use it on the web, directly from cdn:
<script type="module">
import WasiPreview1 from 'https://esm.sh/easywasi'
</script>
And I like to use sourcemaps:
<script type="importmap">
{
"imports": {
"easywasi": "https://esm.sh/easywasi",
"@zenfs/core": "https://esm.sh/@zenfs/core",
"@zenfs/dom": "https://esm.sh/@zenfs/dom",
"@zenfs/zip": "https://esm.sh/@zenfs/zip"
}
}
</script>
<script type="module">
import { WasiPreview1 } from 'easywasi'
import { configure, InMemory, fs } from '@zenfs/core'
import { IndexedDB } from '@zenfs/dom'
import { Zip } from '@zenfs/zip'
</script>
You can use it without a filesystem, like this:
import WasiPreview1 from 'easywasi'
const wasi_snapshot_preview1 = new WasiPreview1()
const {instance: { exports }} = await WebAssembly.instantiateStreaming(fetch('example.wasm'), {
wasi_snapshot_preview1,
// your imports here
})
wasi_snapshot_preview1.start(exports)
To really unlock it's power, though, give it an fs
instance, like from zen-fs. Here is an example that will mount a zip file to /zip
, in-memory storage to /tmp
, and IndexedDB to /home
.
Things to note:
/
has the default in-memory backend./mnt
is a bit special in zenfs, and not traversed by a file-list, so if you want that, put it somewhere else
import WasiPreview1 from 'easywasi'
import { configure, InMemory } from '@zenfs/core'
import { IndexedDB } from '@zenfs/dom'
import { Zip } from '@zenfs/zip'
await configure({
mounts: {
'/zip': { backend: Zip, data: await await fetch('mydata.zip').then(r => r.arrayBuffer()) },
'/tmp': InMemory,
'/home': IndexedDB
}
})
// here, you could use fs to modify filesystem however you need (write files, make directories, etc)
const wasi_snapshot_preview1 = new WasiPreview1({fs})
const {instance: { exports }} = await WebAssembly.instantiateStreaming(fetch('example.wasm'), {
wasi_snapshot_preview1,
// your imports here
})
wasi_snapshot_preview1.start(exports)
Have a look in example to see how I fit it all together.
Keep in mind, you can easily override every function yourself, too, like if you want to implement the socket-API, which is the only thing I left out:
import {defs, WasiPreview1} from 'easywasi'
class WasiPreview1WithSockets extends WasiPreview1 {
constructor(options={}) {
super(options)
// do something with options to setup socket
}
// obviously implement these however
sock_accept (fd, flags) {
return defs.ERRNO_NOSYS
}
sock_recv (fd, riData, riFlags) {
return defs.ERRNO_NOSYS
}
sock_send (fd, siData, riFlags) {
return defs.ERRNO_NOSYS
}
sock_shutdown (fd, how) {
return defs.ERRNO_NOSYS
}
}
// usage
const wasi_snapshot_preview1 = new WasiPreview1WithSockets({fs})
const {instance: { exports }} = await WebAssembly.instantiateStreaming(fetch('example.wasm'), {
wasi_snapshot_preview1
})
wasi_snapshot_preview1.start(exports)
Have a look at WasiPreview1 to figure out how to implement it, if you want things to work differently.
- this article has some nice initial ideas
- this article has some good WASI imeplentations
- browser-wasi-shim has a very nice interface, and this is basically the same in JS, but using more extensible filesystem, and I improved a few little things.