Rust is excellent for WASM plugins due to its zero-cost abstractions, memory safety, and mature WASM tooling. Signal K Rust plugins use buffer-based FFI for string passing, which differs from AssemblyScript's automatic string handling.
| Aspect | AssemblyScript | Rust |
|---|---|---|
| String passing | Automatic via AS loader | Manual buffer-based FFI |
| Memory management | AS runtime handles | allocate/deallocate exports |
| Binary size | 3-10 KB | 50-200 KB |
| Target | wasm32 (AS compiler) |
wasm32-wasip1 |
Create a new Rust library project:
cargo new --lib example-anchor-watch-rust
cd example-anchor-watch-rust
[package]
name = "anchor_watch_rust"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib"]
[dependencies]
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
[profile.release]
opt-level = "z" # Optimize for size
lto = true # Link-time optimization
strip = true # Strip symbols
use std::cell::RefCell;
use serde::{Deserialize, Serialize};
// =============================================================================
// FFI Imports - These MUST match what the Signal K runtime provides in "env"
// =============================================================================
#[link(wasm_import_module = "env")]
extern "C" {
fn sk_debug(ptr: *const u8, len: usize);
fn sk_set_status(ptr: *const u8, len: usize);
fn sk_set_error(ptr: *const u8, len: usize);
fn sk_handle_message(ptr: *const u8, len: usize);
fn sk_register_put_handler(
context_ptr: *const u8, context_len: usize,
path_ptr: *const u8, path_len: usize
) -> i32;
}
// =============================================================================
// Helper wrappers for FFI functions
// =============================================================================
fn debug(msg: &str) {
unsafe { sk_debug(msg.as_ptr(), msg.len()); }
}
fn set_status(msg: &str) {
unsafe { sk_set_status(msg.as_ptr(), msg.len()); }
}
fn set_error(msg: &str) {
unsafe { sk_set_error(msg.as_ptr(), msg.len()); }
}
fn handle_message(msg: &str) {
unsafe { sk_handle_message(msg.as_ptr(), msg.len()); }
}
fn register_put_handler(context: &str, path: &str) -> i32 {
unsafe {
sk_register_put_handler(
context.as_ptr(), context.len(),
path.as_ptr(), path.len()
)
}
}
// =============================================================================
// Memory Allocation - REQUIRED for buffer-based string passing
// =============================================================================
/// Allocate memory for string passing from host
#[no_mangle]
pub extern "C" fn allocate(size: usize) -> *mut u8 {
let mut buf = Vec::with_capacity(size);
let ptr = buf.as_mut_ptr();
std::mem::forget(buf);
ptr
}
/// Deallocate memory
#[no_mangle]
pub extern "C" fn deallocate(ptr: *mut u8, size: usize) {
unsafe {
let _ = Vec::from_raw_parts(ptr, 0, size);
}
}
// =============================================================================
// Plugin State
// =============================================================================
thread_local! {
static STATE: RefCell<PluginState> = RefCell::new(PluginState::default());
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(rename_all = "camelCase")]
struct PluginConfig {
#[serde(default)]
max_radius: f64,
}
#[derive(Debug, Default)]
struct PluginState {
config: PluginConfig,
is_running: bool,
}
// =============================================================================
// Plugin Exports - Core plugin interface
// =============================================================================
static PLUGIN_ID: &str = "my-rust-plugin";
static PLUGIN_NAME: &str = "My Rust Plugin";
static PLUGIN_SCHEMA: &str = r#"{
"type": "object",
"properties": {
"maxRadius": {
"type": "number",
"title": "Max Radius",
"default": 50
}
}
}"#;
/// Return the plugin ID (buffer-based)
#[no_mangle]
pub extern "C" fn plugin_id(out_ptr: *mut u8, out_max_len: usize) -> i32 {
write_string(PLUGIN_ID, out_ptr, out_max_len)
}
/// Return the plugin name (buffer-based)
#[no_mangle]
pub extern "C" fn plugin_name(out_ptr: *mut u8, out_max_len: usize) -> i32 {
write_string(PLUGIN_NAME, out_ptr, out_max_len)
}
/// Return the plugin JSON schema (buffer-based)
#[no_mangle]
pub extern "C" fn plugin_schema(out_ptr: *mut u8, out_max_len: usize) -> i32 {
write_string(PLUGIN_SCHEMA, out_ptr, out_max_len)
}
/// Start the plugin with configuration
#[no_mangle]
pub extern "C" fn plugin_start(config_ptr: *const u8, config_len: usize) -> i32 {
// Read config from buffer
let config_json = unsafe {
let slice = std::slice::from_raw_parts(config_ptr, config_len);
String::from_utf8_lossy(slice).to_string()
};
// Parse configuration
let parsed_config: PluginConfig = match serde_json::from_str(&config_json) {
Ok(c) => c,
Err(e) => {
set_error(&format!("Failed to parse config: {}", e));
return 1;
}
};
// Update state
STATE.with(|state| {
let mut s = state.borrow_mut();
s.config = parsed_config;
s.is_running = true;
});
debug("Plugin started successfully");
set_status("Running");
0 // Success
}
/// Stop the plugin
#[no_mangle]
pub extern "C" fn plugin_stop() -> i32 {
STATE.with(|state| {
state.borrow_mut().is_running = false;
});
debug("Plugin stopped");
set_status("Stopped");
0 // Success
}
// =============================================================================
// Helper Functions
// =============================================================================
/// Write string to output buffer, return bytes written
fn write_string(s: &str, ptr: *mut u8, max_len: usize) -> i32 {
let bytes = s.as_bytes();
let len = bytes.len().min(max_len);
unsafe {
std::ptr::copy_nonoverlapping(bytes.as_ptr(), ptr, len);
}
len as i32
}
{
"name": "my-rust-wasm-plugin",
"version": "0.1.0",
"description": "My Rust WASM plugin for Signal K",
"keywords": ["signalk-wasm-plugin"],
"wasmManifest": "plugin.wasm",
"wasmCapabilities": {
"network": false,
"storage": "vfs-only",
"dataRead": true,
"dataWrite": true,
"putHandlers": true
},
"author": "Your Name",
"license": "Apache-2.0"
}
Note: The package name can be anything - there's no requirement for
@signalk/scope. ThewasmManifestfield is what identifies this as a WASM plugin.
# Build with WASI Preview 1 target (required for Signal K)
cargo build --release --target wasm32-wasip1
# Copy to plugin.wasm
cp target/wasm32-wasip1/release/my_rust_plugin.wasm plugin.wasm
Important: Use
wasm32-wasip1target, NOTwasm32-wasi. Signal K requires WASI Preview 1.
Option 1: Symlink (Recommended for Development)
cd ~/.signalk/node_modules
ln -s /path/to/your/my-rust-wasm-plugin my-rust-wasm-plugin
Option 2: Direct Copy
mkdir -p ~/.signalk/node_modules/my-rust-wasm-plugin
cp plugin.wasm package.json ~/.signalk/node_modules/my-rust-wasm-plugin/
Option 3: NPM Package Install
npm pack
npm install -g ./my-rust-wasm-plugin-0.1.0.tgz
Signal K provides these FFI imports in the env module:
| Function | Parameters | Description |
|---|---|---|
sk_debug |
(ptr, len) |
Log debug message |
sk_set_status |
(ptr, len) |
Set plugin status |
sk_set_error |
(ptr, len) |
Set error message |
sk_handle_message |
(ptr, len) |
Emit delta message |
sk_register_put_handler |
(ctx_ptr, ctx_len, path_ptr, path_len) |
Register PUT handler |
IMPORTANT: Use Exact Function Names
You MUST use the exact function names listed above. Common mistakes:
sk_log_debug,sk_log_info,sk_log_warn→ Usesk_debugfor all loggingsk_emit_delta→ Usesk_handle_messagesk_udp_recv_from→ Usesk_udp_recvThere is only one logging function (
sk_debug). If you need log levels, prefix your message:debug("[INFO] Starting radar scan");
debug("[WARN] Connection timeout");
Your plugin MUST export:
| Export | Signature | Description |
|---|---|---|
plugin_id |
(out_ptr, max_len) -> len |
Return plugin ID |
plugin_name |
(out_ptr, max_len) -> len |
Return plugin name |
plugin_schema |
(out_ptr, max_len) -> len |
Return JSON schema |
plugin_start |
(config_ptr, config_len) -> status |
Start plugin |
plugin_stop |
() -> status |
Stop plugin |
allocate |
(size) -> ptr |
Allocate memory |
deallocate |
(ptr, size) |
Free memory |
Your plugin MAY export:
| Export | Signature | Description |
|---|---|---|
poll |
() -> status |
Called every 1 second while plugin is running. Useful for polling hardware, sockets, or external systems. Return 0 for success, non-zero for errors. |
http_endpoints |
() -> json |
Return JSON array of HTTP endpoint definitions |
delta_handler |
(delta_ptr, delta_len) |
Receives Signal K deltas as JSON strings. Called for every delta emitted by the server. |
See the example-anchor-watch-rust plugin in examples/wasm-plugins/example-anchor-watch-rust/ for a complete working plugin with PUT handlers.