Skip to content

Latest commit

 

History

History
354 lines (282 loc) · 11.1 KB

File metadata and controls

354 lines (282 loc) · 11.1 KB
title Rust Plugins

Creating Rust Plugins

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.

Rust vs AssemblyScript: Key Differences

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

Step 1: Project Structure

Create a new Rust library project:

cargo new --lib example-anchor-watch-rust
cd example-anchor-watch-rust

Step 2: Configure Cargo.toml

[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

Step 3: Implement Plugin (src/lib.rs)

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
}

Step 4: Create package.json

{
  "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. The wasmManifest field is what identifies this as a WASM plugin.

Step 5: Build

# 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-wasip1 target, NOT wasm32-wasi. Signal K requires WASI Preview 1.

Step 6: Install

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

Step 7: Enable in Admin UI

  1. Navigate to ServerPlugin Config
  2. Find "My Rust Plugin"
  3. Click Enable
  4. Configure settings
  5. Click Submit

Rust FFI Interface Reference

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 → Use sk_debug for all logging
  • sk_emit_delta → Use sk_handle_message
  • sk_udp_recv_from → Use sk_udp_recv

There is only one logging function (sk_debug). If you need log levels, prefix your message:

debug("[INFO] Starting radar scan");
debug("[WARN] Connection timeout");

Required Plugin Exports

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

Optional Plugin Exports

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.

Additional Resources

See the example-anchor-watch-rust plugin in examples/wasm-plugins/example-anchor-watch-rust/ for a complete working plugin with PUT handlers.