Skip to main content

Crate sen_plugin_sdk

Crate sen_plugin_sdk 

Source
Expand description

sen-plugin-sdk: SDK for creating WASM plugins

This SDK provides utilities and helpers for creating WASM plugins with minimal boilerplate. Using this SDK, you can create a fully functional plugin in under 30 lines of code.

§Table of Contents

§Project Setup

§1. Create a New Plugin Project

cargo new --lib my-plugin
cd my-plugin

§2. Configure Cargo.toml

Your complete Cargo.toml should look like:

[package]
name = "my-plugin"
version = "0.1.0"
edition = "2021"

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

[dependencies]
sen-plugin-sdk = { version = "0.7" }

# Optimize for size (optional but recommended)
[profile.release]
opt-level = "s"
lto = true
strip = true

§3. Install WASM Target (One-Time)

rustup target add wasm32-unknown-unknown

§4. Build Your Plugin

cargo build --release --target wasm32-unknown-unknown

The output file will be at: target/wasm32-unknown-unknown/release/my_plugin.wasm

§Quick Start

A minimal plugin requires:

  1. A struct implementing the Plugin trait
  2. The export_plugin! macro to generate WASM exports
use sen_plugin_sdk::prelude::*;

struct HelloPlugin;

impl Plugin for HelloPlugin {
    fn manifest() -> PluginManifest {
        PluginManifest::new(
            CommandSpec::new("hello", "Says hello to the world")
                .version("1.0.0")
                .arg(ArgSpec::positional("name").help("Name to greet"))
        )
    }

    fn execute(args: Vec<String>) -> ExecuteResult {
        let name = args.first().map(|s| s.as_str()).unwrap_or("World");
        ExecuteResult::success(format!("Hello, {}!", name))
    }
}

export_plugin!(HelloPlugin);

§Arguments

§Positional Arguments

Positional arguments are passed in order:

CommandSpec::new("copy", "Copy files")
    .arg(ArgSpec::positional("source").required().help("Source file"))
    .arg(ArgSpec::positional("dest").required().help("Destination file"))

Usage: copy src.txt dst.txt

In execute(), args are: ["src.txt", "dst.txt"]

§Options (Flags with Values)

Named options with long and short forms:

CommandSpec::new("greet", "Greet someone")
    .arg(ArgSpec::positional("name").default("World"))
    .arg(
        ArgSpec::option("greeting", "greeting")
            .short('g')
            .help("Custom greeting message")
            .default("Hello")
    )
    .arg(
        ArgSpec::option("count", "count")
            .short('n')
            .help("Number of times to greet")
            .default("1")
    )

Usage: greet Alice -g "Good morning" --count 3

§Required Arguments

Mark arguments as required:

ArgSpec::positional("file")
    .required()
    .help("Input file (required)")

§Default Values

Provide fallback values:

ArgSpec::option("format", "format")
    .short('f')
    .default("json")
    .help("Output format [default: json]")

§Argument Parsing in execute()

Arguments are passed as a Vec<String> in the order they appear. The host handles option parsing; your plugin receives resolved values:

fn execute(args: Vec<String>) -> ExecuteResult {
    // For: greet Alice -g "Hi"
    // args = ["Alice", "Hi"]

    let name = args.get(0).map(|s| s.as_str()).unwrap_or("World");
    let greeting = args.get(1).map(|s| s.as_str()).unwrap_or("Hello");

    ExecuteResult::success(format!("{}, {}!", greeting, name))
}

§Error Handling

Plugins return ExecuteResult which can be:

§Success

ExecuteResult::success("Operation completed successfully")

§User Error (Exit Code 1)

For expected errors like invalid input:

fn execute(args: Vec<String>) -> ExecuteResult {
    let file = match args.first() {
        Some(f) => f,
        None => return ExecuteResult::user_error("Missing required argument: file"),
    };

    if !is_valid_format(file) {
        return ExecuteResult::user_error(format!(
            "Invalid file format: {}. Expected .json or .yaml",
            file
        ));
    }

    ExecuteResult::success("File processed")
}

§System Error (Exit Code 101)

For unexpected internal errors:

fn execute(args: Vec<String>) -> ExecuteResult {
    match process_data(&args) {
        Ok(result) => ExecuteResult::success(result),
        Err(e) => ExecuteResult::system_error(format!("Internal error: {}", e)),
    }
}

§Advanced Usage

§Subcommands

Create nested command structures:

CommandSpec::new("db", "Database operations")
    .subcommand(
        CommandSpec::new("create", "Create a new database")
            .arg(ArgSpec::positional("name").required())
    )
    .subcommand(
        CommandSpec::new("drop", "Drop a database")
            .arg(ArgSpec::positional("name").required())
    )
    .subcommand(
        CommandSpec::new("list", "List all databases")
    )

§Plugin Metadata

Add author and version information:

CommandSpec::new("mytool", "My awesome tool")
    .version("2.1.0")
    // Note: author is set on CommandSpec, not PluginManifest

§Manual Implementation

If you need more control, you can implement the WASM exports manually instead of using the SDK. This is what the export_plugin! macro generates:

use sen_plugin_api::{ArgSpec, CommandSpec, ExecuteResult, PluginManifest, API_VERSION};
use std::alloc::{alloc, dealloc, Layout};

// 1. Memory allocator for host-guest communication
#[no_mangle]
pub extern "C" fn plugin_alloc(size: i32) -> i32 {
    if size <= 0 { return 0; }
    let layout = Layout::from_size_align(size as usize, 1).unwrap();
    unsafe { alloc(layout) as i32 }
}

// 2. Memory deallocator
#[no_mangle]
pub extern "C" fn plugin_dealloc(ptr: i32, size: i32) {
    if ptr == 0 || size <= 0 { return; }
    let layout = Layout::from_size_align(size as usize, 1).unwrap();
    unsafe { dealloc(ptr as *mut u8, layout) }
}

// 3. Return plugin manifest (command specification)
#[no_mangle]
pub extern "C" fn plugin_manifest() -> i64 {
    let manifest = PluginManifest {
        api_version: API_VERSION,
        command: CommandSpec::new("hello", "Says hello")
            .arg(ArgSpec::positional("name").default("World")),
    };
    serialize_to_memory(&manifest)
}

// 4. Execute the command
#[no_mangle]
pub extern "C" fn plugin_execute(args_ptr: i32, args_len: i32) -> i64 {
    let args: Vec<String> = unsafe {
        let slice = std::slice::from_raw_parts(args_ptr as *const u8, args_len as usize);
        rmp_serde::from_slice(slice).unwrap_or_default()
    };

    let name = args.first().map(|s| s.as_str()).unwrap_or("World");
    let result = ExecuteResult::success(format!("Hello, {}!", name));
    serialize_to_memory(&result)
}

// Helper: Pack pointer and length into i64
fn pack_ptr_len(ptr: i32, len: i32) -> i64 {
    ((ptr as i64) << 32) | (len as i64 & 0xFFFFFFFF)
}

// Helper: Serialize value to guest memory
fn serialize_to_memory<T: serde::Serialize>(value: &T) -> i64 {
    let bytes = rmp_serde::to_vec(value).expect("Serialization failed");
    let len = bytes.len() as i32;
    let ptr = plugin_alloc(len);
    if ptr == 0 { return 0; }
    unsafe {
        std::ptr::copy_nonoverlapping(bytes.as_ptr(), ptr as *mut u8, bytes.len());
    }
    pack_ptr_len(ptr, len)
}

§Best Practices

§Do

  • Keep plugins focused: One plugin, one responsibility
  • Validate inputs early: Check arguments at the start of execute()
  • Return meaningful errors: Include context in error messages
  • Use default values: Make common cases convenient
  • Document your commands: Use .help() on all arguments

§Don’t

  • Don’t panic: Always return ExecuteResult::user_error or system_error
  • Don’t use unwrap(): Prefer unwrap_or, unwrap_or_default, or match
  • Don’t allocate excessively: WASM has limited memory
  • Don’t block forever: The host has CPU limits (fuel)

§Example: Robust Argument Handling

fn execute(args: Vec<String>) -> ExecuteResult {
    // Validate required arguments
    let file = match args.get(0) {
        Some(f) if !f.is_empty() => f,
        _ => return ExecuteResult::user_error("Missing required argument: file"),
    };

    // Parse optional numeric argument with default
    let count: usize = args.get(1)
        .and_then(|s| s.parse().ok())
        .unwrap_or(1);

    // Validate value range
    if count == 0 || count > 100 {
        return ExecuteResult::user_error(
            "Count must be between 1 and 100"
        );
    }

    ExecuteResult::success(format!("Processing {} {} time(s)", file, count))
}

§Troubleshooting

§Build Errors

Error: can't find crate for std

Make sure you’re building for the correct target:

cargo build --release --target wasm32-unknown-unknown

Error: crate-type must be cdylib

Add to your Cargo.toml:

[lib]
crate-type = ["cdylib"]

§Runtime Errors

Error: API version mismatch

Your plugin was built with a different API version. Rebuild with the matching sen-plugin-sdk version.

Error: Function not found: plugin_manifest

Make sure you have export_plugin!(YourPlugin); at the end of your lib.rs.

Error: Fuel exhausted

Your plugin is taking too long (possible infinite loop). The host limits CPU usage to prevent runaway plugins.

§Debugging Tips

  1. Test locally first: Write unit tests for your execute() logic
  2. Check WASM size: Large plugins may have unnecessary dependencies
  3. Simplify arguments: Start with positional args, add options later

§Examples

See the examples/ directory for complete working plugins:

  • examples/hello-plugin/: Manual implementation (no SDK)
  • examples/greet-plugin/: SDK-based with options

Modules§

memory
Memory utilities for Wasm plugin development
prelude
Prelude module for convenient imports

Macros§

export_plugin
Macro to export all required plugin functions

Structs§

ArgSpec
Argument specification
Capabilities
Plugin capability declarations
CommandSpec
Command specification returned by plugin’s manifest() function
ExecuteError
Error details from plugin execution
HttpResponse
HTTP response data
NetPattern
Network access pattern
PathPattern
Filesystem path pattern
PluginManifest
Plugin manifest with API version
StdioCapability
Standard I/O capability flags

Enums§

Effect
Effect request from plugin to host
EffectResult
Result of an effect, passed back to plugin via resume
ExecuteResult
Result of plugin execution
NetProtocol
Network protocol

Constants§

API_VERSION
API version for compatibility checking

Traits§

Plugin
Trait that plugins must implement