ext_wasm
The ext_wasm crate provides comprehensive WebAssembly support for Forge applications through the runtime:wasm module, enabling you to load and execute WASM modules with full WASI support.
Overview
ext_wasm integrates the Wasmtime WebAssembly runtime into Forge, providing:
- Module compilation - Compile WASM bytecode from bytes or files with AOT compilation
- Instance management - Create multiple independent instances from a single compiled module
- Function calls - Invoke exported WASM functions with automatic type conversion
- Linear memory access - Direct read/write access to WASM memory
- WASI support - Full WebAssembly System Interface with file system access
- Capability-based security - Controlled file system access via directory preopens
Quick Start
import * as wasm from "runtime:wasm";
// Compile WASM moduleconst wasmBytes = await Deno.readFile("module.wasm");const moduleId = await wasm.compile(wasmBytes);
// Create instanceconst instance = await wasm.instantiate(moduleId);
// Call exported functionconst [result] = await instance.call("add", 10, 32);console.log("Result:", result); // 42
// Cleanupawait instance.drop();await wasm.dropModule(moduleId);Module: runtime:wasm
Module Compilation
Compile WASM modules from bytes or files. Compiled modules are cached and can be reused for multiple instances.
import { compile, compileFile } from "runtime:wasm";
// Compile from bytesconst wasmBytes = await Deno.readFile("module.wasm");const moduleId = await compile(wasmBytes);
// Or compile directly from fileconst moduleId2 = await compileFile("./module.wasm");Performance Note: Compilation is expensive (~10-100ms). Cache the module ID and reuse it for multiple instances.
Instance Creation
Create instances from compiled modules. Each instance has independent state and memory.
import { instantiate } from "runtime:wasm";
// Basic instantiationconst instance = await instantiate(moduleId);
// With WASI configurationconst instance2 = await instantiate(moduleId, { preopens: { "/data": "./app-data" }, env: { "LOG_LEVEL": "debug" }, args: ["--verbose"], inheritStdout: true});Function Calls
Call exported WASM functions with automatic type conversion.
// Automatic type conversionconst [sum] = await instance.call("add", 10, 32);
// Multiple return valuesconst [quotient, remainder] = await instance.call("divmod", 42, 5);
// Explicit type controlimport { types } from "runtime:wasm";const [result] = await instance.call("process", types.i32(42), types.f64(3.14159));Memory Access
Read and write directly to WebAssembly linear memory.
// Write string to memoryconst text = "Hello, WASM!";const bytes = new TextEncoder().encode(text);await instance.memory.write(0, bytes);
// WASM processes the dataawait instance.call("process_string", 0, bytes.length);
// Read resultconst resultBytes = await instance.memory.read(1024, 256);const result = new TextDecoder().decode(resultBytes);
// Check memory size (in 64KB pages)const pages = await instance.memory.size();console.log(`Memory: ${pages * 64}KB`);
// Grow memory if neededif (pages < 16) { await instance.memory.grow(16 - pages);}WASI Configuration
WASI (WebAssembly System Interface) provides system-level access to WASM modules.
Directory Preopens
Map guest virtual paths to host directories for capability-based security.
const instance = await instantiate(moduleId, { preopens: { "/data": "./app-data", // Guest /data -> Host ./app-data "/config": "./config", // Guest /config -> Host ./config "/tmp": "./temp-storage" // Guest /tmp -> Host ./temp-storage }});
// WASM module can now access these directories// but CANNOT access other file system locationsSecurity: Only grant access to the minimum required directories. Never grant root access ("/": "/").
Environment Variables
Provide environment variables to WASM modules.
const instance = await instantiate(moduleId, { env: { "DATABASE_URL": "sqlite:///data/app.db", "API_KEY": "secret-key-here", "LOG_LEVEL": "debug" }});Command-Line Arguments
Pass arguments to WASM modules.
const instance = await instantiate(moduleId, { args: ["--verbose", "--port", "3000", "--workers", "4"]});Standard I/O
Inherit stdin/stdout/stderr from the host process.
const instance = await instantiate(moduleId, { inheritStdin: true, // WASM can read from host stdin inheritStdout: true, // WASM output goes to host stdout inheritStderr: true // WASM errors go to host stderr});
// Useful for WASM CLI tools that need interactive I/OMultiple Instances
Create multiple independent instances from a single compiled module.
import { compile, instantiate } from "runtime:wasm";
// Compile onceconst moduleId = await compile(wasmBytes);
// Create worker instancesconst workers = await Promise.all([ instantiate(moduleId, { env: { "WORKER_ID": "1" } }), instantiate(moduleId, { env: { "WORKER_ID": "2" } }), instantiate(moduleId, { env: { "WORKER_ID": "3" } })]);
// Process in parallelawait Promise.all(workers.map(worker => worker.call("process_batch", batchId)));
// Cleanupawait Promise.all(workers.map(w => w.drop()));await dropModule(moduleId);Each instance has:
- Independent linear memory
- Separate WASI state (file descriptors, environment)
- Isolated execution state
Type System
WebAssembly supports four numeric value types.
Value Types
| Type | Description | Range | JavaScript |
|---|---|---|---|
i32 | 32-bit integer | -2^31 to 2^31-1 | number |
i64 | 64-bit integer | -2^63 to 2^63-1 | bigint or number |
f32 | 32-bit float | IEEE 754 single | number |
f64 | 64-bit float | IEEE 754 double | number |
Automatic Type Conversion
Arguments are automatically converted based on type and value range:
// Integer in i32 range -> i32await instance.call("process", 42);
// Integer outside i32 range -> i64await instance.call("process", 9007199254740991);
// Float -> f64await instance.call("process", 3.14159);
// BigInt -> i64await instance.call("process", 9007199254740991n);Explicit Type Control
Use the types helper for precise type control:
import { types } from "runtime:wasm";
// Force i32 even for small valuesconst [result] = await instance.call("add_i32", types.i32(10), types.i32(32));
// Force i64 for large valuesconst [largeResult] = await instance.call("add_i64", types.i64(9007199254740991n), types.i64(1n));
// Specify float precisionconst [floatResult] = await instance.call("compute", types.f32(3.14159), // Single precision types.f64(2.71828) // Double precision);Export Introspection
Discover available exports before calling functions.
const exports = await instance.getExports();
// List all functionsconst functions = exports.filter(e => e.kind === "func");console.log("Available functions:", functions.map(f => f.name));
// Find memory exportconst memory = exports.find(e => e.kind === "memory");if (memory) { console.log("Memory export:", memory.name);}
// Check if function existsconst hasAdd = exports.some(e => e.kind === "func" && e.name === "add");if (hasAdd) { const [result] = await instance.call("add", 10, 32);}Export kinds:
"func"- Exported function"memory"- Exported linear memory"table"- Exported table"global"- Exported global variable
Error Handling
All operations return structured errors with machine-readable codes.
Error Codes
| Code | Error | Description |
|---|---|---|
| 5000 | CompileError | Failed to compile WASM module |
| 5001 | InstantiateError | Failed to instantiate module |
| 5002 | CallError | Function call failed |
| 5003 | ExportNotFound | Export not found in module |
| 5004 | InvalidModuleHandle | Invalid module handle |
| 5005 | InvalidInstanceHandle | Invalid instance handle |
| 5006 | MemoryError | Memory access out of bounds |
| 5007 | TypeError | Type mismatch in function call |
| 5008 | IoError | IO error (file loading) |
| 5009 | PermissionDenied | Permission denied by capability system |
| 5010 | WasiError | WASI configuration error |
| 5011 | FuelExhausted | Fuel exhaustion (execution limit) |
Error Handling Examples
// Compilation errorstry { const moduleId = await compile(invalidBytes);} catch (error) { // Error 5000: Invalid WASM bytecode console.error("Compilation failed:", error.message);}
// Function call errorstry { await instance.call("nonexistent");} catch (error) { // Error 5003: Export not found console.error("Function not found:", error.message);}
// Memory access errorstry { await instance.memory.read(999999999, 1024);} catch (error) { // Error 5006: Out of bounds console.error("Memory access failed:", error.message);}
// WASI permission errorstry { const instance = await instantiate(moduleId, { preopens: { "/sensitive": "/etc" } // May be denied });} catch (error) { // Error 5009 or 5010: Permission denied console.error("WASI configuration failed:", error.message);}Performance Tips
Compile Once, Instantiate Many
// ❌ BAD: Recompile for each instancefor (let i = 0; i < 10; i++) { const moduleId = await compile(wasmBytes); // Wasteful! const instance = await instantiate(moduleId);}
// ✅ GOOD: Compile once, reuseconst moduleId = await compile(wasmBytes);const instances = await Promise.all( Array(10).fill(null).map(() => instantiate(moduleId)));Batch Memory Operations
// ❌ BAD: Multiple small readsfor (let i = 0; i < 1000; i++) { const byte = await instance.memory.read(i, 1); process(byte);}
// ✅ GOOD: Single large readconst allBytes = await instance.memory.read(0, 1000);for (let i = 0; i < 1000; i++) { process(allBytes[i]);}Minimize Cross-Boundary Calls
// ❌ BAD: Many small function callsfor (let i = 0; i < 1000; i++) { await instance.call("process_item", i);}
// ✅ GOOD: Batch processing in WASMawait instance.call("process_batch", 0, 1000);Common Patterns
Data Processing Pipeline
import { compile, instantiate } from "runtime:wasm";
// Compile image processing moduleconst moduleId = await compile(await Deno.readFile("image_process.wasm"));const instance = await instantiate(moduleId);
// Load image into memoryconst imageData = await Deno.readFile("input.png");await instance.memory.write(0, imageData);
// Process in WASMawait instance.call("apply_filter", 0, imageData.length, 1024);
// Read resultconst processed = await instance.memory.read(1024, imageData.length);await Deno.writeFile("output.png", processed);
await instance.drop();Plugin System
import { compileFile, instantiate } from "runtime:wasm";
// Load user pluginsconst pluginFiles = await Array.fromAsync(Deno.readDir("./plugins"));const plugins = [];
for (const file of pluginFiles.filter(f => f.name.endsWith(".wasm"))) { const moduleId = await compileFile(`./plugins/${file.name}`); const instance = await instantiate(moduleId, { preopens: { "/data": "./plugin-data" } }); plugins.push({ name: file.name, instance });}
// Execute pluginsfor (const plugin of plugins) { const [result] = await plugin.instance.call("execute", taskId); console.log(`Plugin ${plugin.name} result:`, result);}Worker Pool
import { compile, instantiate } from "runtime:wasm";
class WasmWorkerPool { constructor(moduleId, size) { this.workers = []; this.available = [];
// Create worker instances for (let i = 0; i < size; i++) { const instance = await instantiate(moduleId); this.workers.push(instance); this.available.push(instance); } }
async execute(funcName, ...args) { // Wait for available worker while (this.available.length === 0) { await new Promise(r => setTimeout(r, 10)); }
const worker = this.available.pop(); try { return await worker.call(funcName, ...args); } finally { this.available.push(worker); } }
async cleanup() { await Promise.all(this.workers.map(w => w.drop())); }}
// Usageconst moduleId = await compile(wasmBytes);const pool = new WasmWorkerPool(moduleId, 4);
// Execute tasks in parallelconst results = await Promise.all([ pool.execute("process", 1), pool.execute("process", 2), pool.execute("process", 3)]);
await pool.cleanup();Common Pitfalls
1. Resource Cleanup Order
// ❌ ERROR: Drop module before instancesawait dropModule(moduleId);await instance.drop(); // Instance now invalid!
// ✅ CORRECT: Drop instances before moduleawait instance.drop();await dropModule(moduleId);2. Large Integer Precision
// ❌ JavaScript loses precision for large i64const [result] = await instance.call("process_i64", 9007199254740992);
// ✅ Use BigInt for values outside safe integer rangeconst [result] = await instance.call("process_i64", types.i64(9007199254740992n));3. Memory Growth Assumptions
// ❌ Assuming growth always succeedsawait instance.memory.grow(1000); // May fail!
// ✅ Handle growth failuretry { const oldSize = await instance.memory.grow(pagesNeeded); console.log(`Grew from ${oldSize} pages`);} catch (error) { console.error("Failed to grow memory:", error); // Use existing memory or fail gracefully}4. Unsafe Preopen Paths
// ❌ DANGEROUS: Granting root accessawait instantiate(moduleId, { preopens: { "/": "/" } // Full file system access!});
// ✅ SAFE: Minimal required accessawait instantiate(moduleId, { preopens: { "/data": "./app-data", "/tmp": "./temp-storage" }});Implementation Details
Architecture
TypeScript Application ↓ compile(bytes)WasmState (Rust) ├─ Wasmtime Engine (shared) ├─ Compiled Modules (HashMap) └─ Instances (HashMap) ├─ Store (per-instance) ├─ WASI Context └─ Linear MemoryState Management
WasmState: Thread-safe state wrapped inArc<Mutex<>>Engine: Shared Wasmtime engine for all modulesModule: Compiled WASM bytecode (cached)Instance: Runtime instance with independent stateStore: Per-instance execution contextWasiP1Ctx: WASI preview1 context with preopens
Wasmtime Integration
Forge uses Wasmtime 27.0 for WebAssembly execution:
- AOT compilation to native machine code
- WASI preview1 support via
wasmtime-wasi - Capability-based file system access
- Memory bounds checking
- Type validation
Testing
Run the ext_wasm test suite:
# All testscargo test -p ext_wasm
# With outputcargo test -p ext_wasm -- --nocapture
# Specific testcargo test -p ext_wasm test_compile_and_instantiate
# With debug loggingRUST_LOG=ext_wasm=debug cargo test -p ext_wasm -- --nocaptureTest coverage:
- Module compilation from bytes and files
- Instance creation with and without WASI
- Function calls with all value types (i32, i64, f32, f64)
- Multiple return values
- Linear memory operations (read, write, size, grow)
- Export introspection
- Error handling for all error codes
- WASI file system access via preopens
- Multiple instances from single module
- Resource cleanup (drop instance, drop module)
Related Documentation
- Wasmtime Documentation - Wasmtime runtime details
- WASI Specification - WASI standard
- WebAssembly Specification - Core WASM spec
- forge-weld - Code generation library
- ext_fs - File system operations
- ext_process - Process spawning
API Reference
For complete API documentation with all types and methods, see:
- runtime:wasm API Reference - Generated TypeScript API docs
Dependencies
| Dependency | Version | Purpose |
|---|---|---|
wasmtime | 27.0 | WebAssembly runtime and JIT compiler |
wasmtime-wasi | 27.0 | WASI preview1 implementation |
deno_core | 0.373 | Op definitions and runtime integration |
tokio | 1.x | Async runtime for mutex synchronization |
serde | 1.x | Serialization framework |
forge-weld-macro | 0.1 | TypeScript binding generation macros |
forge-weld | 0.1 | Build-time code generation |
Platform Support
| Platform | Support | Notes |
|---|---|---|
| macOS (x64) | ✅ Full | Native Wasmtime support |
| macOS (ARM) | ✅ Full | M1/M2/M3 optimized |
| Linux (x64) | ✅ Full | Native Wasmtime support |
| Linux (ARM) | ✅ Full | ARMv8 support |
| Windows (x64) | ✅ Full | Native Wasmtime support |
| Windows (ARM) | ⚠️ Limited | May have issues with some modules |