Building Wasm Components with Rust
This cookbook guide shows you how to build WebAssembly components using Rust that work with Wassette.
Quick Start
Prerequisites
- Rust toolchain (1.75.0 or later)
- WASI Preview 2 target
Install Tools
# Install Rust
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
source ~/.cargo/env
# Add WASI target
rustup target add wasm32-wasip2
# Install wit-bindgen (optional, for manual binding generation)
cargo install wit-bindgen-cli --version 0.37.0
Step-by-Step Guide
1. Create Your Project
cargo new --lib my-component
cd my-component
2. Configure Cargo.toml
[package]
name = "my-component"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib"]
[dependencies]
wit-bindgen = { version = "0.37.0", default-features = false }
[profile.release]
opt-level = "s"
lto = true
strip = true
3. Define Your Interface (WIT)
Create wit/world.wit
:
package local:my-component;
world calculator {
export add: func(a: s32, b: s32) -> s32;
export divide: func(a: f64, b: f64) -> result<f64, string>;
}
4. Generate Bindings
wit-bindgen rust wit/ --out-dir src/ --runtime-path wit_bindgen_rt --async none
5. Implement Your Component
Create/update src/lib.rs
:
#![allow(unused)] fn main() { // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. mod bindings; use bindings::exports::local::my_component::calculator::Guest; struct Component; impl Guest for Component { fn add(a: i32, b: i32) -> i32 { a + b } fn divide(a: f64, b: f64) -> Result<f64, String> { if b == 0.0 { Err("Division by zero".to_string()) } else { Ok(a / b) } } } bindings::export!(Component with_types_in bindings); }
6. Build Your Component
# Debug build
cargo build --target wasm32-wasip2
# Release build (recommended)
cargo build --target wasm32-wasip2 --release
# Output: target/wasm32-wasip2/release/my_component.wasm
7. Test Your Component
wassette serve --sse --plugin-dir target/wasm32-wasip2/release/
Complete Examples
Simple Calculator
wit/world.wit:
package local:calculator;
world calculator {
export add: func(a: s32, b: s32) -> s32;
export subtract: func(a: s32, b: s32) -> s32;
export multiply: func(a: s32, b: s32) -> s32;
export divide: func(a: s32, b: s32) -> result<s32, string>;
}
src/lib.rs:
#![allow(unused)] fn main() { mod bindings; use bindings::exports::local::calculator::calculator::Guest; struct Calculator; impl Guest for Calculator { fn add(a: i32, b: i32) -> i32 { a.saturating_add(b) } fn subtract(a: i32, b: i32) -> i32 { a.saturating_sub(b) } fn multiply(a: i32, b: i32) -> i32 { a.saturating_mul(b) } fn divide(a: i32, b: i32) -> Result<i32, String> { if b == 0 { Err("Division by zero".to_string()) } else { Ok(a / b) } } } bindings::export!(Calculator with_types_in bindings); }
HTTP Client
wit/world.wit:
package local:http-client;
world fetch {
import wasi:http/outgoing-handler@0.2.0;
export fetch-url: func(url: string) -> result<string, string>;
}
src/lib.rs:
#![allow(unused)] fn main() { mod bindings; use bindings::exports::local::http_client::fetch::Guest; use bindings::wasi::http::outgoing_handler; use bindings::wasi::http::types::{Method, Scheme}; struct Fetch; impl Guest for Fetch { fn fetch_url(url: String) -> Result<String, String> { // Parse URL and create request let request = outgoing_handler::OutgoingRequest::new( Method::Get, Some(&url), Scheme::Https, None, ); // Send request match outgoing_handler::handle(request, None) { Ok(response) => { // Read response body Ok("Response received".to_string()) } Err(e) => Err(format!("HTTP error: {:?}", e)), } } } bindings::export!(Fetch with_types_in bindings); }
File System Operations
wit/world.wit:
package local:filesystem;
world file-ops {
import wasi:filesystem/types@0.2.0;
export read-file: func(path: string) -> result<string, string>;
export write-file: func(path: string, content: string) -> result<_, string>;
}
src/lib.rs:
#![allow(unused)] fn main() { mod bindings; use bindings::exports::local::filesystem::file_ops::Guest; use std::fs; struct FileOps; impl Guest for FileOps { fn read_file(path: String) -> Result<String, String> { fs::read_to_string(&path) .map_err(|e| format!("Failed to read {}: {}", path, e)) } fn write_file(path: String, content: String) -> Result<(), String> { fs::write(&path, content) .map_err(|e| format!("Failed to write {}: {}", path, e)) } } bindings::export!(FileOps with_types_in bindings); }
Error Handling
Rust components use Result<T, E>
for error handling:
#![allow(unused)] fn main() { // Success Ok(value) // Error Err("Error message".to_string()) // Using the ? operator fn process_data(input: String) -> Result<String, String> { let parsed = parse_input(&input)?; let result = transform(parsed)?; Ok(result) } }
Build Automation with Justfile
Create Justfile
for easy building:
install-wasi-target:
rustup target add wasm32-wasip2
install-bindgen:
cargo install wit-bindgen-cli --version 0.37.0
generate-bindings: install-bindgen
wit-bindgen rust wit/ --out-dir src/ --runtime-path wit_bindgen_rt --async none
@COMPONENT_NAME=$(grep '^name = ' Cargo.toml | sed 's/name = "\(.*\)"/\1/' | tr '-' '_'); \
if [ -f "src/$${COMPONENT_NAME}.rs" ]; then mv "src/$${COMPONENT_NAME}.rs" src/bindings.rs; fi
build mode="debug": install-wasi-target generate-bindings
cargo build --target wasm32-wasip2 {{ if mode == "release" { "--release" } else { "" } }}
clean:
cargo clean
rm -f src/bindings.rs
test:
cargo test
all: build
Usage:
just build # Debug build
just build release # Release build
just clean # Clean build artifacts
Best Practices
- Use strong typing - Leverage Rust’s type system for safety
- Handle errors properly - Always use
Result<T, E>
for fallible operations - Optimize for size - Use
opt-level = "s"
and enable LTO in release builds - Avoid unwrap/panic - Return errors instead of panicking
- Use saturating operations - Prevent integer overflow with
saturating_add
, etc.
Common Patterns
String Processing
#![allow(unused)] fn main() { fn process_text(input: String) -> Result<String, String> { if input.is_empty() { return Err("Input cannot be empty".to_string()); } let processed = input.to_uppercase(); Ok(processed) } }
JSON Handling (with serde)
#![allow(unused)] fn main() { use serde::{Deserialize, Serialize}; #[derive(Serialize, Deserialize)] struct Data { name: String, value: i32, } fn parse_json(json: String) -> Result<String, String> { let data: Data = serde_json::from_str(&json) .map_err(|e| format!("JSON parse error: {}", e))?; // Process data let result = Data { name: data.name.to_uppercase(), value: data.value * 2, }; serde_json::to_string(&result) .map_err(|e| format!("JSON serialize error: {}", e)) } }
Stateful Components
#![allow(unused)] fn main() { use std::sync::Mutex; static STATE: Mutex<Vec<String>> = Mutex::new(Vec::new()); fn add_item(item: String) -> Result<(), String> { let mut state = STATE.lock() .map_err(|e| format!("Lock error: {}", e))?; state.push(item); Ok(()) } fn get_items() -> Result<Vec<String>, String> { let state = STATE.lock() .map_err(|e| format!("Lock error: {}", e))?; Ok(state.clone()) } }
Troubleshooting
Build Errors
- Ensure
wasm32-wasip2
target is installed - Check that WIT bindings are up to date
- Verify
wit-bindgen
version matches dependencies
Linker Errors
- Make sure
crate-type = ["cdylib"]
is set in Cargo.toml - Check that all imports are properly declared in WIT
Runtime Errors
- Review WASI permissions in policy configuration
- Check for panics or unwraps in your code
- Validate input/output types match WIT interface
Full Documentation
For complete details, including advanced topics and more examples, see the Rust Development Guide.
Working Examples
See these complete working examples in the repository:
- filesystem-rs - File system operations
- fetch-rs - HTTP client
Next Steps
- Review the complete Rust guide
- Check out working examples
- Learn about wit-bindgen
- Read the FAQ