Skip to main content

lumen_plugin/
export.rs

1//! The export side: [`lumen_plugin!`] and the support code it calls.
2//!
3//! The macro generates the whole C-ABI surface for a plugin crate; authors
4//! write no unsafe code and no FFI. All logic lives in the dispatch functions
5//! below, compiled into the plugin cdylib through this crate, so the
6//! generated code is a handful of one-line thunks.
7
8use std::panic::{AssertUnwindSafe, catch_unwind};
9use std::sync::OnceLock;
10
11use lumen_plugin_abi::raw::{fill, panic_message};
12
13use crate::abi::{Buf, ERR, HostVtable, OK, PANICKED};
14use crate::wire::{Call, CallOut, InitCx};
15use crate::{Cx, Host, HostInner, PluginFn, Registrar, RuntimePlugin, codec};
16
17/// The `free` entry every generated descriptor carries.
18#[doc(hidden)]
19pub use lumen_plugin_abi::raw::free_buf;
20
21/// What init left behind for the calls that follow it. One per loaded
22/// library: a second library, even a copy of the same file, links its own
23/// copy of this crate and gets its own.
24static REGISTERED: OnceLock<Registered> = OnceLock::new();
25
26struct Registered {
27    fns: Vec<PluginFn>,
28    host: &'static HostInner,
29}
30
31/// The init dispatch: read the context, let the plugin register, and answer
32/// with the manifest.
33fn init(plugin: &dyn RuntimePlugin, ctx: &[u8], host: *const HostVtable) -> (i32, Vec<u8>) {
34    if REGISTERED.get().is_some() {
35        return (
36            ERR,
37            b"already initialized; a runtime plugin holds one instance per process, and this \
38              library was loaded more than once"
39                .to_vec(),
40        );
41    }
42    let cx: InitCx = match codec::decode(ctx) {
43        Ok(c) => c,
44        Err(e) => return (ERR, format!("context decode: {e}").into_bytes()),
45    };
46    // Leaked rather than stored in a `OnceLock` beside the registration: a
47    // `Host` handed to a plugin's worker thread outlives every borrow the
48    // engine could hand back, and the table it wraps is process-lifetime on
49    // the host side too.
50    let host: &'static HostInner = Box::leak(Box::new(unsafe { HostInner::from_vtable(host) }));
51    let mut registrar = Registrar::new(Host::new(host));
52    if let Err(e) = plugin.register(&mut registrar, &cx) {
53        return (ERR, e.message.into_bytes());
54    }
55    let manifest = registrar.manifest();
56    let _ = REGISTERED.set(Registered {
57        fns: registrar.take_fns(),
58        host,
59    });
60    match codec::encode(&manifest) {
61        Ok(bytes) => (OK, bytes),
62        Err(e) => (ERR, format!("manifest encode: {e}").into_bytes()),
63    }
64}
65
66/// The call dispatch: run one registered function and answer with what it
67/// returned and emitted.
68fn call(input: &[u8]) -> (i32, Vec<u8>) {
69    let Some(registered) = REGISTERED.get() else {
70        return (ERR, b"called before init".to_vec());
71    };
72    let call: Call = match codec::decode(input) {
73        Ok(c) => c,
74        Err(e) => return (ERR, format!("call decode: {e}").into_bytes()),
75    };
76    let Some(f) = registered.fns.get(call.index as usize) else {
77        return (
78            ERR,
79            format!(
80                "no function at index {}; this plugin registered {}",
81                call.index,
82                registered.fns.len()
83            )
84            .into_bytes(),
85        );
86    };
87    let mut commands = Vec::new();
88    let ret = {
89        let mut cx = Cx::new(&call.args, &mut commands, Host::new(registered.host));
90        f.invoke(&mut cx)
91    };
92    match codec::encode(&CallOut { ret, commands }) {
93        Ok(bytes) => (OK, bytes),
94        // The one payload a body can carry that has no encoding is a
95        // `ScriptCommand::SetProperty` holding a `PropertyValue::Custom`,
96        // which cannot be built from data that crossed the boundary.
97        Err(e) => (
98            ERR,
99            format!("{}: result encode: {e}", f.name()).into_bytes(),
100        ),
101    }
102}
103
104/// The body every generated init thunk calls.
105///
106/// A panic in the plugin is caught here, on the plugin side of the boundary,
107/// because unwinding out of an `extern "C"` function aborts the process.
108/// This is also why a plugin must not be built with `panic = "abort"`: that
109/// setting removes the unwind this catch depends on, and any panic kills the
110/// app instead of failing the plugin.
111///
112/// # Safety
113/// `ctx` must be valid for reads of `ctx_len`; `host`, when non-null, must
114/// point at a live [`HostVtable`]; `out` must be valid for writes.
115#[doc(hidden)]
116pub unsafe fn init_entry(
117    plugin: fn() -> &'static dyn RuntimePlugin,
118    ctx: *const u8,
119    ctx_len: usize,
120    host: *const HostVtable,
121    out: *mut Buf,
122) -> i32 {
123    let ctx = unsafe { std::slice::from_raw_parts(ctx, ctx_len) };
124    // The getter runs inside the catch too: first-call construction is a
125    // plugin-authored code path and a panicking constructor must fail the
126    // module's load, not abort the app.
127    let (status, bytes) = match catch_unwind(AssertUnwindSafe(|| init(plugin(), ctx, host))) {
128        Ok(r) => r,
129        Err(payload) => (PANICKED, panic_message(payload.as_ref()).into_bytes()),
130    };
131    if !bytes.is_empty() {
132        fill(unsafe { &mut *out }, bytes);
133    }
134    status
135}
136
137/// The body every generated call thunk calls. See [`init_entry`] for why the
138/// panic is caught here.
139///
140/// # Safety
141/// `input` must be valid for reads of `input_len`; `out` must be valid for
142/// writes.
143#[doc(hidden)]
144pub unsafe fn call_entry(input: *const u8, input_len: usize, out: *mut Buf) -> i32 {
145    let input = unsafe { std::slice::from_raw_parts(input, input_len) };
146    let (status, bytes) = match catch_unwind(AssertUnwindSafe(|| call(input))) {
147        Ok(r) => r,
148        Err(payload) => (PANICKED, panic_message(payload.as_ref()).into_bytes()),
149    };
150    if !bytes.is_empty() {
151        fill(unsafe { &mut *out }, bytes);
152    }
153    status
154}
155
156/// The body every generated shutdown thunk calls. A plugin whose init never
157/// succeeded is never constructed here, and a panic on the way down is
158/// swallowed: there is no call left to fail.
159#[doc(hidden)]
160pub fn shutdown_entry(plugin: fn() -> &'static dyn RuntimePlugin) {
161    if REGISTERED.get().is_none() {
162        return;
163    }
164    let _ = catch_unwind(AssertUnwindSafe(|| plugin().shutdown()));
165}
166
167/// Export a [`RuntimePlugin`](crate::RuntimePlugin) from a cdylib crate.
168///
169/// Takes an expression that constructs the plugin; it runs once, when the
170/// engine initializes the module:
171///
172/// ```ignore
173/// lumen_plugin::lumen_plugin!(MyPlugin::default);
174/// ```
175///
176/// The plugin crate must keep the default `panic = "unwind"`; see
177/// [`init_entry`] for why.
178#[macro_export]
179macro_rules! lumen_plugin {
180    ($ctor:expr) => {
181        const _: () = {
182            static INSTANCE: ::std::sync::OnceLock<::std::boxed::Box<dyn $crate::RuntimePlugin>> =
183                ::std::sync::OnceLock::new();
184
185            fn instance() -> &'static dyn $crate::RuntimePlugin {
186                INSTANCE
187                    .get_or_init(|| ::std::boxed::Box::new(($ctor)()))
188                    .as_ref()
189            }
190
191            unsafe extern "C" fn init_thunk(
192                ctx: *const u8,
193                ctx_len: usize,
194                host: *const $crate::abi::HostVtable,
195                out: *mut $crate::abi::Buf,
196            ) -> i32 {
197                unsafe { $crate::export::init_entry(instance, ctx, ctx_len, host, out) }
198            }
199
200            unsafe extern "C" fn call_thunk(
201                input: *const u8,
202                input_len: usize,
203                _ctx: *const u8,
204                _ctx_len: usize,
205                out: *mut $crate::abi::Buf,
206            ) -> i32 {
207                unsafe { $crate::export::call_entry(input, input_len, out) }
208            }
209
210            unsafe extern "C" fn shutdown_thunk() {
211                $crate::export::shutdown_entry(instance);
212            }
213
214            static DESC: $crate::abi::Desc = $crate::abi::Desc {
215                abi_version: $crate::abi::ABI_VERSION,
216                struct_size: ::std::mem::size_of::<$crate::abi::Desc>() as u32,
217                script_wire_version: $crate::SCRIPT_WIRE_VERSION,
218                flags: if ::std::cfg!(panic = "abort") {
219                    $crate::abi::FLAG_PANIC_ABORT
220                } else {
221                    0
222                },
223                reserved: 0,
224                name: concat!(env!("CARGO_PKG_NAME"), "\0").as_ptr()
225                    as *const ::std::os::raw::c_char,
226                version: concat!(env!("CARGO_PKG_VERSION"), "\0").as_ptr()
227                    as *const ::std::os::raw::c_char,
228                init: Some(init_thunk),
229                call: Some(call_thunk),
230                shutdown: Some(shutdown_thunk),
231                free: Some($crate::export::free_buf),
232            };
233
234            #[unsafe(no_mangle)]
235            pub extern "C" fn lumen_plugin_v1() -> *const $crate::abi::Desc {
236                &DESC
237            }
238        };
239    };
240}