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}