webassembly-component-development
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-fetch
wash 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 --release
Composition 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.wasm
wasmCloud 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 build
This 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 build
What 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.0
Root 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 interface
Runtime-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-reload
Version Alignment Strategies
Strategy 1: Match Runtime Version
[dependencies]
wasi = "=0.14.0" # Exact version matching runtime
Strategy 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 interface
4. 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
- Review dependencies for
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.wasm
Part 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 tests
Common 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
More from cosmonic-labs/skills
wash
Expert in wasmCloud Shell (wash) CLI tool for building, running, and managing WebAssembly components and wasmCloud applications. Use this skill when working with wasmCloud, WebAssembly components, WIT (WebAssembly Interface Types) definitions, or wash commands.
6helm-best-practices
Expert guidance for authoring and maintaining Helm charts following standardized conventions, global registry support, templating best practices, and Kubernetes deployment patterns.
6rust-development
Expert Rust development guidance covering naming conventions, type safety, error handling, documentation, and best practices based on official Rust guidelines.
5helm-charts
Expert guidance for authoring and maintaining Helm charts following standardized conventions, global registry support, templating best practices, and Kubernetes deployment patterns.
2brand-guidelines
Brand guidelines for Cosmonic and wasmCloud. Use this skill when creating visual content, presentations, websites, diagrams, or any materials that need to follow Cosmonic or wasmCloud brand standards. Includes an Excalidraw library for diagrams.
2