webassembly-component-development

Installation
SKILL.md

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

  1. Language Interoperability: Use the best language for each task
  2. Code Reuse: Share components across projects
  3. Modularity: Clear boundaries and interfaces
  4. Independent Development: Teams can work on different components
  5. 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:http and wasi:sockets
  • UTF-8 guaranteed string encoding
  • Async via tokio (1.50+) or wstd; 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-wasip2 target
  • Handles WIT dependency fetching and binding generation
  • Respects .wash/config.yaml build 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 result types 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 rayon fail at runtime
  • Any code using std::thread::spawn fails

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:

  • OsStr from wasip2 is guaranteed to be valid UTF-8
  • Avoid as_encoded_bytes() - uses unspecified encoding
  • Conversion to str should 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 APIs
  • wasi: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 networking
  • tokio::time — timers and delays
  • tokio::sync — channels, mutexes, etc.
  • tokio::io — async I/O utilities
  • Single-threaded runtime (rt feature)

What does NOT work yet:

  • tokio::net::lookup_host — DNS lookups require multithreading workaround
  • tokio::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_unstable flag required
  • You need wstd::http client/server APIs (higher-level than raw TCP)
  • You want wstd::rand for 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:

  1. Runtime doesn't support WASI Preview 2
  2. Runtime doesn't implement that specific interface
  3. 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:wash interface not published (use wash build --skip-fetch for 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

  1. Check target compatibility:

    rustc --print target-list | grep wasi
    
  2. Fetch WIT dependencies:

    # WIT deps are fetched automatically by wash build
    # For manual management, use the wkg CLI
    wkg wit fetch
    
  3. Inspect component imports:

    wasm-tools component wit your-component.wasm
    
  4. 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.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

Development

  1. Use wash build when the project has .wash/config.yaml; otherwise use wasm-tools component new to produce a component
  2. Avoid Threading: Design for single-threaded execution
  3. Use UTF-8: Don't rely on platform-specific encodings
  4. Minimal Dependencies: Fewer dependencies = fewer WASI compatibility issues
  5. Test Early: Run components in target runtime during development
  6. Async options: Use tokio (1.50+, with --cfg tokio_unstable) or wstd for async in Wasm components

Component Design

  1. Single Responsibility: Each component should have one clear purpose
  2. Coarse-Grained Interfaces: Design for fewer, larger operations
  3. Version Your Interfaces: Use WIT package versioning
  4. Document Dependencies: Note which WASI interfaces your component needs

Runtime Compatibility

  1. Check Runtime Support: Verify WASI interfaces before using them
  2. Keep Tools Updated: Regularly update wash, wasmtime, and Rust
  3. Pin Versions in Production: Use exact versions for reproducibility
  4. 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 build for wasmCloud projects - handles compatibility automatically
  • Document runtime requirements and maintain compatibility matrices
Related skills
Installs
25
GitHub Stars
16
First Seen
Feb 12, 2026