- Notifications
You must be signed in to change notification settings - Fork396
Description
PoC implementation is going on here:https://github.com/scala-wasm/scala-wasm
Background
This issue is part of#4991 to support "server-side Wasm"
To support "server-side Wasm", we have to remove all the JS dependencies.WASI (WebAssembly System Interface) allows us to implement standard libraries that interact with systems (e.g.println
usingget-stdout
from WASIp2) without JS.
This issue discusses how do we supportWASI Preview2 (WASIp2) andWasm Component Model in Scala.js Wasm backend.
Wasm Component Model
The Wasm Component Model provides:
- Anew binary format calledcomponent that encapsulates traditional Wasm modules.
- AWIT (WebAssembly Interface Type) IDL for defining interfaces and types in a language-agnostic way.
- ACanonicalABI for standardizing how complex types like strings and structs are represented and exchanged between components.
Once we write a WIT, tools likewit-bindgen would generate bindings for any languages that supports Component Model. (people say "SDKs for free").
Additionally, unlike the traditional interop approach in Wasm, components avoid directly exposing linear memory, and thus it leads reducing the attack surface.
WASI preview2 (WASIp2)
WASI is a set of standardized APIs that allow Wasm modules to interact with system resources (e.g., files, networks, environment variables) in a portable way.WASI Preview2 (WASIp2) is the next version of WASI, designed based on Wasm Component Model.
**Design choice: why WASIp2 instead of WASIp1?**
One of the challenges of adopting WASIp2 is their current immaturity, as not many VMs and tools support them yet1. It might be safer to stick with WASI Preview1 (WASIp1), which is very stable and widely supported by many VMs and tools?
Why we didn't go with WASIp1
Previously, we prototyped server-side Wasm support using WASIp1 inthis issue.
We realized that supporting WASIp1 leads to introducing memory-related APIs (e.g., allocate/free and load/store operations on Wasm linear memory) at Scala.js source level and/or hard-code every WASIp1 functions in standard libraries since we couldn't find clear instruction how the "high-level structs" should be serialized into core Wasm values.
However, we don't like to expose such "unsafe" memory APIs in the Scala.js language, even if the "unsafe" parts of the code would typically be generated by tools likewit-bindgen
. At the source language and Scala.js IR level, these low-level details should be abstracted away and handled by the linker backend, rather than being exposed to developers.
WASIp2
The Wasm Component Model provides clear instructions on how high-level interface types should be serialized and deserialized through the Canonical ABI. This allows us to implement the serialization and deserialization logic for linear memory (or on the stack) within the Wasm linker backend, without exposing memory APIs to developers.
We never support WASIp1?
Depending on the future status of WASIp2 support, we might also want to support WASIp1. In that case, we would need to carefully examine the WASIp1witx
definitions, and it might be possible to identify high-level types and serialization/deserialization mechanisms similar to those in the Component Model.
https://github.com/WebAssembly/WASI/blob/40019a6181352388397b5b903740f29b26742146/legacy/preview1/witx/wasi_snapshot_preview1.witx
TL;DR: WIP demo
I have a PoC implementation for Wasm Component Model support in the forked repository:scala-wasm#5
packagetanishiking:test@0.0.1;// Scalaworldsocket {importtest;importwasi:cli/stdout@0.2.0;exportwasi:cli/run@0.2.0;}// Rustworldplug {exporttest;}interfacetest {ferris-say:func(content:string,width:u32)->string;}
// Cli.scalaobjectRun {@ComponentExport("wasi:cli/run@0.2.0","run")defrun(): component.Result[Unit,Unit]= {valout=Stdio.getStdout()valferris=Test.ferrisSay("Hello Scala!",80) out.blockingWriteAndFlush(ferris.getBytes()) component.Ok(()) }}objectTest {@ComponentImport("tanishiking:test/test@0.0.1","ferris-say")defferrisSay(content:String,width:UInt):String= component.native}// Stdio.scalaobjectStdio {@ComponentImport("wasi:cli/stdout@0.2.0","get-stdout")defgetStdout():OutputStream= component.native}@ComponentResourceImport("wasi:io/streams@0.2.0")traitOutputStreamextends component.Resource {@ComponentResourceMethod("blocking-write-and-flush")defblockingWriteAndFlush(contents:Array[UByte]): component.Result[Unit,StreamError]}
#[allow(warnings)]mod bindings;usecrate::bindings::exports::tanishiking::test::test::Guest;use ferris_says::say;structComponent;implGuestforComponent{fnferris_say(content:String,width:u32) ->String{letmut buf =Vec::new();say(content.as_str(), width.try_into().unwrap(),&mut buf).unwrap();returnString::from_utf8(buf).unwrap();}}bindings::export!(Component with_types_in bindings);
$ wasm-tools component embed wit examples/helloworld/.2.12/target/scala-2.12/hello-world-scalajs-example-fastopt/main.wasm -o main.wasm -w socket --encoding utf16$ wasm-tools component new main.wasm -o main.wasm$ wac plug --plug plugin/target/wasm32-wasip1/release/plugin.wasm main.wasm -o out.wasm$ wasmtime -W function-references,gc out.wasm ______________< Hello Scala!> -------------- \ \ _~^~^~_\) / o o\(/'_ - _' /'-----' \
(Note that the wasm-tools, wasmtime, and wac might need to be updated or built from source2.)
Footnotes
wasmtime supports it /WasmEdge is WIP /WAMR seems to be willing to support but not yet /wazero won't support it until it's standardized /WasmKit is WIP↩
wasmtime needs to be compiled from source or use
v30>=
in future? forhttps://github.com/bytecodealliance/wasmtime/pull/9952 that includes a fix in wasm-toolshttps://github.com/bytecodealliance/wasm-tools/pull/1968. Andwac
also needs to be compiled from source or0.6.2>=
forhttps://github.com/bytecodealliance/wac/pull/146↩