Skip to content

WebAssembly Runtime

EvidentSource uses WebAssembly (WASM) as its extensibility mechanism, allowing users to write custom business logic in any language that compiles to WebAssembly. This provides a secure, performant, and language-agnostic way to extend the platform.

  • Language Agnostic: Write components in Rust, Go, C++, AssemblyScript, or any WASM-targeting language
  • Sandboxed Execution: Components run in isolation with no access to host resources
  • Predictable Performance: Near-native execution speed with deterministic behavior
  • Portable: Same component works across all platforms and deployments
  • Version Compatible: Components remain compatible across EvidentSource versions
  1. State Views: Transform events into materialized views
  2. State Changes: Process commands and emit events
  3. Custom Validators: Implement domain-specific validation
  4. Data Transformations: Convert between formats
  5. Business Rules: Encode complex domain logic

EvidentSource uses the WebAssembly Component Model (WASI Preview 2):

graph TB
subgraph "EvidentSource Host"
A[WASM Runtime]
B[Component Registry]
C[Resource Manager]
D[Security Sandbox]
end
subgraph "WASM Component"
E[Exports]
F[Imports]
G[Linear Memory]
H[Component State]
end
A --> E
F --> A
A --> G
D --> G
B --> A
C --> A

EvidentSource uses Wasmtime, a production-ready WebAssembly runtime:

// Runtime configuration
let config = Config::new();
config.wasm_component_model(true);
config.async_support(true);
config.consume_fuel(true);
config.epoch_interruption(true);
let engine = Engine::new(&config)?;
let linker = Linker::new(&engine);
// Component instantiation
let component = Component::from_file(&engine, "state-view.wasm")?;
let instance = linker.instantiate(&mut store, &component)?;

State Views implement this WIT interface:

package evident:db@0.1.0;
interface state-view {
use types.{event, state};
// Core reduce function
reduce: func(
state: state,
event: event,
parameters: list<tuple<string, string>>
) -> result<state, string>;
// Optional: Initialize state
initialize: func(
parameters: list<tuple<string, string>>
) -> result<state, string>;
}

State Changes implement this interface:

package evident:db@0.1.0;
interface state-change {
use types.{event, state, command};
// Process command and emit events
process: func(
state: state,
command: command,
parameters: list<tuple<string, string>>
) -> result<list<event>, error>;
// Validate command without side effects
validate: func(
state: state,
command: command
) -> result<bool, string>;
}

Common types used across interfaces:

interface types {
// CloudEvent representation
record event {
id: string,
source: string,
spec-version: string,
type: string,
datacontenttype: option<string>,
dataschema: option<string>,
subject: option<string>,
time: option<string>,
data: option<list<u8>>,
extensions: list<tuple<string, string>>,
}
// State representation
record state {
content-type: string,
schema-id: option<string>,
bytes: list<u8>,
}
// Command representation
record command {
content-type: string,
schema-id: option<string>,
bytes: list<u8>,
}
// Error information
record error {
code: string,
message: string,
details: option<list<u8>>,
}
}

A State View that counts events by type:

use wit_bindgen::generate;
generate!({
world: "state-view",
exports: {
"evident:db/state-view": Component,
},
});
use exports::evident::db::state_view::{Event, State};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Serialize, Deserialize, Default)]
struct EventCounts {
by_type: HashMap<String, u64>,
total: u64,
}
struct Component;
impl exports::evident::db::state_view::Guest for Component {
fn reduce(
state: State,
event: Event,
_parameters: Vec<(String, String)>,
) -> Result<State, String> {
// Deserialize current state
let mut counts: EventCounts = if state.bytes.is_empty() {
EventCounts::default()
} else {
serde_json::from_slice(&state.bytes)
.map_err(|e| format!("Failed to parse state: {}", e))?
};
// Update counts
counts.total += 1;
*counts.by_type.entry(event.type_.clone()).or_insert(0) += 1;
// Serialize and return new state
let bytes = serde_json::to_vec(&counts)
.map_err(|e| format!("Failed to serialize: {}", e))?;
Ok(State {
content_type: "application/json".to_string(),
schema_id: None,
bytes,
})
}
fn initialize(
_parameters: Vec<(String, String)>,
) -> Result<State, String> {
let counts = EventCounts::default();
let bytes = serde_json::to_vec(&counts)
.map_err(|e| format!("Failed to serialize: {}", e))?;
Ok(State {
content_type: "application/json".to_string(),
schema_id: None,
bytes,
})
}
}

Component development requires the EvidentSource SDK (available in the customer portal). The SDK includes:

  • Component templates for various languages
  • Build tools and optimizers
  • Testing framework
  • Documentation and examples

For custom component development:

  1. Download the SDK from the customer portal
  2. Follow the included development guide
  3. Use provided build tools for optimization
  4. Test with the included test harness

Professional services are available for custom component development.

Components have isolated linear memory:

  • Initial Size: 1MB default
  • Maximum Size: 4GB limit
  • Growth: Automatic based on usage
  • Isolation: No shared memory between components
// Configure memory limits
let mut store = Store::new(&engine, ());
store.limiter(|_| &mut StoreLimits {
memory_size: 100 * 1024 * 1024, // 100MB max
table_elements: 10000,
instances: 10,
memories: 1,
});

Prevent runaway components:

// Fuel-based execution limits
store.add_fuel(1_000_000)?; // 1M fuel units
let remaining = store.consume_fuel(0)?;
// Time-based limits
store.set_epoch_deadline(1); // 1 epoch tick
engine.increment_epoch(); // In separate thread

Components can return errors:

fn reduce(state: State, event: Event) -> Result<State, String> {
if event.data.is_none() {
return Err("Event missing required data".to_string());
}
// Process event...
Ok(new_state)
}

Host handles errors gracefully:

match instance.call_reduce(&mut store, &state, &event) {
Ok(new_state) => {
// Update state
}
Err(e) => {
// Log error, maintain previous state
error!("Component error: {}", e);
}
}

Components have no capabilities by default:

  • ❌ No filesystem access
  • ❌ No network access
  • ❌ No system calls
  • ❌ No random number generation
  • ✅ Only computation on provided data

Multiple layers of isolation:

  1. Memory Isolation: Separate linear memory
  2. Stack Isolation: Independent call stacks
  3. Resource Limits: CPU and memory bounds
  4. Type Safety: Interface type checking
graph LR
subgraph "Trusted"
A[EvidentSource Core]
B[Storage Layer]
end
subgraph "Untrusted"
C[User Component]
D[Component Memory]
end
A -->|"Controlled API"| C
C -->|"Return Values Only"| A
C --> D

Near-native performance characteristics:

  • Startup: ~1ms component instantiation
  • Execution: 80-95% of native speed
  • Memory: Efficient linear memory model
  • Compilation: AOT compilation via Wasmtime
  1. Minimize Allocations: Reuse buffers
  2. Batch Operations: Process multiple items
  3. Efficient Serialization: Use binary formats
  4. Avoid Syscalls: Pure computation only

Example optimization:

// Inefficient: Allocates on every call
fn process(data: Vec<u8>) -> Vec<u8> {
let parsed = parse(&data); // Allocation
let result = transform(parsed); // Allocation
serialize(result) // Allocation
}
// Efficient: Reuse buffers
fn process(data: &[u8], output: &mut Vec<u8>) {
output.clear();
transform_in_place(data, output); // No allocations
}

Measure component performance:

#[cfg(test)]
mod benches {
use criterion::{criterion_group, Criterion};
fn bench_reduce(c: &mut Criterion) {
c.bench_function("reduce", |b| {
b.iter(|| {
component.reduce(state.clone(), event.clone())
});
});
}
criterion_group!(benches, bench_reduce);
}

Components can emit logs via imports:

interface logging {
log: func(level: log-level, message: string);
enum log-level {
error,
warn,
info,
debug,
trace,
}
}

Usage in component:

logging::log(LogLevel::Info, &format!("Processing event: {}", event.id));
  1. WABT: WebAssembly Binary Toolkit

    Terminal window
    wasm2wat component.wasm > component.wat
    wasm-validate component.wasm
  2. Wasmtime CLI: Testing and debugging

    Terminal window
    wasmtime run --invoke reduce component.wasm
  3. Component Inspector: Analyze components

    Terminal window
    wasm-tools component wit component.wasm
  1. Stateless Functions: Avoid global state
  2. Deterministic: Same input → same output
  3. Error Handling: Return meaningful errors
  4. Efficient Serialization: Choose appropriate formats
  5. Version Compatibility: Plan for schema evolution

Comprehensive test coverage:

#[test]
fn test_reduce_empty_state() {
let state = State::default();
let event = create_test_event();
let result = Component::reduce(state, event, vec![]).unwrap();
assert_eq!(result.content_type, "application/json");
let counts: EventCounts = serde_json::from_slice(&result.bytes).unwrap();
assert_eq!(counts.total, 1);
}
#[test]
fn test_reduce_error_handling() {
let invalid_state = State {
bytes: b"invalid json".to_vec(),
..Default::default()
};
let result = Component::reduce(invalid_state, event, vec![]);
assert!(result.is_err());
}
  1. Component Size: Keep under 10MB
  2. Memory Usage: Monitor and limit
  3. CPU Usage: Set appropriate fuel limits
  4. Error Rates: Monitor and alert
  5. Version Management: Track deployed versions
  1. Component Composition: Combine multiple components
  2. Async Support: Non-blocking I/O operations
  3. WASI Preview 3: Enhanced capabilities
  4. Component Registry: Discover and share components
  5. Hot Reloading: Update without downtime

Building a rich ecosystem:

  • Standard Library: Common utilities
  • Component Templates: Quick start examples
  • Testing Framework: Component test harness
  • Performance Profiler: Optimization tools
  • Component Marketplace: Share components