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}