Adding a New Plugin
A plugin is a Rust cdylib crate that depends on elle-plugin (not elle) and exports elle_plugin_init. Plugins use a stable ABI and can be compiled independently from elle.
Files to create / modify (in order)
1. plugins/myplugin/Cargo.toml — New crate with crate-type = ["cdylib"].
2. plugins/myplugin/src/lib.rs — Plugin implementation.
3. Cargo.toml (root) — Add "plugins/myplugin" to [workspace] members.
4. Makefile — Add myplugin to the PLUGINS variable (one name per line, alphabetical).
5. tests/elle/plugins/myplugin.lisp — Integration tests.
6. plugins/myplugin/AGENTS.md — Documentation.
Step by step
Step 1: Create the crate.
# plugins/myplugin/Cargo.toml
[package]
name = "elle-myplugin"
version = "1.0.0"
edition = "2021"
[lib]
crate-type = ["cdylib"]
[dependencies]
elle-plugin = { path = "../../elle-plugin" }
Step 2: Implement the plugin. Every plugin follows the same structure — an elle_plugin_init entry point generated by the define_plugin! macro:
use elle_plugin::{ElleResult, ElleValue, EllePrimDef, SIG_OK};
elle_plugin::define_plugin!("myplugin/", &PRIMITIVES);
extern "C" fn prim_hello(args: *const ElleValue, nargs: usize) -> ElleResult {
let a = api();
if nargs != 0 {
return a.err("arity-error", "myplugin/hello: expected 0 arguments");
}
a.ok(a.string("hello"))
}
static PRIMITIVES: &[EllePrimDef] = &[
EllePrimDef::exact(
"myplugin/hello", prim_hello, SIG_OK, 0,
"Say hello.", "myplugin", "(myplugin/hello)",
),
];
Key points:
- Primitives are
extern "C" fn(args: const ElleValue, nargs: usize) -> ElleResult - Use
api()to access the resolved API (constructors, accessors, etc.) - Use
a.ok(value)for success,a.err(kind, msg)for errors EllePrimDef::exact(name, func, signal, arity, doc, category, example)
Step 3: Register in the workspace. Add to the root Cargo.toml:
[workspace]
members = [
# ...
"plugins/myplugin",
]
Step 4: Add to CI. Add myplugin to the PLUGINS variable in the Makefile, keeping alphabetical order. Run make check-plugin-list to verify the Makefile and Cargo.toml stay in sync.
Step 5: Write tests in tests/elle/plugins/myplugin.lisp:
(elle/epoch 1)
(def [ok? plugin] (protect (import "plugin/myplugin")))
(when (not ok?)
(println "SKIP: myplugin plugin not built")
(exit 0))
(def hello-fn (get plugin :hello))
(assert (= (hello-fn) "hello") "myplugin/hello works")
API reference
The api() function returns a reference to the resolved Api struct. Common methods:
let a = api();
// Constructors
a.int(42) // i64 → ElleValue
a.float(3.14) // f64 → ElleValue
a.string("hello") // &str → ElleValue
a.keyword("error") // &str → ElleValue (keyword)
a.bytes(&[1, 2, 3]) // &[u8] → ElleValue
a.nil() // nil
a.boolean(true) // bool → ElleValue
a.array(&[v1, v2]) // &[ElleValue] → ElleValue
a.build_struct(&[("key", val)]) // &[(&str, ElleValue)] → ElleValue
a.external("name", data) // wrap Rust value
// Accessors
a.get_int(v) // Option<i64>
a.get_float(v) // Option<f64>
a.get_string(v) // Option<&str>
a.get_bytes(v) // Option<&[u8]>
a.get_bool(v) // Option<bool>
a.get_external::<T>(v, "name") // Option<&T>
// Results
a.ok(value) // ElleResult with SIG_OK
a.err("kind", "msg") // ElleResult with SIG_ERROR
a.yield_io(request) // ElleResult with SIG_YIELD | SIG_IO
// Async
a.poll_fd(fd, events) // Create poll-fd I/O request
Conventions
- Plugin functions should validate arity and types.
- Return
a.ok(value)for success,a.err(kind, msg)for errors. - Wrap external Rust types via
a.external("typename", value). - The entry point is generated by
define_plugin!— don't write it manually.
See also
- Cookbook index
- Plugin list
elle-plugin/AGENTS.md— ABI types referenceplugins/AGENTS.md— plugin system reference