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:

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


See also