Skip to main content

WASM Plugin Development

WebAssembly (WASM) plugins provide a secure, sandboxed execution environment for untrusted code. MoFA’s WASM runtime enables you to run plugins from any language that compiles to WASM with strict resource limits and memory isolation.

Why WASM Plugins?

Security

Complete sandboxing with no direct system access

Portability

Write in any language that targets WASM

Performance

Near-native execution speed

Resource Control

Strict memory and execution time limits

Architecture

MoFA’s WASM runtime architecture:
┌─────────────────────────────────────────────────────────────┐
│                      Host (MoFA)                            │
│  ┌────────────────────────────────────────────────────────┐   │
│  │              WasmRuntime                               │   │
│  │  ┌──────────┐  ┌──────────┐  ┌──────────┐     │   │
│  │  │  Engine   │  │   Linker  │  │   Store   │     │   │
│  │  └──────────┘  └──────────┘  └──────────┘     │   │
│  └────────────────────────────────────────────────────────┘   │
│                            │                                │
│  ┌────────────────────────────────────────────────────────┐   │
│  │              Host Functions                           │   │
│  │  ┌────────┐ ┌────────┐ ┌────────┐ ┌────────┐        │   │
│  │  │  log   │ │ get_   │ │ send_  │ │ call_  │        │   │
│  │  │        │ │ config │ │ message│ │ tool   │        │   │
│  │  └────────┘ └────────┘ └────────┘ └────────┘        │   │
│  └────────────────────────────────────────────────────────┘   │
│                            │                                │
│  ┌────────────────────────────────────────────────────────┐   │
│  │              WASM Sandbox                             │   │
│  │  ┌─────────────────────────────────────────────────┐     │   │
│  │  │            Plugin Instance                  │     │   │
│  │  │  ┌─────────┐  ┌─────────┐  ┌─────────┐     │     │   │
│  │  │  │ Memory  │  │ Exports │  │ Imports │     │     │   │
│  │  │  └─────────┘  └─────────┘  └─────────┘     │     │   │
│  │  └─────────────────────────────────────────────────┘     │   │
│  └────────────────────────────────────────────────────────┘   │
└─────────────────────────────────────────────────────────────┘

Creating a WASM Plugin

Let’s create a simple math plugin using WAT (WebAssembly Text format):
1

Write WASM Module

Create a WAT file with exported functions:
examples/wasm_plugin/src/main.rs
(module
    ;; Add two integers
    (func (export "add") (param i32 i32) (result i32)
        local.get 0
        local.get 1
        i32.add
    )

    ;; Subtract
    (func (export "sub") (param i32 i32) (result i32)
        local.get 0
        local.get 1
        i32.sub
    )

    ;; Multiply
    (func (export "mul") (param i32 i32) (result i32)
        local.get 0
        local.get 1
        i32.mul
    )

    ;; Factorial (recursive)
    (func $factorial (export "factorial") (param i32) (result i32)
        (if (result i32) (i32.le_s (local.get 0) (i32.const 1))
            (then (i32.const 1))
            (else
                (i32.mul
                    (local.get 0)
                    (call $factorial (i32.sub (local.get 0) (i32.const 1)))
                )
            )
        )
    )
)
2

Create Runtime

Initialize the WASM runtime:
examples/wasm_plugin/src/main.rs
use mofa_plugins::wasm_runtime::{
    WasmRuntime, RuntimeConfig, WasmPluginConfig
};

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
    // Create runtime with config
    let mut rt_config = RuntimeConfig::new();
    rt_config.execution_config.fuel_metering = false;
    rt_config.execution_config.epoch_interruption = false;

    let runtime = WasmRuntime::new(rt_config)?;

    Ok(())
}
3

Load Plugin

Compile and load the WASM module:
examples/wasm_plugin/src/main.rs
// Compile WAT to WASM
let wat = r#"
    (module
        (func (export "add") (param i32 i32) (result i32)
            local.get 0
            local.get 1
            i32.add
        )
    )
"#;

let compiled = runtime.compile_wat("math-module", wat).await?;
println!("Compiled module: {}", compiled.name);
println!("Size: {} bytes", compiled.size_bytes);
println!("Compile time: {}ms", compiled.compile_time_ms);

// Create plugin from compiled module
let mut config = WasmPluginConfig::new("math-plugin");
config.resource_limits.max_fuel = None;

let plugin = runtime.create_plugin(&compiled, config).await?;
4

Execute Functions

Call exported WASM functions:
examples/wasm_plugin/src/main.rs
// Initialize plugin
plugin.initialize().await?;

// Call 'add' function
let result = plugin.call_i32(
    "add",
    &[wasmtime::Val::I32(10), wasmtime::Val::I32(5)]
).await?;

println!("10 + 5 = {}", result);

// Call 'factorial' function
let result = plugin.call_i32(
    "factorial",
    &[wasmtime::Val::I32(5)]
).await?;

println!("5! = {}", result);

// Stop plugin
plugin.stop().await?;

Resource Limits

Configure strict resource limits for safety:
examples/wasm_plugin/src/main.rs
use mofa_plugins::wasm_runtime::{RuntimeConfig, ResourceLimits};

let rt_config = RuntimeConfig::new()
    .with_resource_limits(ResourceLimits {
        max_memory_pages: 16,          // 1MB max memory (64KB per page)
        max_table_elements: 1000,      // Max table size
        max_instances: 5,              // Max concurrent instances
        max_execution_time_ms: 1000,   // 1 second timeout
        max_fuel: None,                // Disable fuel metering
        max_call_depth: 100,           // Max recursion depth
    });

let runtime = WasmRuntime::new(rt_config)?;
Resource limits protect against malicious or poorly written plugins consuming excessive resources.

Plugin Manager

Manage multiple WASM plugins:
examples/wasm_plugin/src/main.rs
use mofa_plugins::wasm_runtime::WasmPluginManager;
use std::sync::Arc;

let runtime = Arc::new(WasmRuntime::new(rt_config)?);
let manager = WasmPluginManager::new(runtime);

// Subscribe to events
let mut event_rx = manager.subscribe();

// Load multiple plugins
let greet_wat = r#"
    (module
        (func (export "greet_len") (result i32)
            i32.const 13  ;; "Hello, World!".len()
        )
    )
"#;

let greet_handle = manager.load_wat(greet_wat, None).await?;
println!("Loaded plugin: {}", greet_handle.id());

// Initialize and use
manager.initialize(&greet_handle).await?;
let len = manager.call_i32(&greet_handle, "greet_len", &[]).await?;
println!("Greeting length: {}", len);

// List all plugins
let plugins = manager.list_plugins().await;
println!("Loaded plugins: {:?}",
    plugins.iter().map(|h| h.id()).collect::<Vec<_>>());

// Get plugin info
let info = manager.get_info(&greet_handle).await?;
println!("State: {:?}", info.state);
println!("Calls: {}", info.metrics.call_count);

// Get manager stats
let stats = manager.stats().await;
println!("Active plugins: {}", stats.active_plugins);
println!("Total calls: {}", stats.total_calls);

// Cleanup
manager.unload_all().await?;

Plugin with Memory

Use linear memory for data storage:
(module
    (memory (export "memory") 1)  ;; 1 page = 64KB

    (func (export "get_memory_size") (result i32)
        memory.size
    )

    (func (export "write_byte") (param i32 i32)
        local.get 0  ;; address
        local.get 1  ;; value
        i32.store8
    )

    (func (export "read_byte") (param i32) (result i32)
        local.get 0  ;; address
        i32.load8_u
    )
)
Use from Rust:
examples/wasm_plugin/src/main.rs
// Check memory size
let size = plugin.call_i32("get_memory_size", &[]).await?;
println!("Memory size: {} page(s) ({}KB)", size, size * 64);

// Write data
plugin.call_void(
    "write_byte",
    &[wasmtime::Val::I32(0), wasmtime::Val::I32(42)]
).await?;

// Read data
let value = plugin.call_i32("read_byte", &[wasmtime::Val::I32(0)]).await?;
println!("Read value: {}", value);

Plugin Capabilities

Control plugin permissions:
use mofa_plugins::wasm_runtime::PluginCapability;

let config = WasmPluginConfig::new("secure-plugin")
    .with_capability(PluginCapability::ReadConfig)
    .with_capability(PluginCapability::WriteState)
    .with_capability(PluginCapability::CallHostFunction);

// Without NetworkAccess capability, network calls will fail
let plugin = runtime.create_plugin_from_wat(wat, config).await?;

Plugin Metrics

Monitor plugin performance:
// Execute multiple times
for i in 1..=5 {
    plugin.call_i32("factorial", &[wasmtime::Val::I32(i)]).await?;
}

// Get metrics
let metrics = plugin.metrics().await;
println!("Total calls: {}", metrics.call_count);
println!("Success count: {}", metrics.success_count);
println!("Failed count: {}", metrics.failure_count);
println!("Avg execution time: {}ns", metrics.avg_execution_time_ns);
println!("Total execution time: {}ns", metrics.total_execution_time_ns);

Compiling Rust to WASM

Build plugins in Rust:
// lib.rs
#[no_mangle]
pub extern "C" fn add(a: i32, b: i32) -> i32 {
    a + b
}

#[no_mangle]
pub extern "C" fn process_data(ptr: *const u8, len: usize) -> i32 {
    let data = unsafe {
        std::slice::from_raw_parts(ptr, len)
    };

    // Process data
    data.len() as i32
}
Compile:
# Add WASM target
rustup target add wasm32-unknown-unknown

# Compile
cargo build --target wasm32-unknown-unknown --release

# Output: target/wasm32-unknown-unknown/release/my_plugin.wasm
Load in MoFA:
use std::fs;

let wasm_bytes = fs::read("my_plugin.wasm")?;
let compiled = runtime.compile("my_plugin", &wasm_bytes).await?;
let plugin = runtime.create_plugin(&compiled, config).await?;

Advanced Features

Global Variables

(module
    (global $counter (mut i32) (i32.const 0))

    (func (export "increment") (result i32)
        global.get $counter
        i32.const 1
        i32.add
        global.set $counter
        global.get $counter
    )

    (func (export "get") (result i32)
        global.get $counter
    )

    (func (export "reset")
        i32.const 0
        global.set $counter
    )
)

Tables and Indirect Calls

(module
    (table 2 funcref)
    (elem (i32.const 0) $add $mul)

    (func $add (param i32 i32) (result i32)
        local.get 0
        local.get 1
        i32.add
    )

    (func $mul (param i32 i32) (result i32)
        local.get 0
        local.get 1
        i32.mul
    )

    (func (export "call_indirect") (param i32 i32 i32) (result i32)
        local.get 1
        local.get 2
        local.get 0
        call_indirect (param i32 i32) (result i32)
    )
)

Module Linking

// Import host functions
let mut linker = runtime.create_linker();

linker.func_wrap(
    "env",
    "log",
    |msg: i32| {
        println!("Plugin log: {}", msg);
    }
)?;

let plugin = runtime.create_plugin_with_linker(
    &compiled,
    config,
    linker
).await?;

Security Best Practices

Prevent resource exhaustion with strict limits on memory, time, and call depth.
Only grant necessary capabilities to plugins.
Never trust data coming from WASM plugins.
Track plugin performance and detect anomalies.
Each plugin should run in its own instance for complete isolation.

Error Handling

use mofa_plugins::wasm_runtime::WasmError;

match plugin.call_i32("factorial", &[wasmtime::Val::I32(10)]).await {
    Ok(result) => println!("Result: {}", result),
    Err(WasmError::Timeout) => {
        eprintln!("Plugin execution timed out");
    }
    Err(WasmError::OutOfMemory) => {
        eprintln!("Plugin ran out of memory");
    }
    Err(WasmError::InvalidFunction(name)) => {
        eprintln!("Function not found: {}", name);
    }
    Err(e) => eprintln!("Error: {}", e),
}

Performance Tips

Pre-compile Modules

Compile modules once and reuse for multiple instances

Module Caching

Enable module caching to avoid repeated compilation

Disable Fuel Metering

For trusted code, disable fuel metering for better performance

Optimize WASM

Use wasm-opt to optimize WASM binaries before deployment

Next Steps

Plugin Overview

Learn about the full plugin system

Compile-time Plugins

Build native Rust plugins

Build docs developers (and LLMs) love