Skip to main content

lumen_plugin/
lib.rs

1//! SDK for Lumen runtime plugins.
2//!
3//! A runtime plugin is a Rust cdylib the engine loads when an app starts. It
4//! registers native functions the app's scripts call, ships language source
5//! that wraps them, and pushes events at the app from its own threads.
6//! Authors implement [`RuntimePlugin`] and export it with [`lumen_plugin!`];
7//! no unsafe code is involved on the author side.
8//!
9//! The plugin and the engine exchange bytes over a C ABI ([`abi`]), so a
10//! plugin works with the prebuilt engine as long as both were built from the
11//! same release tag (enforced by a version handshake at load).
12//!
13//! # A plugin is not in the engine's address space
14//!
15//! The cdylib links its own copy of every Lumen crate it names. The types
16//! re-exported below are data: they encode, cross the boundary, and decode
17//! into the engine's own copy of the same type. Anything that is not data
18//! does not cross. A `static` inside the plugin is a different static from
19//! the engine's, a resource the plugin inserts into a world of its own is not
20//! the app's world, and a handle it holds means nothing on the other side.
21//!
22//! Every effect on the app therefore goes one of two ways: a
23//! [`ScriptCommand`] emitted from a call through [`Cx::emit`], or an event
24//! pushed at any time through [`Host`]. That is the whole surface, and it is
25//! why this crate re-exports data types only and never a whole engine crate.
26
27pub mod abi;
28#[doc(hidden)]
29pub mod export;
30#[cfg(feature = "host")]
31mod host;
32#[cfg(feature = "testing")]
33pub mod testing;
34pub mod wire;
35
36use serde::{Deserialize, Serialize};
37
38use crate::abi::{HostVtable, LogLevel};
39
40#[cfg(feature = "host")]
41pub use host::{FailureReason, HostHooks, InitEnv, LoadFailure, PluginSet, ResolvedModule};
42pub use wire::{Call, CallOut, FnDecl, InitCx, Manifest, PluginEvent};
43
44// The wire codec is shared with the compiler plugin system; it lives in
45// `lumen-plugin-abi` and is re-exported here so a plugin crate names only
46// this one.
47pub use lumen_plugin_abi::codec;
48/// The script surface a plugin describes its functions in and answers calls
49/// with. Data types, from the engine's own crate, so a value a plugin builds
50/// decodes into the one the script layer routes.
51pub use lumen_script::{
52    SCRIPT_WIRE_VERSION, ScriptCommand, ScriptField, ScriptNs, ScriptParam, ScriptPrelude,
53    ScriptSig, ScriptStruct, ScriptTy, ScriptValue,
54};
55
56/// What a runtime plugin does.
57///
58/// One instance serves the whole process and [`register`](Self::register)
59/// takes `&self`, so a plugin holding mutable state brings its own lock (the
60/// `Send + Sync` bound is what makes that explicit).
61pub trait RuntimePlugin: Send + Sync + 'static {
62    /// Declare what the plugin offers. Runs once, before the app's scripts
63    /// load; failing here fails the module's load and the app carries on
64    /// without it.
65    fn register(&self, r: &mut Registrar, cx: &InitCx) -> Result<(), Error>;
66
67    /// Release what the plugin owns. Runs once, when the app is going down.
68    /// The default does nothing.
69    fn shutdown(&self) {
70        // Most plugins own nothing that outlives the process.
71    }
72}
73
74/// What a plugin registers into, handed to
75/// [`RuntimePlugin::register`](RuntimePlugin::register).
76pub struct Registrar {
77    fns: Vec<PluginFn>,
78    preludes: Vec<ScriptPrelude>,
79    host: Host,
80}
81
82impl Default for Registrar {
83    fn default() -> Self {
84        Self {
85            fns: Vec::new(),
86            preludes: Vec::new(),
87            host: Host::disconnected(),
88        }
89    }
90}
91
92impl Registrar {
93    /// Registration with a host attached, which is what the engine hands a
94    /// plugin.
95    pub(crate) fn new(host: Host) -> Self {
96        Self {
97            host,
98            ..Self::default()
99        }
100    }
101
102    /// The host, for a plugin that starts a thread while it registers.
103    ///
104    /// A plugin that watches a file, polls a device, or waits on a socket
105    /// starts doing so here, and the handle it needs to deliver what it
106    /// finds is this one. It stays valid for the process.
107    pub fn host(&self) -> Host {
108        self.host
109    }
110
111    /// Offer a function. Registration order is the order the app's script
112    /// hosts bind it in, and a later function of the same namespace and name
113    /// shadows an earlier one.
114    pub fn script_fn(&mut self, f: PluginFn) -> &mut Self {
115        self.fns.push(f);
116        self
117    }
118
119    /// Ship source in `lang` that the host of that language compiles ahead of
120    /// the app's own program, wrapping the functions registered under `ns`.
121    ///
122    /// This is how a plugin offers sugar over what it registered: a struct
123    /// and an `impl` block in that language, so a script calls
124    /// `Gpio::read(pin)` rather than the free function.
125    pub fn prelude(&mut self, lang: &str, ns: &str, source: &str) -> &mut Self {
126        self.preludes.push(ScriptPrelude {
127            lang: lang.to_string(),
128            ns: ns.to_string(),
129            source: source.to_string(),
130        });
131        self
132    }
133
134    /// The manifest describing what was registered.
135    fn manifest(&self) -> Manifest {
136        Manifest {
137            fns: self.fns.iter().map(PluginFn::decl).collect(),
138            preludes: self.preludes.clone(),
139            capabilities: Vec::new(),
140        }
141    }
142
143    /// Take the bodies, leaving the registrar empty.
144    fn take_fns(&mut self) -> Vec<PluginFn> {
145        std::mem::take(&mut self.fns)
146    }
147}
148
149/// The body of a [`PluginFn`]: the arguments and the command sink go in, a
150/// value or the message the script raises comes out.
151pub type PluginFnBody = Box<dyn Fn(&mut Cx<'_>) -> Result<ScriptValue, String> + Send + Sync>;
152
153/// One function a plugin offers to the app's scripts.
154///
155/// Describes the same thing `lumen_script::ScriptFn` does in-process: a name,
156/// a namespace, a signature, and a body.
157pub struct PluginFn {
158    name: String,
159    ns: ScriptNs,
160    sig: ScriptSig,
161    body: PluginFnBody,
162}
163
164impl PluginFn {
165    /// Start a description. Finish it with
166    /// [`PluginFnBuilder::build`](PluginFnBuilder::build).
167    ///
168    /// ```
169    /// use lumen_plugin::{PluginFn, ScriptTy, ScriptValue};
170    ///
171    /// let f = PluginFn::new("gpio_read")
172    ///     .param("pin", ScriptTy::Int)
173    ///     .ret(ScriptTy::Bool)
174    ///     .doc("Read a GPIO pin.")
175    ///     .build(|cx| Ok(ScriptValue::Bool(cx.int_arg(0) % 2 == 0)));
176    /// ```
177    // A `PluginFn` is not complete until it has a body, so the entry point
178    // hands back the builder that collects one.
179    #[allow(clippy::new_ret_no_self)]
180    pub fn new(name: impl Into<String>) -> PluginFnBuilder {
181        PluginFnBuilder {
182            name: name.into(),
183            ns: ScriptNs::Extension,
184            sig: ScriptSig::default(),
185        }
186    }
187
188    /// The name the script calls it by.
189    pub fn name(&self) -> &str {
190        &self.name
191    }
192
193    /// Run the body over one call.
194    pub(crate) fn invoke(&self, cx: &mut Cx<'_>) -> Result<ScriptValue, String> {
195        (self.body)(cx)
196    }
197
198    /// How this function appears in the manifest.
199    fn decl(&self) -> FnDecl {
200        FnDecl {
201            name: self.name.clone(),
202            ns: self.ns.clone(),
203            sig: self.sig.clone(),
204        }
205    }
206}
207
208/// Builder for a [`PluginFn`]. Start it with [`PluginFn::new`].
209pub struct PluginFnBuilder {
210    name: String,
211    ns: ScriptNs,
212    sig: ScriptSig,
213}
214
215impl PluginFnBuilder {
216    /// Append a required parameter. Call in argument order.
217    #[must_use]
218    pub fn param(mut self, name: impl Into<String>, ty: ScriptTy) -> Self {
219        self.sig.params.push(ScriptParam {
220            name: name.into(),
221            ty,
222        });
223        self.sig.min_arity = self.sig.params.len();
224        self
225    }
226
227    /// Set the return type. Defaults to [`ScriptTy::Any`]; a function whose
228    /// result really is open declares [`ScriptTy::Dynamic`].
229    #[must_use]
230    pub fn ret(mut self, ty: ScriptTy) -> Self {
231        self.sig.ret = ty;
232        self
233    }
234
235    /// Set the one-line description editor tooling shows.
236    #[must_use]
237    pub fn doc(mut self, doc: impl Into<String>) -> Self {
238        self.sig.doc = doc.into();
239        self
240    }
241
242    /// Choose the namespace. Defaults to [`ScriptNs::Extension`].
243    /// [`ScriptNs::Builtin`] is the runtime's own surface and a host refuses
244    /// a manifest that claims it.
245    #[must_use]
246    pub fn ns(mut self, ns: ScriptNs) -> Self {
247        self.ns = ns;
248        self
249    }
250
251    /// Make the trailing parameters optional: a call may pass as few as
252    /// `min_arity` arguments, and the body reads the rest as
253    /// [`ScriptValue::Unit`].
254    #[must_use]
255    pub fn min_arity(mut self, min_arity: usize) -> Self {
256        self.sig.min_arity = min_arity;
257        self
258    }
259
260    /// Accept arguments past the declared parameters.
261    #[must_use]
262    pub fn variadic(mut self) -> Self {
263        self.sig.variadic = true;
264        self
265    }
266
267    /// Finish with the body.
268    ///
269    /// The body returns `Ok(value)`, or `Err(message)` to raise `message` in
270    /// the script that called it.
271    pub fn build<F>(self, body: F) -> PluginFn
272    where
273        F: Fn(&mut Cx<'_>) -> Result<ScriptValue, String> + Send + Sync + 'static,
274    {
275        PluginFn {
276            name: self.name,
277            ns: self.ns,
278            sig: self.sig,
279            body: Box::new(body),
280        }
281    }
282}
283
284/// The call a [`PluginFn`] body receives: its arguments, the sink it emits
285/// commands into, and the host it can reach.
286///
287/// Mirrors `lumen_script::ScriptFnCx`, which is what a body would receive if
288/// the same function were registered in-process.
289pub struct Cx<'a> {
290    args: &'a [ScriptValue],
291    out: &'a mut Vec<ScriptCommand>,
292    host: Host,
293}
294
295impl<'a> Cx<'a> {
296    /// Wrap the arguments of one call, the scratch its commands land in, and
297    /// the host it may call back into.
298    pub fn new(args: &'a [ScriptValue], out: &'a mut Vec<ScriptCommand>, host: Host) -> Self {
299        Self { args, out, host }
300    }
301
302    /// Every argument the script passed.
303    pub fn args(&self) -> &[ScriptValue] {
304        self.args
305    }
306
307    /// Argument `i`, or [`ScriptValue::Unit`] when the call passed fewer.
308    pub fn arg(&self, i: usize) -> ScriptValue {
309        self.arg_ref(i).clone()
310    }
311
312    /// Argument `i` by reference, for a body that only reads it.
313    pub fn arg_ref(&self, i: usize) -> &ScriptValue {
314        self.args.get(i).unwrap_or(&ScriptValue::Unit)
315    }
316
317    /// Argument `i` as a string. Non-strings take their canonical rendering;
318    /// a missing argument is the empty string.
319    pub fn str_arg(&self, i: usize) -> String {
320        self.args
321            .get(i)
322            .map(ScriptValue::stringify)
323            .unwrap_or_default()
324    }
325
326    /// Argument `i` as an integer. Floats truncate, numeric strings parse,
327    /// and anything else (including a missing argument) is `0`.
328    pub fn int_arg(&self, i: usize) -> i64 {
329        match self.args.get(i) {
330            Some(ScriptValue::I64(v)) => *v,
331            Some(ScriptValue::F64(v)) => *v as i64,
332            Some(ScriptValue::Bool(b)) => i64::from(*b),
333            Some(ScriptValue::Str(s)) => s.trim().parse().unwrap_or(0),
334            _ => 0,
335        }
336    }
337
338    /// Argument `i` as a float. Integers widen, numeric strings parse, and
339    /// anything else (including a missing argument) is `0.0`.
340    pub fn float_arg(&self, i: usize) -> f64 {
341        match self.args.get(i) {
342            Some(ScriptValue::F64(v)) => *v,
343            Some(ScriptValue::I64(v)) => *v as f64,
344            Some(ScriptValue::Str(s)) => s.trim().parse().unwrap_or(0.0),
345            _ => 0.0,
346        }
347    }
348
349    /// Argument `i` as a boolean. `0` and `""` are false, other numbers and
350    /// non-empty strings other than `"false"` are true, and a missing
351    /// argument is false.
352    pub fn bool_arg(&self, i: usize) -> bool {
353        match self.args.get(i) {
354            Some(ScriptValue::Bool(b)) => *b,
355            Some(ScriptValue::I64(v)) => *v != 0,
356            Some(ScriptValue::F64(v)) => *v != 0.0,
357            Some(ScriptValue::Str(s)) => !s.is_empty() && s != "false" && s != "0",
358            _ => false,
359        }
360    }
361
362    /// Queue a command. The engine applies it on the tick the call happened,
363    /// even when the call goes on to fail.
364    pub fn emit(&mut self, cmd: ScriptCommand) {
365        self.out.push(cmd);
366    }
367
368    /// The host, for the effects that outlive the call.
369    pub fn host(&self) -> &Host {
370        &self.host
371    }
372}
373
374/// The engine, as a plugin can reach it.
375///
376/// Cheap to clone and usable from any thread, so a plugin keeps one in a
377/// worker and pushes at the app whenever it has something. Nothing here
378/// blocks on the tick thread: a call queues its payload and returns.
379#[derive(Clone, Copy)]
380pub struct Host {
381    inner: &'static HostInner,
382}
383
384/// The vtable entries, copied out of the host's table at init.
385pub(crate) struct HostInner {
386    ctx: *mut std::ffi::c_void,
387    emit_event: Option<unsafe extern "C" fn(*mut std::ffi::c_void, *const u8, usize) -> i32>,
388    log: Option<unsafe extern "C" fn(*mut std::ffi::c_void, i32, *const u8, usize)>,
389    wake: Option<unsafe extern "C" fn(*mut std::ffi::c_void)>,
390}
391
392// The context pointer is opaque host state that lives for the process, and
393// every entry above is documented as callable from any thread.
394unsafe impl Send for HostInner {}
395unsafe impl Sync for HostInner {}
396
397impl HostInner {
398    /// A host that offers nothing. What a plugin sees when the loader passed
399    /// no table at all, which no Lumen release does.
400    pub(crate) const fn disconnected() -> Self {
401        Self {
402            ctx: std::ptr::null_mut(),
403            emit_event: None,
404            log: None,
405            wake: None,
406        }
407    }
408
409    /// Copy the entries out of a host's table, taking only the fields its
410    /// `struct_size` says are there.
411    ///
412    /// # Safety
413    /// `table`, when non-null, points at a `HostVtable` whose leading `u32`
414    /// is the size of the struct the host built.
415    pub(crate) unsafe fn from_vtable(table: *const HostVtable) -> Self {
416        if table.is_null() {
417            return Self::disconnected();
418        }
419        let size = unsafe { table.cast::<u32>().read() } as usize;
420        /// Read one field, when the host's `struct_size` covers it.
421        ///
422        /// # Safety
423        /// `offset` is that field's offset in `HostVtable`, so the read is
424        /// aligned and in bounds once the size check passes.
425        unsafe fn field<T: Copy>(
426            table: *const HostVtable,
427            size: usize,
428            offset: usize,
429        ) -> Option<T> {
430            (offset + std::mem::size_of::<T>() <= size)
431                .then(|| unsafe { table.cast::<u8>().add(offset).cast::<T>().read() })
432        }
433        unsafe {
434            Self {
435                ctx: field(table, size, std::mem::offset_of!(HostVtable, ctx))
436                    .unwrap_or(std::ptr::null_mut()),
437                emit_event: field(table, size, std::mem::offset_of!(HostVtable, emit_event))
438                    .flatten(),
439                log: field(table, size, std::mem::offset_of!(HostVtable, log)).flatten(),
440                wake: field(table, size, std::mem::offset_of!(HostVtable, wake)).flatten(),
441            }
442        }
443    }
444}
445
446impl Host {
447    /// Wrap the entries the loader handed this plugin.
448    pub(crate) fn new(inner: &'static HostInner) -> Self {
449        Self { inner }
450    }
451
452    /// A host that takes nothing: what a plugin holds when it was built
453    /// outside a load, and what every call reports failure through.
454    pub(crate) fn disconnected() -> Self {
455        static DISCONNECTED: HostInner = HostInner::disconnected();
456        Self::new(&DISCONNECTED)
457    }
458
459    /// Call a handler in the app's script.
460    ///
461    /// `event` is the handler name, `key` the identifier it receives so one
462    /// handler can serve several sources, and `fallback` a handler to try
463    /// when the app defines no `event` (empty for none).
464    ///
465    /// Returns false when the engine is no longer taking events, which is
466    /// what a plugin's worker thread sees while the app shuts down.
467    pub fn call_handler(
468        &self,
469        event: &str,
470        key: &str,
471        fallback: &str,
472        args: Vec<ScriptValue>,
473    ) -> bool {
474        self.send(PluginEvent::Call {
475            event: event.to_string(),
476            key: key.to_string(),
477            fallback: fallback.to_string(),
478            args,
479        })
480    }
481
482    /// Apply commands, as if a call had emitted them.
483    ///
484    /// Returns false when the engine is no longer taking events.
485    pub fn emit(&self, commands: Vec<ScriptCommand>) -> bool {
486        self.send(PluginEvent::Commands(commands))
487    }
488
489    /// Write a line to the engine's diagnostic output.
490    pub fn log(&self, level: LogLevel, message: &str) {
491        let Some(log) = self.inner.log else {
492            return;
493        };
494        unsafe {
495            log(
496                self.inner.ctx,
497                level.into(),
498                message.as_ptr(),
499                message.len(),
500            )
501        };
502    }
503
504    /// Encode an event, hand it over, and ask for a tick to drain it. The
505    /// wake is not the caller's to remember: an event delivered to a
506    /// sleeping app that is never woken arrives whenever the next input
507    /// does, which reads as a plugin that sometimes works.
508    fn send(&self, event: PluginEvent) -> bool {
509        let (Some(emit), Ok(bytes)) = (self.inner.emit_event, codec::encode(&event)) else {
510            return false;
511        };
512        let status = unsafe { emit(self.inner.ctx, bytes.as_ptr(), bytes.len()) };
513        if status != abi::OK {
514            return false;
515        }
516        if let Some(wake) = self.inner.wake {
517            unsafe { wake(self.inner.ctx) };
518        }
519        true
520    }
521}
522
523/// A failure inside a plugin. The message reaches the user prefixed with the
524/// module's name.
525#[derive(Debug, Clone, Serialize, Deserialize)]
526pub struct Error {
527    /// What went wrong.
528    pub message: String,
529}
530
531// Blanket over Display so `?` works on any error in a plugin body. This
532// compiles only while `Error` itself implements neither Display nor
533// std::error::Error; adding either collides with core's reflexive
534// `From<T> for T`. If that trade ever needs reversing, replace this with
535// explicit From impls for the common error types.
536impl<E: std::fmt::Display> From<E> for Error {
537    fn from(e: E) -> Self {
538        Error {
539            message: e.to_string(),
540        }
541    }
542}
543
544#[cfg(test)]
545mod tests {
546    use super::*;
547
548    fn host() -> Host {
549        Host::disconnected()
550    }
551
552    #[test]
553    fn a_builder_describes_what_it_was_told_and_defaults_the_rest() {
554        let f = PluginFn::new("gpio_read")
555            .param("pin", ScriptTy::Int)
556            .ret(ScriptTy::Bool)
557            .doc("Read a GPIO pin.")
558            .ns(ScriptNs::Named("gpio".to_string()))
559            .min_arity(0)
560            .variadic()
561            .build(|cx| Ok(ScriptValue::Bool(cx.int_arg(0) > 0)));
562
563        let decl = f.decl();
564        assert_eq!(decl.name, "gpio_read");
565        assert_eq!(decl.ns, ScriptNs::Named("gpio".to_string()));
566        assert_eq!(decl.sig.params[0].ty, ScriptTy::Int);
567        assert_eq!(decl.sig.ret, ScriptTy::Bool);
568        assert_eq!(decl.sig.doc, "Read a GPIO pin.");
569        assert_eq!(decl.sig.min_arity, 0);
570        assert!(decl.sig.variadic);
571
572        let default = PluginFn::new("plain")
573            .build(|_| Ok(ScriptValue::Unit))
574            .decl();
575        assert_eq!(default.ns, ScriptNs::Extension);
576    }
577
578    #[test]
579    fn the_registrar_keeps_registration_order_and_declares_no_capabilities() {
580        let mut r = Registrar::default();
581        r.script_fn(PluginFn::new("a").build(|_| Ok(ScriptValue::Unit)))
582            .script_fn(PluginFn::new("b").build(|_| Ok(ScriptValue::Unit)))
583            .prelude("candela", "gpio", "fn wrap() {}");
584
585        let manifest = r.manifest();
586        let names: Vec<&str> = manifest.fns.iter().map(|f| f.name.as_str()).collect();
587        assert_eq!(names, ["a", "b"]);
588        assert_eq!(manifest.preludes[0].lang, "candela");
589        assert_eq!(manifest.preludes[0].ns, "gpio");
590        assert!(manifest.capabilities.is_empty());
591        assert_eq!(r.take_fns().len(), 2);
592        assert!(r.manifest().fns.is_empty());
593    }
594
595    #[test]
596    fn the_call_context_coerces_its_arguments() {
597        let mut out = Vec::new();
598        let args = [
599            ScriptValue::Str("7".into()),
600            ScriptValue::F64(2.5),
601            ScriptValue::Bool(true),
602        ];
603        let mut cx = Cx::new(&args, &mut out, host());
604        assert_eq!(cx.int_arg(0), 7);
605        assert_eq!(cx.float_arg(1), 2.5);
606        assert_eq!(cx.int_arg(1), 2);
607        assert!(cx.bool_arg(2));
608        assert_eq!(cx.str_arg(1), "2.5");
609        assert_eq!(cx.arg(9), ScriptValue::Unit);
610        assert_eq!(cx.args().len(), 3);
611        assert!(!cx.bool_arg(9));
612        assert_eq!(cx.int_arg(9), 0);
613        assert_eq!(cx.float_arg(9), 0.0);
614        assert_eq!(cx.str_arg(9), "");
615
616        cx.emit(ScriptCommand::Print("hi".into()));
617        assert_eq!(out.len(), 1);
618    }
619
620    #[test]
621    fn a_disconnected_host_reports_that_nothing_was_delivered() {
622        let host = host();
623        assert!(!host.call_handler("on_tick", "k", "", Vec::new()));
624        assert!(!host.emit(vec![ScriptCommand::Print("x".into())]));
625        // Nothing to log through, and saying so must not be a panic.
626        host.log(LogLevel::Info, "ignored");
627    }
628
629    #[test]
630    fn a_short_host_table_yields_only_the_fields_it_covers() {
631        unsafe extern "C" fn emit(_ctx: *mut std::ffi::c_void, _p: *const u8, _l: usize) -> i32 {
632            abi::OK
633        }
634        let table = HostVtable {
635            // Everything past `emit_event` is what a host built against an
636            // earlier ABI would not have.
637            struct_size: (std::mem::offset_of!(HostVtable, emit_event)
638                + std::mem::size_of::<usize>()) as u32,
639            _pad: 0,
640            ctx: std::ptr::null_mut(),
641            emit_event: Some(emit),
642            log: None,
643            wake: None,
644        };
645        let inner = unsafe { HostInner::from_vtable(&table) };
646        assert!(inner.emit_event.is_some());
647        assert!(inner.log.is_none() && inner.wake.is_none());
648
649        let none = unsafe { HostInner::from_vtable(std::ptr::null()) };
650        assert!(none.emit_event.is_none());
651    }
652
653    #[test]
654    fn any_display_type_converts_into_error() {
655        assert_eq!(Error::from("plain").message, "plain");
656        assert_eq!(
657            Error::from(std::fmt::Error).message,
658            std::fmt::Error.to_string()
659        );
660    }
661}