Developing Wasm component model in MoonBit with minimal output size
Wasm Componentβ
WebAssembly is a new low-level virtual instruction set standard for a sandbox model. It is low-level, meaning it is close to native speed. It is virtual, meaning it can be run on many runtimes, including the browsers or on the operating sytems with projects such as wasmtime or wamr. It is a sandbox model, meaning that it can not interact with outside world unless using FFI. The FFI, however, can only return numbers, making the usage through linear memory a more efficient way. Many programming languages can compile to it, including Java, JavaScript/TypeScript, Python, Rust and, of course, MoonBit.
So how can we combine the Wasm components that are implemented in different programming languages? Enter the Component Model, a proposal to unify the surface. With the Component Model, we define a high-level API, and components can be combined with other components as long as the interfaces match.
This blog will follow a step-by-step guide on writing a small HTTP server that prints "Hello World" with MoonBit and demonstrate how MoonBit achieves high compatibility and interoperability while significantly reducing output size.
How-toβ
We will write a small HTTP server that prints "Hello World" with MoonBit. The prerequisites are:
-
wit-bindgen with MoonBit (You will need to build it from source for now, so Rust toolchain is also needed)
-
and of course, the MoonBit toolchain.
Define WITβ
First, you need to define the interface with WIT
. See the manual for detailed explanations.
We specify our dependencies under wit/deps.toml
. Today we are just using wasi-http
version 0.2.0.
http = "https://github.com/WebAssembly/wasi-http/archive/v0.2.0.tar.gz"
You need to update the dependencies with wit-deps
, and you'll see all the dependencies in the wit/deps
folder.
Then we specify our "world", corresponding to the resulting Wasm, under wit/world.wit
:
package moonbit:example;
world server {
export wasi:http/incoming-handler@0.2.0;
}
A world may include other worlds, or import/export interfaces. Here we export the interface incoming-handler
of wasi:http
version 0.2.0, since as an HTTP server, exporting an incoming handler is needed so that the runtime can use it to treat the incoming requests and generate responses.
Generate codeβ
You need to generate the code with wit-bindgen. Currently, you need to install the project with:
cargo install wit-bindgen-cli --git https://github.com/peter-jerry-ye/wit-bindgen/ --branch moonbit
After having the wit-bindgen
command, simply execute it with the proper subcommand (moonbit
) and location of the WIT files (wit
). There are also arguments to specify whether the types should derive the Show
trait or the Eq
trait.
wit-bindgen moonbit wit --derive-show --derive-eq --out-dir .
And here's what you will get:
.
βββ ffi
β βββ moon.pkg.json
β βββ top.mbt
βββ gen
β βββ ffi.mbt
β βββ interface_exports_wasi_http_incoming_handler_export.mbt
β βββ moon.pkg.json
β βββ worlds_server_export.mbt
βββ interface
β βββ exports
β β βββ wasi
β β βββ http
β β βββ incomingHandler
β β βββ moon.pkg.json
β β βββ README.md
β β βββ top.mbt
β βββ imports
β βββ wasi
β βββ clocks
β β βββ monotonicClock
β β βββ moon.pkg.json
β β βββ README.md
β β βββ top.mbt
β βββ http
β β βββ types
β β βββ moon.pkg.json
β β βββ README.md
β β βββ top.mbt
β βββ io
β βββ error
β β βββ moon.pkg.json
β β βββ top.mbt
β βββ poll
β β βββ moon.pkg.json
β β βββ README.md
β β βββ top.mbt
β βββ streams
β βββ moon.pkg.json
β βββ README.md
β βββ top.mbt
βββ moon.mod.json
βββ wit // contents ignored here
βββ worlds
βββ server
βββ import.mbt
βββ moon.pkg.json
βββ top.mbt
The generated project has four folders:
-
ffi
andgen
are the generated files to help with the Wasm bindings that you can safely ignore. Thegen
directory contains the entrance of the project. -
interface
contains all the interfaces that are imported into the selected world. It is divided toimports
andexports
where theimports
provide all the imported functions and types, while theexports
is where you need to fill the stub functions that will be exported. -
worlds
contains the world. Similar tointerface
, it contains aimport.mbt
, providing the imported functions and types of the world level, andtop.mbt
, containing the stub export functions.
Then you can develop as a normal MoonBit application, and moon check --target wasm
should finish successfully. To see what APIs you have and the documentation of the types or functions, you can run moon doc --serve
to see the documentation. Don't forget to execute moon fmt
to make the code look better.
Developβ
Here's our code for a minimum Hello-World server for demonstration:
pub fn handle(
request : @types.IncomingRequest,
response_out : @types.ResponseOutparam
) -> Unit {
let response = match request.path_with_query() {
None | Some("/") => make_response(b"Hello, World")
_ => make_response(b"Not Found", status_code=404)
}
|> Ok
response_out.set(response)
}
fn make_response(
body : Bytes,
~status_code : UInt = 200
) -> @types.OutgoingResponse {
...
}
See the full example on Github.
Componentizeβ
What we have achieved is a core Wasm, namely a Wasm following the WebAssembly standard. However, we need to turn it into a component so that it can be shared along with necessary information.
You need to use wasm-tools to embed the core Wasm into a component. First, we embed the WIT information into the custom section of our core Wasm. You need to specify the encoding as UTF-16 at this step. Then we turn the core Wasm into a component Wasm.
moon build --target wasm
wasm-tools component embed wit target/wasm/release/build/gen/gen.wasm -o target/wasm/release/build/gen/gen.wasm --encoding utf16
wasm-tools component new target/wasm/release/build/gen/gen.wasm -o target/wasm/release/build/gen/gen.wasm
You may also use JCO for this step if you prefer using npm
or pnpm
.
Useβ
With the Wasm we created, you can serve it with Wasmtime:
wasmtime serve target/wasm/release/build/gen/gen.wasm
You may also use JCO to serve it on Node.js or Deno, or serve it with WasmCloud or Spin.
Compareβ
We implement a simple HTTP server that simply replies "Hello World". Comparing with the http-hello-world
provided by WasmCloud, we have the following sizes:
Language | Output size |
---|---|
Python | 17M |
TypeScript | 8.7M |
Rust | 100K |
MoonBit | 27K |
Conclusionβ
We've demonstrated how to create a Wasm following the Component Model standard with MoonBit. The Component Model provides a new standard for creating interoperable Wasm. By pulling the WIT files from Spin, for example, we can easily build a serverless AI application in 5 minutes.
By supporting the WebAssembly component model, MoonBit enhances its application scenarios in microservice architectures and cloud-native applications with high compilation performance and compact code size, allowing for quick deployment and execution in various environments.
On August 18, MoonBit will reach the beta preview version, indicating language stability for broader testing and real-world use. In the future, we will continue to expand the MoonBit ecosystem and optimize documentation and toolchain for a better user experience. Stay tuned!
Additional resources:
- Get started with MoonBit.
- Check out the MoonBit Docs.
- Learn MoonBit with the open course.
- Join our Discord community.
- Explore MoonBit programming projects in the MoonBit Gallery.