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
- Quick Start
- Arguments
- Error Handling
- Advanced Usage
- Manual Implementation
- Best Practices
- Troubleshooting
§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-unknownThe output file will be at:
target/wasm32-unknown-unknown/release/my_plugin.wasm
§Quick Start
A minimal plugin requires:
- A struct implementing the
Plugintrait - 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_errororsystem_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-unknownError: 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
- Test locally first: Write unit tests for your
execute()logic - Check WASM size: Large plugins may have unnecessary dependencies
- 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§
Macros§
- export_
plugin - Macro to export all required plugin functions
Structs§
- ArgSpec
- Argument specification
- Capabilities
- Plugin capability declarations
- Command
Spec - Command specification returned by plugin’s
manifest()function - Execute
Error - Error details from plugin execution
- Http
Response - HTTP response data
- NetPattern
- Network access pattern
- Path
Pattern - Filesystem path pattern
- Plugin
Manifest - Plugin manifest with API version
- Stdio
Capability - Standard I/O capability flags
Enums§
- Effect
- Effect request from plugin to host
- Effect
Result - Result of an effect, passed back to plugin via resume
- Execute
Result - Result of plugin execution
- NetProtocol
- Network protocol
Constants§
- API_
VERSION - API version for compatibility checking
Traits§
- Plugin
- Trait that plugins must implement