Skip to main content

lumen_plugin/
abi.rs

1//! The C ABI a runtime plugin exports and the engine loads.
2//!
3//! Everything that crosses the boundary is bytes and C scalars: the init
4//! context, the manifest a plugin answers with, and every call travel as
5//! bincode buffers (see [`crate::codec`]), never as Rust types, so a plugin
6//! built by any compiler works against any engine built from the same release
7//! tag. Compatibility is enforced by the handshake fields on [`Desc`], not by
8//! hoping both sides agree.
9//!
10//! [`Desc`] and [`HostVtable`] are what make this the *runtime's* ABI; the
11//! buffer, the hook signature, and the status codes below come from
12//! `lumen_plugin_abi::raw` and mean the same thing at every Lumen plugin
13//! boundary.
14//!
15//! # Threading
16//!
17//! [`Desc::init`], [`Desc::call`], and [`Desc::shutdown`] run on the engine's
18//! tick thread, one at a time; the engine never calls into a plugin from a
19//! background thread and never re-enters it from inside a host callback. The
20//! [`HostVtable`] entries go the other way and have the opposite contract:
21//! they are callable from any thread at any time, including while a call is
22//! running, which is what lets a plugin own a worker thread.
23
24use std::ffi::c_void;
25use std::os::raw::c_char;
26
27pub use lumen_plugin_abi::raw::{
28    Buf, ERR, FLAG_PANIC_ABORT, FreeFn, HookFn, OK, PANICKED, UNCHANGED,
29};
30
31/// Version of this descriptor layout and of the SDK-owned wire shapes.
32/// Bump on any change to [`Desc`], [`HostVtable`], [`Buf`], the status codes,
33/// or any serde type this crate sends across the boundary (`InitCx`,
34/// `Manifest`, `Call`, `CallOut`); the `lumen_script` types - `PluginEvent`
35/// among them - are covered by [`Desc::script_wire_version`] instead.
36///
37/// 2: dropped `Desc::paint_wire_version` (no paint types cross the
38/// boundary), widening `reserved` in its place.
39pub const ABI_VERSION: u32 = 2;
40
41/// The one symbol a plugin cdylib exports:
42/// `unsafe extern "C" fn lumen_plugin_v1() -> *const Desc`.
43pub const ENTRY: &[u8] = b"lumen_plugin_v1\0";
44
45/// Bring a plugin up: hand it the init context and the host callbacks, and
46/// take back its manifest.
47///
48/// `ctx` is a bincode [`InitCx`](crate::InitCx) borrowed for the call. `host`
49/// points at a [`HostVtable`] that lives for the process. On [`OK`], `out`
50/// holds a bincode [`Manifest`](crate::Manifest); on [`ERR`] or [`PANICKED`]
51/// it holds a UTF-8 message.
52pub type InitFn = unsafe extern "C" fn(
53    ctx: *const u8,
54    ctx_len: usize,
55    host: *const HostVtable,
56    out: *mut Buf,
57) -> i32;
58
59/// The descriptor a plugin's entry function returns. Lives for the process;
60/// the host reads it after the handshake below.
61///
62/// `abi_version` and `struct_size` sit first and their offsets are frozen
63/// forever: the host reads them before it trusts anything else about the
64/// struct, so a future layout can still be refused with a clear error.
65#[repr(C)]
66pub struct Desc {
67    /// Must equal [`ABI_VERSION`].
68    pub abi_version: u32,
69    /// `size_of::<Desc>()` on the plugin side; a shorter value than the
70    /// host's own means a truncated or foreign struct.
71    pub struct_size: u32,
72    /// `lumen_script::SCRIPT_WIRE_VERSION` the plugin was built against. The
73    /// values, commands, and signatures it exchanges are only meaningful when
74    /// it matches the host's.
75    pub script_wire_version: u16,
76    /// Bit set of [`FLAG_PANIC_ABORT`] and future flags; zero otherwise.
77    pub flags: u16,
78    /// Zero. Keeps the pointer fields below 8-aligned on every target.
79    pub reserved: u32,
80    /// NUL-terminated UTF-8, `'static`. Must match the name the app declared
81    /// the module under.
82    pub name: *const c_char,
83    /// NUL-terminated UTF-8, `'static`.
84    pub version: *const c_char,
85    /// Required. Runs once, before any call.
86    pub init: Option<InitFn>,
87    /// Required when the manifest declares functions. `input` is a bincode
88    /// [`Call`](crate::Call), `ctx` is empty (the plugin already has its
89    /// context from init), and `out` is a bincode
90    /// [`CallOut`](crate::CallOut).
91    pub call: Option<HookFn>,
92    /// Optional. Runs once, when the app is going down.
93    pub shutdown: Option<unsafe extern "C" fn()>,
94    /// Required. Frees every buffer the entries above hand out.
95    pub free: Option<FreeFn>,
96}
97
98// The host stores a `&'static Desc` inside a Send + Sync set; the raw
99// pointers inside are 'static C strings the macro generates.
100unsafe impl Send for Desc {}
101unsafe impl Sync for Desc {}
102
103/// What the host offers a plugin: an opaque context pointer and the calls a
104/// plugin makes back into the engine.
105///
106/// The table and everything it points at live for the process. `ctx` is
107/// host-owned and opaque; a plugin passes it back verbatim and never frees
108/// it.
109///
110/// Every entry is callable from any thread, at any time, including
111/// concurrently with a call the plugin is already inside. None of them block
112/// on the tick thread, and the host never calls back into the plugin from
113/// inside one, so a plugin's worker thread can emit while the tick thread is
114/// running one of its functions.
115#[repr(C)]
116pub struct HostVtable {
117    /// `size_of::<HostVtable>()` on the host side. A plugin built against a
118    /// later ABI reads only the fields this covers.
119    pub struct_size: u32,
120    /// Zero. Keeps `ctx` 8-aligned on every target.
121    pub _pad: u32,
122    /// Opaque host state, passed back to every entry below.
123    pub ctx: *mut c_void,
124    /// Deliver a bincode [`PluginEvent`](crate::PluginEvent) to the engine.
125    /// Returns [`OK`] when the event was queued, [`ERR`] when the queue is
126    /// gone (the app is shutting down) or the payload did not decode.
127    pub emit_event: Option<unsafe extern "C" fn(*mut c_void, *const u8, usize) -> i32>,
128    /// Write a UTF-8 line to the engine's diagnostic output at `level`, one
129    /// of the [`LogLevel`] codes.
130    pub log: Option<unsafe extern "C" fn(*mut c_void, level: i32, *const u8, usize)>,
131    /// Ask the engine for another tick, so an event emitted from a worker
132    /// thread is drained without waiting for input.
133    pub wake: Option<unsafe extern "C" fn(*mut c_void)>,
134}
135
136/// How loud a line handed to [`HostVtable::log`] is.
137#[derive(Clone, Copy, Debug, PartialEq, Eq)]
138pub enum LogLevel {
139    /// Something failed and the plugin could not do what it was asked.
140    Error,
141    /// Something is wrong but the plugin carried on.
142    Warn,
143    /// Worth saying once.
144    Info,
145    /// Detail for someone debugging the plugin.
146    Debug,
147    /// Per-call detail.
148    Trace,
149}
150
151impl From<LogLevel> for i32 {
152    fn from(level: LogLevel) -> Self {
153        match level {
154            LogLevel::Error => 0,
155            LogLevel::Warn => 1,
156            LogLevel::Info => 2,
157            LogLevel::Debug => 3,
158            LogLevel::Trace => 4,
159        }
160    }
161}
162
163/// A code no version of this ABI defines reads as [`LogLevel::Info`]: a line
164/// from a plugin built against a later Lumen is still worth printing.
165impl From<i32> for LogLevel {
166    fn from(code: i32) -> Self {
167        match code {
168            0 => Self::Error,
169            1 => Self::Warn,
170            3 => Self::Debug,
171            4 => Self::Trace,
172            _ => Self::Info,
173        }
174    }
175}
176
177#[cfg(test)]
178mod tests {
179    use super::*;
180
181    #[test]
182    fn the_descriptor_keeps_its_pointers_aligned() {
183        assert_eq!(std::mem::offset_of!(Desc, abi_version), 0);
184        assert_eq!(std::mem::offset_of!(Desc, struct_size), 4);
185        assert_eq!(std::mem::offset_of!(Desc, name) % 8, 0);
186        assert_eq!(std::mem::offset_of!(HostVtable, ctx) % 8, 0);
187    }
188
189    #[test]
190    fn log_levels_round_trip_and_unknown_codes_read_as_info() {
191        for level in [
192            LogLevel::Error,
193            LogLevel::Warn,
194            LogLevel::Info,
195            LogLevel::Debug,
196            LogLevel::Trace,
197        ] {
198            assert_eq!(LogLevel::from(i32::from(level)), level);
199        }
200        assert_eq!(LogLevel::from(99), LogLevel::Info);
201        assert_eq!(LogLevel::from(-1), LogLevel::Info);
202    }
203}