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}