Skip to content

Rust Guide

Build Noorle plugins with Rust.

Why Rust?

Strengths

  • Zero-Cost Abstractions - High-level code compiles to efficient WASM without runtime overhead
  • Memory Safety - Prevent buffer overflows and memory bugs at compile time
  • Small Binaries - Excellent WASM optimization and dead code elimination
  • Mature Toolchain - First-class wasm32-wasip2 target with robust tooling
  • Type Safety - Strong typing prevents runtime errors

Best For

  • High-performance plugins
  • Security-critical applications
  • Resource-constrained environments
  • Complex data processing
  • Systems integration

Trade-offs

  • Longer compilation times
  • Steeper learning curve for beginners
  • More verbose than dynamic languages

Quick Start

Prerequisites

bash
# Install Noorle CLI
curl -L cli.noorle.dev | sh

Create Plugin

bash
# Initialize from template
noorle plugin init my-plugin --template rust
cd my-plugin

# Install dependencies (Rust toolchain, wasm32-wasip2 target, wasm tools)
noorle plugin prepare

# Build and deploy
noorle plugin publish

Minimal Example

rust
wit_bindgen::generate!({
    world: "my-plugin-component",
    path: "./wit",
});

struct MyPlugin;

impl Guest for MyPlugin {
    fn process(input: String) -> Result<String, String> {
        Ok(format!("Processed: {}", input))
    }
}

export!(MyPlugin);

Project Structure

tree
my-plugin/
├── src/lib.rs            # Main implementation
├── wit/world.wit         # API definition
├── Cargo.toml            # Rust dependencies
├── build.sh              # Build script
├── prepare.sh            # Dependency installer
├── noorle.yaml           # Plugin config (optional)
└── dist/                 # Build output
    ├── plugin.wasm       # Compiled component
    └── *.npack           # Deployment archive

File Descriptions

FilePurpose
src/lib.rsPlugin implementation with exported functions
wit/world.witWebAssembly interface definition
Cargo.tomlDependencies and package configuration
build.shCompilation and component creation
prepare.shInstalls Rust toolchain and wasm tools

Implementation Guide

Basic Plugin Structure

rust
// Generate bindings from WIT
wit_bindgen::generate!({
    world: "my-plugin-component",
    path: "./wit",
});

// Plugin struct
struct MyPluginComponent;

// Implement the Guest trait
impl Guest for MyPluginComponent {
    fn say_hi(name: String) -> String {
        format!("Hello, {}!", name)
    }

    fn add(a: f64, b: f64) -> f64 {
        a + b
    }
}

// Export the component
export!(MyPluginComponent);

Working with WIT

Define your API in wit/world.wit:

wit
package example:my-plugin;

world my-plugin-component {
    /// Greet a person by name
    export say-hi: func(name: string) -> string;

    /// Add two numbers
    export add: func(a: f64, b: f64) -> f64;

    /// Process with error handling
    export process: func(input: string) -> result<string, string>;
}

Rust-specific binding:

rust
wit_bindgen::generate!({
    world: "my-plugin-component",
    path: "./wit",
});

impl Guest for MyPlugin {
    fn process(input: String) -> Result<String, String> {
        // Result maps directly to WIT result type
        if input.is_empty() {
            Err("Input cannot be empty".to_string())
        } else {
            Ok(format!("Processed: {}", input))
        }
    }
}

Error Handling

rust
use anyhow::{Context, Result, bail};

// Internal function with rich error types
fn process_internal(input: &str) -> Result<String> {
    if input.is_empty() {
        bail!("Input cannot be empty");
    }

    let data = parse_data(input)
        .context("Failed to parse input")?;

    Ok(format!("Processed: {:?}", data))
}

// Guest implementation converts to string errors
impl Guest for MyPlugin {
    fn process(input: String) -> Result<String, String> {
        process_internal(&input)
            .map_err(|e| e.to_string())
    }
}

Environment Variables

rust
use std::env;

fn get_config() -> Result<String, String> {
    // Read environment variable
    let api_key = env::var("API_KEY")
        .map_err(|_| "API_KEY not set".to_string())?;

    // Use with default
    let debug = env::var("DEBUG")
        .unwrap_or_else(|_| "false".to_string());

    Ok(format!("Key: {}, Debug: {}", api_key, debug))
}

JSON Processing

rust
use serde::{Deserialize, Serialize};
use serde_json;

#[derive(Debug, Serialize, Deserialize)]
struct RequestData {
    command: String,
    value: f64,
    metadata: Option<HashMap<String, String>>,
}

impl Guest for MyPlugin {
    fn process_json(input: String) -> Result<String, String> {
        // Parse JSON
        let data: RequestData = serde_json::from_str(&input)
            .map_err(|e| format!("Invalid JSON: {}", e))?;

        // Process data
        let response = RequestData {
            command: data.command.to_uppercase(),
            value: data.value * 2.0,
            metadata: data.metadata,
        };

        // Serialize back to JSON
        serde_json::to_string(&response)
            .map_err(|e| format!("Serialization failed: {}", e))
    }
}

HTTP Requests (Optional)

Required WIT imports:

wit
world my-plugin-component {
    import wasi:http/types@0.2.0;
    import wasi:http/outgoing-handler@0.2.0;

    export fetch-data: func(url: string) -> result<string, string>;
}

Implementation using waki:

rust
use waki::Client;
use std::time::Duration;
use anyhow::{bail, Context, Result};

fn fetch_internal(url: &str) -> Result<String> {
    let response = Client::new()
        .get(url)
        .connect_timeout(Duration::from_secs(10))
        .send()
        .context("Request failed")?;

    if response.status_code() != 200 {
        bail!("HTTP error: {}", response.status_code());
    }

    String::from_utf8(response.body()?)
        .context("Invalid UTF-8 in response")
}

impl Guest for MyPlugin {
    fn fetch_data(url: String) -> Result<String, String> {
        fetch_internal(&url).map_err(|e| e.to_string())
    }
}

Required permissions:

yaml
permissions:
  network:
    allow:
      - host: "api.example.com"

Testing

Local Testing with Wasmtime

bash
# Build the plugin
noorle plugin build

# Test with WAVE format
wasmtime run --invoke 'say_hi("World")' dist/plugin.wasm
wasmtime run --invoke 'add(2.5, 3.7)' dist/plugin.wasm
wasmtime run --invoke 'process("test data")' dist/plugin.wasm

# With environment variables
wasmtime run --env API_KEY=test123 --invoke 'process("test")' dist/plugin.wasm

Language-Specific Features

Cargo Component

Rust uses cargo-component for building WASM components:

toml
# Cargo.toml
[package.metadata.component]
package = "example:my-plugin"

[package.metadata.component.dependencies]

Type-Safe Bindings

rust
// WIT types become Rust types
// WIT: record person { name: string, age: u32 }
// Rust:
#[derive(Debug)]
struct Person {
    name: String,
    age: u32,
}

// Enums map directly
// WIT: enum status { pending, processing, complete }
// Rust:
#[derive(Debug)]
enum Status {
    Pending,
    Processing,
    Complete,
}

Build Configuration

Dependencies

Cargo.toml:

toml
[dependencies]
wit-bindgen = "0.46.0"              # Required for WIT bindings
anyhow = "1.0"                      # Error handling
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"                  # JSON support
waki = "0.5.1"                      # HTTP client (optional)
chrono = "0.4"                      # Date/time (optional)

[lib]
crate-type = ["cdylib"]             # Required for WASM

[profile.release]
opt-level = "z"                     # Optimize for size
lto = true                          # Link-time optimization

Build Optimization

toml
# Cargo.toml optimization settings
[profile.release]
opt-level = "z"     # Optimize for size
lto = true          # Link-time optimization

Build Process

bash
# Build your plugin (handles compilation, optimization, packaging)
noorle plugin build

The build process:

  1. Compiles to wasm32-wasip2 target
  2. wkg fetches WIT dependencies from imports
  3. Creates component with cargo-component
  4. Optimizes with wasm-tools
  5. Packages into .npack archive

Troubleshooting

Common Issues

Build Fails

Problem: error: target 'wasm32-wasip2' not found

Solution:

bash
rustup target add wasm32-wasip2

Runtime Error

Problem: Guest export 'process' not found

Solution:

rust
// Ensure export macro is present
export!(MyPluginComponent);

// Check struct name matches
struct MyPluginComponent; // Must match export

Cargo Component Issues

Problem: cargo-component command not found

Solution:

bash
cargo install cargo-component

Debugging

Enable debug output:

bash
DEBUG=1 noorle plugin build

Check generated bindings:

bash
ls -la target/wasm32-wasip2/release/

View WASM exports:

bash
wasm-tools component wit dist/plugin.wasm

Performance

Optimization tips:

  • Use --release for production builds
  • Enable LTO in Cargo.toml
  • Minimize dependencies
  • Use wasm-opt for additional optimization
  • Profile with wasmtime --profile

Best Practices

Do's

  • ✅ Use anyhow for internal error handling
  • ✅ Convert to string errors at boundaries
  • ✅ Validate input early and return clear errors
  • ✅ Use serde for JSON parsing
  • ✅ Keep dependencies minimal for smaller binaries
  • ✅ Use cargo clippy for linting

Don'ts

  • ❌ Don't use panic! or unwrap() in production
  • ❌ Don't block on async operations (no tokio)
  • ❌ Don't use heavy dependencies unnecessarily
  • ❌ Don't leak sensitive data in errors

Security

  • Validate all inputs with strong types
  • Use minimal permissions in noorle.yaml
  • Never log sensitive data
  • Sanitize error messages before returning
  • Use #[derive(Debug)] carefully with sensitive structs

Advanced Topics

Custom WASI Interfaces

wit
// Advanced WIT with custom types
interface types {
    record request {
        id: string,
        timestamp: u64,
        payload: list<u8>,
    }

    record response {
        status: u16,
        body: string,
    }
}

world my-plugin {
    use types.{request, response};

    export process-request: func(req: request) -> response;
}
rust
// Implementation with custom types
impl Guest for MyPlugin {
    fn process_request(req: Request) -> Response {
        Response {
            status: 200,
            body: format!("Processed request {}", req.id),
        }
    }
}

Performance Monitoring

rust
use std::time::Instant;

fn timed_operation<F, R>(name: &str, f: F) -> R
where
    F: FnOnce() -> R
{
    let start = Instant::now();
    let result = f();
    let duration = start.elapsed();
    eprintln!("{} took {:?}", name, duration);
    result
}

impl Guest for MyPlugin {
    fn process(input: String) -> Result<String, String> {
        timed_operation("processing", || {
            process_internal(&input)
        }).map_err(|e| e.to_string())
    }
}

Working with Custom Types

rust
// Complex types from WIT
use crate::bindings::types::{Request, Response};

impl Guest for MyPlugin {
    fn handle_complex(req: Request) -> Result<Response, String> {
        // Process complex types
        Ok(Response {
            status: 200,
            headers: req.headers,
            body: process_body(req.body)?,
        })
    }
}

Resources

Documentation

Tools

Examples

Next Steps

Continue Learning

Build Your Plugin