Skip to main content

confium_api/
handle.rs

1//! Opaque handle helpers for boxing/unboxing Rust plugin state behind
2//! the type-erased `*mut c_void` plugin contract.
3//!
4//! Confium plugins expose their per-instance state through opaque pointers
5//! (e.g. `*mut FFIHash`). The loader never inspects the pointee — it just
6//! hands the pointer back to the plugin on every call. Conventionally a
7//! plugin author writes `Box::into_raw(Box::new(state)) as *mut c_void`
8//! in `create` and `Box::from_raw(ptr)` in `destroy`, sprinkling `unsafe`
9//! through their code.
10//!
11//! [`OpaqueHandle`] wraps that pattern so plugin authors can stay in safe
12//! Rust for the lifetime of the handle:
13//!
14//! ```
15//! use confium_api::OpaqueHandle;
16//! #
17//! # struct MyHash { buf: Vec<u8> }
18//!
19//! // `create` returns an opaque pointer the loader will hand back.
20//! fn create() -> *mut std::ffi::c_void {
21//!     let state = MyHash { buf: Vec::new() };
22//!     OpaqueHandle::<MyHash>::new(state)
23//! }
24//!
25//! // Every other entry point borrows the live state.
26//! # // SAFETY: ptr was produced by `create` and is reclaimed exactly once.
27//! unsafe fn update(ptr: *mut std::ffi::c_void, data: &[u8]) {
28//!     let handle = OpaqueHandle::<MyHash>::borrow_raw(ptr);
29//!     handle.buf.extend_from_slice(data);
30//! }
31//!
32//! // `destroy` reclaims ownership and drops the state.
33//! # // SAFETY: ptr was produced by `create` and is reclaimed exactly once.
34//! unsafe fn destroy(ptr: *mut std::ffi::c_void) {
35//!     let _ = OpaqueHandle::<MyHash>::from_raw(ptr);
36//! }
37//! ```
38//!
39//! ## Safety contract
40//!
41//! The pointer returned by [`OpaqueHandle::into_raw`] is owned by the
42//! caller (typically the Confium loader) until the matching `destroy`
43//! symbol is invoked. Borrowing it from any other thread, or calling
44//! `from_raw` more than once, is undefined behavior. Plugins are
45//! single-threaded with respect to a given instance handle in the v0
46//! contract.
47
48use std::ffi::c_void;
49use std::marker::PhantomData;
50
51/// Wrapper around a `Box<T>` that knows how to round-trip through a
52/// raw `*mut c_void` pointer without exposing `unsafe` at call sites.
53///
54/// See the module docs for the safety contract.
55pub struct OpaqueHandle<T: ?Sized> {
56    _marker: PhantomData<T>,
57}
58
59impl<T> OpaqueHandle<T> {
60    /// Box `value` and return it as an opaque raw pointer. The caller
61    /// owns the allocation; pass it to [`from_raw`](Self::from_raw) to
62    /// reclaim it.
63    #[allow(clippy::new_ret_no_self)] // intentional: returns a raw pointer
64    pub fn new(value: T) -> *mut c_void {
65        Box::into_raw(Box::new(value)) as *mut c_void
66    }
67
68    /// Reclaim a pointer produced by [`new`](Self::new) (or
69    /// [`into_raw`](Self::into_raw)) and drop the underlying value.
70    /// Calling this on a NULL pointer is a no-op. Calling it twice on
71    /// the same non-NULL pointer is undefined behavior.
72    ///
73    /// # Safety
74    ///
75    /// `ptr` must either be NULL or have been produced by
76    /// [`OpaqueHandle::new`] / [`OpaqueHandle::into_raw`] for the same
77    /// type `T`, and must not have been reclaimed already.
78    pub unsafe fn from_raw(ptr: *mut c_void) {
79        if ptr.is_null() {
80            return;
81        }
82        // SAFETY: upheld by the caller — the pointer was produced by
83        // `Box::into_raw(Box::new(value))` for this exact `T`, and is
84        // being reclaimed exactly once.
85        unsafe { drop(Box::from_raw(ptr as *mut T)) };
86    }
87
88    /// Take ownership of `value`, box it, and yield the raw pointer.
89    /// Equivalent to [`new`](Self::new) but reads better when you already
90    /// have a value to hand off.
91    pub fn into_raw(value: T) -> *mut c_void {
92        Self::new(value)
93    }
94}
95
96impl<T: ?Sized> OpaqueHandle<T> {
97    /// Borrow the live state behind `ptr` for the duration of the
98    /// returned reference. The pointer is **not** reclaimed — the
99    /// allocation remains owned by whoever holds it (typically the
100    /// Confium loader, until `destroy` is called).
101    ///
102    /// The returned `&mut T` borrow is tied to the lifetime `'a` so
103    /// callers cannot accidentally outlive it through `Copy`/`Clone`.
104    ///
105    /// # Safety
106    ///
107    /// `ptr` must be a non-NULL pointer produced by
108    /// [`OpaqueHandle::new`] / [`OpaqueHandle::into_raw`] for a type
109    /// that is layout-compatible with `T`, and must remain valid for
110    /// the duration of `'a`. The v0 plugin contract guarantees this:
111    /// instance handles are single-threaded and live until `destroy`.
112    pub unsafe fn borrow_raw<'a>(ptr: *mut c_void) -> &'a mut T
113    where
114        T: Sized,
115    {
116        debug_assert!(
117            !ptr.is_null(),
118            "OpaqueHandle::borrow_raw on a NULL pointer is a plugin bug"
119        );
120        // SAFETY: upheld by the caller.
121        unsafe { &mut *(ptr as *mut T) }
122    }
123}
124
125#[cfg(test)]
126mod tests {
127    use super::*;
128
129    #[test]
130    fn roundtrips_a_value() {
131        let ptr = OpaqueHandle::new(42u32);
132        let borrowed = unsafe { OpaqueHandle::<u32>::borrow_raw(ptr) };
133        assert_eq!(*borrowed, 42);
134        *borrowed = 7;
135        let borrowed2 = unsafe { OpaqueHandle::<u32>::borrow_raw(ptr) };
136        assert_eq!(*borrowed2, 7);
137        unsafe {
138            OpaqueHandle::<u32>::from_raw(ptr);
139        }
140    }
141
142    #[test]
143    fn from_raw_on_null_is_a_noop() {
144        unsafe {
145            OpaqueHandle::<u32>::from_raw(std::ptr::null_mut());
146        }
147    }
148
149    #[test]
150    fn roundtrips_a_struct() {
151        struct State {
152            count: u32,
153            label: String,
154        }
155        let ptr = OpaqueHandle::new(State {
156            count: 0,
157            label: "hash".to_string(),
158        });
159        let s = unsafe { OpaqueHandle::<State>::borrow_raw(ptr) };
160        s.count += 1;
161        assert_eq!(s.label, "hash");
162        assert_eq!(s.count, 1);
163        unsafe {
164            OpaqueHandle::<State>::from_raw(ptr);
165        }
166    }
167}