Rust Language Guide
Rust has first-class support for building WebAssembly components. The wasm32-wasip2 compiler target ships with the standard Rust toolchain, and the wstd crate provides an async standard library purpose-built for WASI 0.2 components.
This guide covers the toolchain, the wstd library, adding custom WASI interfaces, and practical guidance for building Rust components on wasmCloud.
If you're looking for a quick walkthrough of creating, building, and running a Rust component, see the Developer Guide.
Toolchain overview
Build pipeline
Unlike TypeScript or Go, Rust compiles directly to a Wasm component in a single step. No post-processing, bundling, or external tools are required.
wasm32-wasip2 target
The wasm32-wasip2 target has been a Tier 2 target since Rust 1.82. It compiles Rust code to a WebAssembly component targeting WASI 0.2 (Preview 2), which includes the Component Model.
Install the target:
rustup target add wasm32-wasip2Build a component:
cargo build --target wasm32-wasip2 --releaseThe compiled component is written to target/wasm32-wasip2/release/<crate_name>.wasm.
wstd
wstd is a minimal async Rust standard library for Wasm components, maintained by the Bytecode Alliance. It provides high-level APIs for HTTP, networking, I/O, timers, and randomness — all backed by WASI 0.2 interfaces.
wstd handles WASI bindings internally through its dependency on the wasip2 crate. For standard HTTP components, wstd is the only dependency you need.
wstd is a transitional librarywstd exists because mainstream async runtimes like tokio and async-std do not yet support WASI 0.2. Once they do, the wstd project recommends migrating to those runtimes. The API is designed to make that migration straightforward.
Available modules:
| Module | Description |
|---|---|
wstd::http | HTTP request/response types, #[http_server] macro |
wstd::io | Async I/O abstractions |
wstd::net | Async networking (TcpListener, TcpStream) |
wstd::time | Async timers and durations |
wstd::rand | Random number generation (backed by wasi:random) |
wstd::task | Async task types |
wstd::runtime | Async event loop (block_on() executor) |
wit-bindgen
wit-bindgen is a lower-level tool that generates Rust code from WIT interface definitions. You don't need wit-bindgen for standard HTTP components — wstd handles those bindings internally. However, wit-bindgen is essential when you need to use WASI interfaces that aren't included in wstd, such as wasi:keyvalue or wasi:config.
See Adding custom WASI interfaces for details on using wstd and wit-bindgen together.
Handling HTTP requests
The #[wstd::http_server] macro is the standard way to build an HTTP component in Rust. It wires an async function to the wasi:http/incoming-handler export:
use wstd::http::{Body, Request, Response, StatusCode};
#[wstd::http_server]
async fn main(req: Request<Body>) -> Result<Response<Body>, wstd::http::Error> {
match req.uri().path() {
"/" => home().await,
_ => not_found().await,
}
}
async fn home() -> Result<Response<Body>, wstd::http::Error> {
Ok(Response::new(Body::from("Hello from wasmCloud!\n")))
}
async fn not_found() -> Result<Response<Body>, wstd::http::Error> {
Ok(Response::builder()
.status(StatusCode::NOT_FOUND)
.body(Body::from("Not found\n"))?)
}Key points:
- The function must be
asyncand accept aRequest<Body>, returningResult<Response<Body>, wstd::http::Error>. - Route matching is manual (pattern match on
req.uri().path()). For complex routing, you can use helper functions or a lightweight router crate. Response::new()creates a 200 OK response. UseResponse::builder()for custom status codes or headers.- Handler functions can be
async, enabling outgoing HTTP calls, key-value operations, or other async work within a request.
Outgoing HTTP requests
Components can make outgoing HTTP requests using wstd::http::Client:
use wstd::http::{Body, Client, Request, Response};
#[wstd::http_server]
async fn main(req: Request<Body>) -> Result<Response<Body>, wstd::http::Error> {
let client = Client::new();
let upstream_resp = client.send(
Request::get("https://api.example.com/data").body(Body::empty())?
).await?;
Ok(Response::new(upstream_resp.into_body()))
}To make outgoing HTTP requests, your WIT world must import wasi:http/outgoing-handler:
world hello {
import wasi:http/outgoing-handler@0.2.2;
export wasi:http/incoming-handler@0.2.2;
}Adding custom WASI interfaces
wstd (through its wasip2 dependency) provides bindings for standard WASI interfaces: wasi:http, wasi:io, wasi:clocks, wasi:random, and others. For draft or experimental interfaces like wasi:keyvalue, wasi:config, or custom interfaces, you need wit-bindgen to generate bindings for the additional interfaces.
How wstd and wit-bindgen coexist
wstdhandles the HTTP export via the#[wstd::http_server]macrowasip2(bundled withwstd) provides bindings for standard WASI importswit-bindgengenerates bindings for your custom imports only
Many draft interfaces like wasi:keyvalue are self-contained — they don't share types with wasi:io or other standard interfaces. This means there are no type conflicts between what wasip2 provides and what wit-bindgen generates.
Example: Adding wasi:keyvalue
1. Add wit-bindgen to Cargo.toml:
[package]
name = "my-component"
edition = "2024"
version = "0.1.0"
[lib]
crate-type = ["cdylib"]
[dependencies]
wstd = "0.6"
wit-bindgen = "0.41"2. Declare the imports in your WIT world:
package myorg:mycomponent;
world my-world {
import wasi:keyvalue/store@0.2.0-draft;
import wasi:keyvalue/atomics@0.2.0-draft;
export wasi:http/incoming-handler@0.2.2;
}3. Fetch the WIT dependencies:
wkg wit fetchThis populates wit/deps/ with the interface definitions for wasi:keyvalue, wasi:http, and their transitive dependencies.
4. Generate bindings and use the interface:
use wstd::http::{Body, Request, Response};
// Generate bindings for all interfaces in the WIT world.
// wstd handles the HTTP export; wit-bindgen generates the keyvalue imports.
wit_bindgen::generate!({
world: "my-world",
path: "wit",
generate_all,
});
use wasi::keyvalue::{atomics, store};
#[wstd::http_server]
async fn main(req: Request<Body>) -> Result<Response<Body>, wstd::http::Error> {
let bucket = store::open("")
.map_err(|e| wstd::http::Error::msg(format!("keyvalue error: {:?}", e)))?;
let count = atomics::increment(&bucket, "visitor-count", 1)
.map_err(|e| wstd::http::Error::msg(format!("increment error: {:?}", e)))?;
Ok(Response::new(Body::from(format!("Visit count: {count}\n"))))
}The generate_all option tells wit-bindgen to generate bindings for all imports in the world. Since wasi:keyvalue isn't in wasip2, wit-bindgen generates it. The HTTP export is still handled by wstd, not wit-bindgen.
Generated bindings are available under wasi::keyvalue::*, matching the WIT package namespace.
Version alignment
The wasi:http/incoming-handler version in your WIT world must match the version provided by your wasip2 dependency. Check your resolved version with:
cargo tree -p wasip2wasip2 version | WASI HTTP version |
|---|---|
| 1.0.0, 1.0.1 | 0.2.4 |
| 1.0.2+ | 0.2.9 |
If versions mismatch, you'll get a linker error: failed to find export of interface wasi:http/incoming-handler@... function handle.
To fix this, update the version in wit/world.wit and re-fetch dependencies:
rm -rf wit/deps wkg.lock && wkg wit fetchHandling type conflicts
If a custom interface imports types from wasi:io or other standard interfaces (creating overlap with wasip2), use wit-bindgen's with option to point to wasip2's types:
wit_bindgen::generate!({
world: "my-world",
path: "wit",
with: {
"wasi:io/streams@0.2.9": wasip2::wasi::io::streams,
"wasi:io/poll@0.2.9": wasip2::wasi::io::poll,
},
generate_all,
});Self-contained interfaces like wasi:keyvalue@0.2.0-draft don't share types with standard interfaces, so this isn't needed in most cases.
Project structure
A typical Rust component project:
my-component/
├── .wash/
│ └── config.yaml # wash project configuration
├── src/
│ └── lib.rs # Application code
├── wit/
│ ├── world.wit # WIT world definition
│ └── deps/ # Fetched WIT dependencies (gitignored)
├── Cargo.toml
├── Cargo.lock
└── wkg.lock # WIT dependency lock file
Cargo.toml
[package]
name = "my-component"
version = "0.1.0"
edition = "2024"
[lib]
crate-type = ["cdylib"]
[dependencies]
wstd = "0.6"crate-type = ["cdylib"]is required — it tells Cargo to produce a C-compatible dynamic library, which is the format used by thewasm32-wasip2target to emit a.wasmcomponent.- For a standard HTTP component,
wstdis the only dependency needed.
WIT world
package wasmcloud:hello;
world hello {
export wasi:http/incoming-handler@0.2.2;
}The WIT world declares your component's imports and exports. A component exporting wasi:http/incoming-handler can handle incoming HTTP requests. Adding imports (like wasi:http/outgoing-handler or wasi:keyvalue/store) declares the capabilities your component requires from the runtime.
The wasi:http/incoming-handler version in your WIT world must match the version provided by your wasip2 dependency. The version shown here (@0.2.2) matches the wasmCloud project template. See Version alignment for details on matching versions.
WIT dependency management with wkg
wash bundles the wkg WebAssembly package manager, so you don't need to install a separate tool. You can fetch WIT dependencies directly with wash:
wash wit fetchThis reads your WIT world, populates wit/deps/ with downloaded interface definitions, and creates a wkg.lock file. You should:
- Add
wit/deps/to.gitignore— these are fetched dependencies - Commit
wkg.lock— this ensures reproducible builds
When you run wash build, it calls wash wit fetch automatically, so you typically don't need to run it manually. If you have wkg installed separately, it works the same way — wash and wkg share the same underlying crate and produce the same wkg.lock file.
The following namespaces are resolved automatically without configuration:
wasi— standard WASI interfaceswasmcloud— wasmCloud project interfaceswrpc— wRPC interfacesba— Bytecode Alliance interfaces
.wash/config.yaml
Rust projects use a project configuration file at .wash/config.yaml that tells wash how to build the component:
build:
command: cargo build --target wasm32-wasip2 --release
component_path: target/wasm32-wasip2/release/hello_world.wasmThe command field specifies the build command and component_path points to the compiled .wasm binary. For a full reference of configuration options, see the Configuration page.
Optimizing binary size
Wasm component binaries can be reduced with standard Cargo release profile settings:
[profile.release]
opt-level = "z" # Optimize for size
lto = true # Link-time optimization
codegen-units = 1 # Better optimization (slower compile)
strip = true # Remove debug symbolsCrate compatibility
What works
Any Rust crate that compiles to wasm32-wasip2 works inside a Wasm component. This includes:
- Pure computation —
serde,serde_json,regex,uuid,base64,chrono(withoutstdtime), etc. - Error handling —
anyhow,thiserror - Async —
wstd's own async runtime (nottokioorasync-std— see below) - HTTP types —
wstd::httpprovidesRequest,Response,Body,StatusCode, etc.
What does not work
tokio,async-std,smol— mainstream async runtimes do not yet support WASI 0.2.wstdexists specifically to fill this gap. When these runtimes add WASI 0.2 support, migration fromwstdis expected to be straightforward.- Crates that use
std::netorstd::fsdirectly — these require OS-level syscalls not available in the Wasm sandbox. Usewstd::netand WASI filesystem interfaces instead. - Crates with native C dependencies — anything that links to a C library via
build.rs(e.g.,openssl-sys,libsqlite3-sys) will not compile forwasm32-wasip2. - Crates using threads —
std::threadis not available in WASI 0.2. Async concurrency throughwstdis the alternative.
Practical guidance
- Check a crate's compatibility by attempting
cargo check --target wasm32-wasip2before committing to it - Prefer crates with
no_stdsupport or explicit Wasm compatibility - For JSON handling,
wstdhas optionalserdeandserde_jsonfeature flags:wstd = { version = "0.6", features = ["serde", "serde_json"] }
Building and running
Create a new project
Use wash new to scaffold a Rust component project:
wash new https://github.com/wasmCloud/wash.git --name hello --subfolder examples/http-hello-worldDevelopment loop
Start a development loop that builds, runs, and watches for changes:
wash devSend a request to test:
curl localhost:8000Build a component
Compile your project to a .wasm binary using standard Cargo:
cargo build --target wasm32-wasip2 --releaseThe compiled component is written to target/wasm32-wasip2/release/<crate_name>.wasm.
If your WIT world references external interfaces (e.g. wasi:keyvalue), fetch the definitions first:
wkg wit fetchwash build as an alternativewash build wraps both steps — it runs wkg wit fetch and then cargo build using the command from your .wash/config.yaml. Either approach produces the same output. This guide shows cargo directly because it's the standard Rust build command and works without any wasmCloud-specific configuration.
Inspect a component
Verify your component's imports and exports with wasm-tools:
wasm-tools component wit target/wasm32-wasip2/release/my_component.wasmThis prints the resolved WIT world, showing exactly which interfaces the component imports and exports.
Further reading
- Developer Guide — quickstart tutorial for creating, building, and running a Rust component
- wasmCloud Rust examples — example projects in the
washrepository wstddocumentation — API reference for the async Wasm standard librarywstdrepository — source code and exampleswit-bindgendocumentation — API reference for the WIT bindings generatorwasm32-wasip2platform support — Rust target documentation- Component Model: Rust — Component Model documentation for Rust
- Language Support overview — summary of all supported languages and toolchains