Skip to main content

lumen_plugin_abi/
raw.rs

1//! The C scalars every Lumen plugin boundary is built from.
2//!
3//! Everything that crosses a plugin boundary is bytes and C scalars: hook
4//! payloads travel as bincode buffers (see [`crate::codec`]), never as Rust
5//! types, so a plugin built by any compiler works against any host built from
6//! the same release tag. What differs between the plugin systems is the
7//! descriptor that names the hooks; the buffer, the hook signature, and the
8//! status codes below are the same in all of them.
9
10use std::mem::ManuallyDrop;
11
12/// A byte buffer allocated by the plugin. The host reads it, then returns it
13/// through the plugin's own free function; it never frees plugin memory with
14/// its own allocator.
15#[repr(C)]
16pub struct Buf {
17    pub ptr: *mut u8,
18    pub len: usize,
19    pub cap: usize,
20}
21
22impl Buf {
23    /// An empty buffer, the state a hook receives `out` in.
24    pub const fn empty() -> Self {
25        Buf {
26            ptr: std::ptr::null_mut(),
27            len: 0,
28            cap: 0,
29        }
30    }
31}
32
33/// One hook entry point. `input`/`ctx` are borrowed for the call; on return
34/// the status code says what `out` holds.
35pub type HookFn = unsafe extern "C" fn(
36    input: *const u8,
37    input_len: usize,
38    ctx: *const u8,
39    ctx_len: usize,
40    out: *mut Buf,
41) -> i32;
42
43/// Frees a buffer previously returned by any hook of the same plugin.
44pub type FreeFn = unsafe extern "C" fn(ptr: *mut u8, len: usize, cap: usize);
45
46/// Set in a descriptor's `flags` by a plugin built with `panic = "abort"`.
47/// The host refuses such a plugin at load: the panic-to-error contract
48/// depends on unwinding, and an aborting plugin would kill the host on any
49/// hook panic.
50pub const FLAG_PANIC_ABORT: u16 = 1;
51
52/// `out` holds the hook's payload.
53pub const OK: i32 = 0;
54/// `out` holds a UTF-8 error message; the call fails with it.
55pub const ERR: i32 = 1;
56/// The hook panicked; `out` holds the panic message.
57pub const PANICKED: i32 = 2;
58/// `out` is empty; the host keeps the input unchanged.
59pub const UNCHANGED: i32 = 3;
60
61/// Move a byte vector across the boundary. The host returns it through
62/// [`free_buf`].
63pub fn fill(out: &mut Buf, bytes: Vec<u8>) {
64    let mut v = ManuallyDrop::new(bytes);
65    out.ptr = v.as_mut_ptr();
66    out.len = v.len();
67    out.cap = v.capacity();
68}
69
70/// The `free` entry every generated descriptor carries: rebuilds the vector
71/// [`fill`] leaked and drops it.
72///
73/// # Safety
74/// `ptr`/`len`/`cap` must be exactly the triple a hook of this plugin
75/// returned, unfreed.
76pub unsafe extern "C" fn free_buf(ptr: *mut u8, len: usize, cap: usize) {
77    if !ptr.is_null() {
78        unsafe { drop(Vec::from_raw_parts(ptr, len, cap)) };
79    }
80}
81
82/// The message a caught panic crosses the boundary as. `panic!` payloads are
83/// `&str` or `String`; anything else has no text to report.
84pub fn panic_message(payload: &(dyn std::any::Any + Send)) -> String {
85    payload
86        .downcast_ref::<&str>()
87        .map(|s| s.to_string())
88        .or_else(|| payload.downcast_ref::<String>().cloned())
89        .unwrap_or_else(|| "non-string panic payload".to_string())
90}
91
92#[cfg(test)]
93mod tests {
94    use super::*;
95
96    #[test]
97    fn a_filled_buffer_round_trips_through_free() {
98        let mut buf = Buf::empty();
99        assert!(buf.ptr.is_null());
100        fill(&mut buf, b"payload".to_vec());
101        assert_eq!(buf.len, 7);
102        let copied = unsafe { std::slice::from_raw_parts(buf.ptr, buf.len) }.to_vec();
103        assert_eq!(copied, b"payload");
104        unsafe { free_buf(buf.ptr, buf.len, buf.cap) };
105    }
106
107    #[test]
108    fn freeing_a_null_buffer_is_a_no_op() {
109        unsafe { free_buf(std::ptr::null_mut(), 0, 0) };
110    }
111
112    #[test]
113    fn panic_payloads_report_their_text_or_say_they_have_none() {
114        let str_payload = std::panic::catch_unwind(|| panic!("literal")).unwrap_err();
115        assert_eq!(panic_message(str_payload.as_ref()), "literal");
116
117        let owned = std::panic::catch_unwind(|| panic!("{}", "formatted")).unwrap_err();
118        assert_eq!(panic_message(owned.as_ref()), "formatted");
119
120        let other = std::panic::catch_unwind(|| std::panic::panic_any(42usize)).unwrap_err();
121        assert_eq!(panic_message(other.as_ref()), "non-string panic payload");
122    }
123}