WebAssembly Component Development
This skill provides comprehensive expertise in developing WebAssembly components, including WASI fundamentals, composition patterns, runtime compatibility, and troubleshooting.
Part 1: Core Concepts
What is the Component Model?
The WebAssembly Component Model enables:
- Combining multiple components into a single application
- Using libraries written in one language from another language
- Building modular, reusable WebAssembly modules
- Creating component graphs with defined dependencies
Benefits
- Language Interoperability: Use the best language for each task
- Code Reuse: Share components across projects
- Modularity: Clear boundaries and interfaces
- Independent Development: Teams can work on different components
- Type Safety: WIT ensures type-safe composition
WASI (wasip2)
The current WASI standard targets wasm32-wasip2 (Rust 1.82+ tier-2 target):
- Full Component Model support
- Native networking via
wasi:httpandwasi:sockets - UTF-8 guaranteed string encoding
- Async via
tokio(1.50+) orwstd; WASIp3 (0.3) adds native async (experimental)
Part 2: Component Composition
Composition Patterns
1. Simple Dependency
One component depends on another component's interface.
// component-a/wit/world.wit
package example:component-a;
interface math {
add: func(a: s32, b: s32) -> s32;
}
world component-a {
export math;
}// component-b/wit/world.wit
package example:component-b;
world component-b {
import example:component-a/math;
export calculate: func(x: s32, y: s32) -> s32;
}2. Adapter Pattern
Adapting one interface to another for compatibility.
world adapter {
import old-api/interface;
export new-api/interface;
}3. Middleware Pattern
Components that intercept and process data between other components.
world http-logger {
import wasi:http/incoming-handler;
export wasi:http/outgoing-handler;
// Logs all HTTP requests/responses
}4. Facade Pattern
Single component providing simplified access to multiple components.
world application-facade {
import database/client;
import cache/client;
import auth/service;
export api/handler;
}Designing Component Boundaries
Good Boundaries:
- Clear, well-defined responsibilities
- Minimal coupling between components
- Coarse-grained interfaces (fewer, larger operations)
- Domain-aligned (matches business/technical domains)
// Good: Clear, focused interface
interface user-service {
create-user: func(name: string, email: string) -> result<user-id, error>;
get-user: func(id: user-id) -> result<user, error>;
update-user: func(id: user-id, updates: user-updates) -> result<_, error>;
}Poor Boundaries (Anti-patterns):
- Too fine-grained (many small operations)
- Tight coupling (components know too much about each other)
- Chatty interfaces (many back-and-forth calls)
// Bad: Too fine-grained, chatty interface
interface user-service {
set-user-name: func(id: user-id, name: string);
get-user-name: func(id: user-id) -> string;
set-user-email: func(id: user-id, email: string);
get-user-email: func(id: user-id) -> string;
}Building Components
If the project has a .wash/config.yaml, use wash to build and develop:
wash build # Build a valid WebAssembly component
wash dev # Development with hot-reload
wash build --skip-fetchwash build automatically:
- Compiles to the correct
wasm32-wasip2target - Handles WIT dependency fetching and binding generation
- Respects
.wash/config.yamlbuild configuration
For non-wash projects, you can target wasip2
cargo build --target wasm32-wasip2 --releaseComposition Tools
wac (WebAssembly Composition)
A modern composition tool using a declarative .wac language:
# Install wac
cargo install wac-cli
# Quick plug: wire one component's exports into another's imports
wac plug component-a.wasm --plug component-b.wasm -o composed.wasm
# Or use a .wac composition file for complex graphs
wac compose composition.wac -o composed.wasmwasmCloud Composition
wasmCloud handles runtime linking automatically via wRPC over NATS. Components are wired together using link definitions and deployed via wash.
Build Configuration with.wash/config.yaml
For projects using wash, the .wash/config.yaml file in the project root configures custom build commands:
# .wash/config.yaml
dev:
command: make build
build:
command: make bindgen buildThis is useful for non-Rust projects (Go, TinyGo, etc.) or projects with custom build steps, controlling how wash build and wash dev invoke the build.
Part 3: Language Interoperability
Cross-Language Composition
Example: Python library used from Rust
// python-ml-lib/wit/world.wit
package ml:inference;
interface model {
predict: func(features: list<float32>) -> list<float32>;
}
world ml-component {
export model;
}// Rust component using Python ML library
wit_bindgen::generate!({
world: "app",
path: "wit",
});
impl Guest for MyApp {
fn process(data: Vec<f32>) -> Vec<f32> {
ml::inference::model::predict(&data)
}
}Supported Languages
- Rust - First-class support via
wash build - Python - Via
componentize-py - JavaScript/TypeScript - Via
componentize-js(experimental, uses SpiderMonkey) - Go - Via TinyGo 0.33+ with native WASI P2 support, plus Go Component SDK
- C/C++ - Via WASI SDK
- C# - Experimental support
Language Compatibility Considerations
Data Types:
- WIT provides common types that map to each language
- Complex types (records, variants, lists) are automatically converted
- Strings are always UTF-8
Performance:
- Cross-language calls have overhead (serialization/deserialization)
- Keep interfaces coarse-grained to minimize calls
- Pass handles/resources instead of large data when possible
Error Handling:
- Use WIT
resulttypes for cross-language error propagation - Each language maps
result<T, E>to its native error handling
Part 4: WASI Gotchas
1. No Threading Support
Problem: Thread pool operations are unsupported on wasm32-wasip2.
Symptoms:
- Panic with "operation not supported on this platform"
- Libraries like
rayonfail at runtime - Any code using
std::thread::spawnfails
Solution:
- Avoid libraries that require threading (check dependency trees)
- Use single-threaded algorithms and data structures
- Consider async/await patterns instead of threads
2. String/Encoding Issues
Problem: Unsafe assumptions about string encoding across platforms.
Key Facts:
OsStrfrom wasip2 is guaranteed to be valid UTF-8- Avoid
as_encoded_bytes()- uses unspecified encoding - Conversion to
strshould almost never fail on wasip2
// Good: Safe UTF-8 handling
let path_str = path.to_str().expect("wasip2 guarantees UTF-8");
// Bad: Platform-specific encoding
let bytes = path.as_os_str().as_encoded_bytes(); // Don't do this!3. Network Capabilities
Available WASI networking interfaces:
wasi:http- HTTP client and server APIswasi:sockets- Low-level socket APIs
Part 5: Async in WebAssembly Components
Tokio on wasip2
As of tokio 1.50+, tokio supports wasm32-wasip2 networking. This means you can use tokio's async runtime and networking APIs in WebAssembly components.
Requirements:
- tokio 1.50+
- Build with
RUSTFLAGS="--cfg tokio_unstable"(required until stabilized)
# Cargo.toml
[dependencies]
tokio = { version = "1.50", features = ["rt", "net", "macros", "time", "io-util", "sync"] }# Build with wash (tokio_unstable flag required)
RUSTFLAGS="--cfg tokio_unstable" wash buildWhat works on wasip2:
tokio::net::TcpListener,TcpStream— full async TCP networkingtokio::time— timers and delaystokio::sync— channels, mutexes, etc.tokio::io— async I/O utilities- Single-threaded runtime (
rtfeature)
What does NOT work yet:
tokio::net::lookup_host— DNS lookups require multithreading workaroundtokio::fs— async file I/O (may work with future WASIp3 threading)- Multi-threaded runtime (
rt-multi-thread) — WASI has no threading support yet tokio::process— no process spawning in WASI
#[tokio::main(flavor = "current_thread")]
async fn main() {
let listener = tokio::net::TcpListener::bind("0.0.0.0:8080").await.unwrap();
loop {
let (stream, _addr) = listener.accept().await.unwrap();
tokio::spawn(async move {
handle_connection(stream).await;
});
}
}wstd
wstd is a lightweight, WebAssembly-native async standard library built specifically for WASI 0.2. It provides a minimal async runtime, HTTP, networking, and other APIs purpose-built for the Wasm component model.
# Cargo.toml
[dependencies]
wstd = "0.6"When to use wstd over tokio:
- You want a minimal, Wasm-native dependency with no
tokio_unstableflag required - You need
wstd::httpclient/server APIs (higher-level than raw TCP) - You want
wstd::randfor random number generation in Wasm - You prefer a purpose-built Wasm library over a general-purpose runtime
| Module | Purpose | Example Types |
|---|---|---|
wstd::http | HTTP client and server | Client, Server |
wstd::io | Async I/O abstractions | Read, Write traits |
wstd::net | Async TCP networking | TcpListener |
wstd::time | Async time interfaces | Duration, Instant |
wstd::rand | Random number generation | thread_rng() |
wstd::runtime | Async event loop | Task spawning |
#[wstd::main]
async fn main() {
let client = wstd::http::Client::new();
let response = client.get("https://example.com").await.unwrap();
println!("Status: {}", response.status());
}#[wstd::http_server]
async fn handle(request: wstd::http::IncomingRequest) -> wstd::http::Response {
wstd::http::Response::new(200, "Hello, World!")
}Note: wstd defines its own I/O traits separate from tokio — do not mix wstd and tokio in the same component.
Part 6: Runtime Compatibility
Unknown Import Errors
The Most Common Error:
Error: instantiation failed: unknown import
component name: wasi:cli/environment@0.2.0Root Causes:
- Runtime doesn't support WASI Preview 2
- Runtime doesn't implement that specific interface
- Version mismatch between component and runtime
Diagnosis:
# Check what your component imports
wasm-tools component wit your-component.wasm
# Try running and see what fails
wasmtime run component.wasm 2>&1 | grep "unknown import"Solutions:
# Solution 1: Update runtime
cargo install wasmtime-cli --version 18.0.0
# Solution 2: Re-fetch WIT dependencies
wkg wit fetch
# Solution 3: Remove dependency on unsupported interfaceRuntime-Specific Compatibility
wasmtime
- Full WASI 0.2 support (stable)
- WASI 0.3 (WASIp3) support is experimental — adds native async
wasmtime run component.wasm
wasmtime serve component.wasm # HTTP components
wasmtime serve -S cli component.wasm # Enable wasi:cli (env vars, filesystem, sockets, clocks)
wasmtime serve -S cli,inherit-network component.wasm # Full host network access within the guest
wasmtime serve -S cli,allow-ip-name-lookup component.wasm # Enable wasi:sockets/ip-name-lookup
wasmtime serve -S p3 component.wasm # Enable WASIp3 (experimental)wasmCloud
- Full WASI 0.2 support in recent versions
- Custom wasmCloud interfaces (wasmcloud:*)
wasmcloud:washinterface not published (usewash build --skip-fetchfor plugins)
wash dev # Development testing with hot-reloadVersion Alignment Strategies
Strategy 1: Match Runtime Version
[dependencies]
wasi = "=0.14.0" # Exact version matching runtimeStrategy 2: Use Conservative Interfaces
- ✅
wasi:cli/environment - ✅
wasi:cli/stdin/stdout/stderr - ✅
wasi:filesystem(basic operations) - ⚠️
wasi:http(check runtime support) - ⚠️
wasi:sockets(not universally available)
Strategy 3: Feature Detection
#[cfg(feature = "wasi-http")]
mod http_impl { /* HTTP-specific code */ }
#[cfg(not(feature = "wasi-http"))]
mod http_impl { /* Fallback implementation */ }Part 7: Testing Strategies
1. Lightweight Host Harness
use wasmtime::{Engine, Store, component::Component};
#[cfg(test)]
mod tests {
#[test]
fn test_component() -> Result<()> {
let engine = Engine::default();
let mut store = Store::new(&engine, ());
let component = Component::from_file(&engine, "component.wasm")?;
// Instantiate and test...
Ok(())
}
}2. Mock Components
wit_bindgen::generate!({
world: "mock-database",
exports: {
"database:client/query": MockDatabase,
}
});
struct MockDatabase;
impl database::client::Query for MockDatabase {
fn execute(query: String) -> Result<Vec<Row>, Error> {
Ok(vec![/* test rows */])
}
}3. Integration Tests with wash
wash dev # Start development environment
curl http://localhost:8080/api/test # Test the component's HTTP interface4. Feature Flags for Host-Specific Code
#[cfg(target_family = "wasm")]
mod wasm_impl {
pub fn get_data() -> Vec<u8> {
wasi::filesystem::read("/data/file.bin")
}
}
#[cfg(not(target_family = "wasm"))]
mod native_impl {
pub fn get_data() -> Vec<u8> {
vec![1, 2, 3, 4] // Test data
}
}5. Environment Setup
use wasmtime_wasi::{WasiCtxBuilder, Dir};
#[test]
fn test_with_filesystem() -> Result<()> {
let test_dir = tempfile::tempdir()?;
std::fs::write(test_dir.path().join("test.txt"), "test data")?;
let wasi = WasiCtxBuilder::new()
.env("TEST_MODE", "true")?
.preopened_dir(
Dir::open_ambient_dir(test_dir.path(), ambient_authority())?,
"/data",
)?
.build();
// Test component...
Ok(())
}Part 8: Debugging
Debugging Workflow
- Check target compatibility:
rustc --print target-list | grep wasi - Fetch WIT dependencies:
# WIT deps are fetched automatically by wash build # For manual management, use the wkg CLI wkg wit fetch - Inspect component imports:
wasm-tools component wit your-component.wasm - Check for threading issues:
- Review dependencies for rayon, tokio (threaded), std::thread
Common Error Patterns
| Error | Cause | Solution |
|---|---|---|
| "operation not supported on this platform" | Threading issue | Remove threaded dependencies |
| "unknown import: wasi:cli/..." | Runtime doesn't support interface | Update runtime or remove dependency |
| WIT version mismatch | Version conflict | Re-fetch deps with wkg wit fetch or rebuild with wash build |
| Component instantiation failed | Adapter/runtime incompatibility | Check adapter version |
Tools
# Inspect component structure
wasm-tools print component.wasm
# Extract WIT interfaces
wasm-tools component wit component.wasm
# Validate component
wasm-tools validate component.wasm
# Check component info (wasmCloud)
wash inspect component.wasmPart 9: Performance Optimization
Minimize Cross-Component Calls
// Bad: Multiple fine-grained calls
let name = user_service.get_name(id)?;
let email = user_service.get_email(id)?;
// Good: Single coarse-grained call
let user = user_service.get_user(id)?;Use Streaming for Large Data
interface data-processor {
resource stream {
read-chunk: func() -> option<list<u8>>;
}
process-stream: func(input: stream) -> result<_, error>;
}Batch Operations
// Bad: Many individual calls
for item in items {
database.insert(item)?;
}
// Good: Single batch operation
database.insert_batch(items)?;Part 10: Best Practices
Documentation References
- wasmCloud docs: Always use https://wasmcloud.com/docs/ — do NOT use v1 docs (
/docs/v1/), which are outdated - wstd docs: https://docs.rs/wstd/latest/wstd/
- Component Model: https://component-model.bytecodealliance.org/
Development
- Use
wash buildwhen the project has.wash/config.yaml; otherwise usewasm-tools component newto produce a component - Avoid Threading: Design for single-threaded execution
- Use UTF-8: Don't rely on platform-specific encodings
- Minimal Dependencies: Fewer dependencies = fewer WASI compatibility issues
- Test Early: Run components in target runtime during development
- Async options: Use
tokio(1.50+, with--cfg tokio_unstable) orwstdfor async in Wasm components
Component Design
- Single Responsibility: Each component should have one clear purpose
- Coarse-Grained Interfaces: Design for fewer, larger operations
- Version Your Interfaces: Use WIT package versioning
- Document Dependencies: Note which WASI interfaces your component needs
Runtime Compatibility
- Check Runtime Support: Verify WASI interfaces before using them
- Keep Tools Updated: Regularly update
wash,wasmtime, and Rust - Pin Versions in Production: Use exact versions for reproducibility
- Maintain Compatibility Matrix: Document supported runtimes
CI/CD
# .github/workflows/test.yml
- name: Test with wasmtime
run: wasmtime run component.wasm
- name: Test with wasmCloud
run: |
wash build
wash dev &
# Run integration testsCommon Architecture Patterns
Microservices
world api-gateway {
import user:service/handler;
import order:service/handler;
import payment:service/handler;
export http:handler/incoming;
}Plugin Architecture
world app-core {
export plugin:host/register;
export plugin:host/execute;
}
world plugin {
import plugin:host/core-api;
export plugin:interface/handler;
}Data Pipeline
world data-pipeline {
import source:reader/stream;
import transform:processor/apply;
import sink:writer/write;
export pipeline:orchestrator/run;
}Summary
- Component Model enables language-agnostic, composable WebAssembly applications
- WASI 0.2 (wasip2) is the current standard with full networking and UTF-8 guarantees
- Design coarse-grained interfaces for better performance
- Test in target runtime early and often
- Use
wash buildfor wasmCloud projects - handles compatibility automatically - Document runtime requirements and maintain compatibility matrices