| name | nemo-plugin-patterns |
| description | Native and WASM plugin development patterns for Nemo including PluginContext API, plugin lifecycle, manifest configuration, and build/deploy workflows. Use when creating or debugging Nemo plugins. |
Nemo Plugin Patterns
Use this skill when creating, modifying, or debugging Nemo plugins (native or WASM).
Three-Tier Extension Model
XML (declarative) → Rhai scripts (lightweight logic) → Native/WASM plugins (full power)
| Tier | Sandboxing | Use Case |
|---|
| Rhai scripts | Operation limits, no I/O | Event handlers, simple transforms, UI logic |
| WASM plugins | Memory isolation, capability-based | Data sources, complex transforms, sandboxed logic |
| Native plugins | None (Rust safety only) | Full system access, performance-critical, background threads |
PluginContext API
Available to all plugins at runtime:
trait PluginContext: Send + Sync {
fn get_data(&self, path: &str) -> Option<PluginValue>;
fn set_data(&self, path: &str, value: PluginValue) -> Result<(), PluginError>;
fn emit_event(&self, event_type: &str, payload: PluginValue);
fn get_config(&self, path: &str) -> Option<PluginValue>;
fn log(&self, level: LogLevel, message: &str);
fn get_component_property(&self, component_id: &str, property: &str) -> Option<PluginValue>;
fn set_component_property(&self, component_id: &str, property: &str, value: PluginValue) -> Result<(), PluginError>;
}
PluginValue Types
enum PluginValue {
Null, Bool(bool), Integer(i64), Float(f64),
String(String), Array(Vec<PluginValue>),
Object(HashMap<String, PluginValue>),
}
Native Plugin Pattern
Cargo.toml
[package]
name = "my-plugin"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib"]
[dependencies]
nemo-plugin-api = { path = "../../crates/nemo-plugin-api" }
semver = { workspace = true }
src/lib.rs
use nemo_plugin_api::*;
fn init(registrar: &mut dyn PluginRegistrar) {
let ctx = registrar.context_arc();
ctx.log(LogLevel::Info, "Plugin initialized");
std::thread::spawn(move || {
loop {
std::thread::sleep(std::time::Duration::from_secs(2));
let _ = ctx.set_data("my.value", PluginValue::Float(42.0));
}
});
}
declare_plugin!(
PluginManifest::new("my-plugin", "My Plugin", semver::Version::new(0, 1, 0))
.with_description("What it does")
.with_capability(Capability::DataSource("my".to_string())),
init
);
Build
cargo build -p my-plugin --release
Lifecycle
ExtensionLoader scans plugin directories for .dylib/.so/.dll
PluginHost calls libloading::Library::new(path)
- Host calls
nemo_plugin_manifest() → gets manifest
- Host calls
nemo_plugin_entry(registrar) → plugin registers + starts
- Plugin uses
PluginContext via registrar.context_arc() throughout lifetime
WASM Plugin Pattern
Cargo.toml
[package]
name = "my-wasm-plugin"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib"]
[dependencies]
nemo-wasm-guest = { path = "../../crates/nemo-wasm-guest" }
wit-bindgen = { workspace = true }
src/lib.rs
wit_bindgen::generate!({
path: "../../crates/nemo-wasm/wit",
world: "nemo-plugin",
});
struct MyPlugin;
impl Guest for MyPlugin {
fn get_manifest() -> PluginManifest {
PluginManifest {
id: "my-wasm-plugin".to_string(),
name: "My WASM Plugin".to_string(),
version: "0.1.0".to_string(),
description: "What it does".to_string(),
}
}
fn init() {
host_api::log(LogLevel::Info, "WASM plugin initialized");
}
fn tick() -> u64 {
let value = PluginValue::Float(42.0);
let _ = host_api::set_data("my.value", value);
2000
}
}
export_nemo_plugin!(MyPlugin);
Build
cargo component build -p my-wasm-plugin --release
Lifecycle
ExtensionLoader scans for .wasm files
WasmHost creates Component::from_binary() via wasmtime
- Host calls
get-manifest() export
- Host calls
init() export
- Host calls
tick() at plugin-specified intervals
- Plugin returns ms until next tick (0 = stop)
Key Difference: Tick-Based vs Threaded
- WASM: No threads. Host drives execution via
tick(). Return interval in ms.
- Native: Full thread access. Can
std::thread::spawn for background work.
Rhai Script Pattern
Script File
// scripts/handlers.rhai
fn on_button_click(component_id, event_data) {
log_info("Clicked: " + component_id);
let value = get_data("some.path");
set_component_text("label_id", "Updated: " + value);
}
fn on_input_change(component_id, event_data) {
set_data("search.query", event_data);
}
Handler calling convention. Every XML-referenced handler (on-click,
on-change, on-load, …) is invoked as fn name(component_id, event_data) —
two string arguments. Rhai resolves functions by name and arity, so a
mismatch (e.g. a zero-parameter handler) fails at runtime with
Function not found: <name>. Match the two-argument signature exactly. This
includes on-load, which is dispatched with component_id = "app" and
event_data = "load".
Available Rhai Functions
Data: get_data(path), set_data(path, value), get_config(path)
Components: get_component_property(id, prop), set_component_property(id, prop, value), get_component_label(id), get_component_text(id), set_component_text(id, text), set_component_label(id, label)
HTTP: http_get(url), http_post(url, body), http_put(url, body), http_delete(url) — synchronous, blocking; return {status, body, ok} on success or {error} on failure. Intended for UI event handlers (e.g. on-click), not async data-source contexts.
JSON: json_parse(str) → parsed value, json_stringify(value) → string
Math: abs, min, max, clamp, floor, ceil, round, sqrt, pow
Strings: trim, to_upper, to_lower, starts_with, ends_with, contains, replace
Type conversion: parse_float, parse_int, to_string, to_int, to_float
Logging: log_debug, log_info, log_warn, log_error, print
Sandboxing Limits
- Max operations: 100,000
- Max string size: 64KB
- Max array/map size: 10,000
- Max call stack depth: 64
- No
eval, no file I/O. Network access IS available via the http_* functions above (file/system access remain gated off by default).
Plugin Capabilities
enum Capability {
Component(String),
DataSource(String),
Transform(String),
Action(String),
EventHandler(String),
}
Plugin Permissions
struct PluginPermissions {
network: bool,
filesystem: bool,
system: bool,
data_read: bool,
data_write: bool,
component_read: bool,
component_write: bool,
event_emit: bool,
}
XML Plugin Configuration
<nemo>
<plugin name="my-plugin" path="./plugins/my-plugin" />
<plugin name="my-wasm-plugin" path="./plugins/my-wasm-plugin.wasm" />
</nemo>
Workspace Setup
Add to root Cargo.toml:
[workspace]
members = [
"plugins/my-plugin",
]
Debugging Plugins
- Plugin not loading → Check
crate-type = ["cdylib"], verify file extension matches platform
- Manifest errors → Check
declare_plugin! macro or WIT get-manifest return
- Context API failures → Check that
PluginContext methods are called correctly
- WASM type issues → Complex types (arrays/objects) use JSON serialization in WIT bridge
- Extension discovery → Check
nemo-extension/src/loader.rs for path scanning logic