Skip to main content

rpi_plugin_sdk/
handles.rs

1//! Safe storage for plugin-owned tool state behind opaque ABI handles.
2//!
3//! Handles are non-reused numeric tokens, never addresses to dereference.
4//! Each plugin owns its registry; Rust objects and locks stay on its side of
5//! the C ABI. Removing a handle cancels it and prevents future lookups, while
6//! in-flight callbacks retain their own `Arc` until they return.
7#![forbid(unsafe_code)]
8
9use crate::StepHandle;
10use std::collections::HashMap;
11use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
12use std::sync::{Arc, Mutex, MutexGuard};
13
14static NEXT_HANDLE: AtomicUsize = AtomicUsize::new(1);
15
16pub struct ToolHandles<T> {
17    entries: Mutex<Option<HashMap<usize, Arc<ToolState<T>>>>>,
18}
19
20pub struct ToolState<T> {
21    state: Mutex<T>,
22    pub cancelled: Arc<AtomicBool>,
23}
24
25impl<T> ToolState<T> {
26    /// Serialize mutations without holding the registry lock.
27    pub fn lock(&self) -> MutexGuard<'_, T> {
28        self.state.lock().unwrap_or_else(|p| p.into_inner())
29    }
30
31    /// Cancellation never waits for a potentially blocking poll.
32    pub fn cancel(&self) {
33        self.cancelled.store(true, Ordering::SeqCst);
34    }
35}
36
37impl<T> Default for ToolHandles<T> {
38    fn default() -> Self {
39        Self::new()
40    }
41}
42
43impl<T> ToolHandles<T> {
44    pub const fn new() -> Self {
45        Self {
46            entries: Mutex::new(None),
47        }
48    }
49
50    pub fn insert(&self, state: T) -> StepHandle {
51        self.insert_with_cancel(state, Arc::new(AtomicBool::new(false)))
52    }
53
54    /// Share the cancellation flag with a worker started by the plugin.
55    pub fn insert_with_cancel(&self, state: T, cancelled: Arc<AtomicBool>) -> StepHandle {
56        let token = NEXT_HANDLE
57            .fetch_update(Ordering::Relaxed, Ordering::Relaxed, |next| {
58                next.checked_add(1)
59            })
60            .expect("tool handle tokens exhausted");
61        let entry = Arc::new(ToolState {
62            state: Mutex::new(state),
63            cancelled,
64        });
65        self.entries
66            .lock()
67            .unwrap_or_else(|p| p.into_inner())
68            .get_or_insert_with(HashMap::new)
69            .insert(token, entry);
70        token as StepHandle
71    }
72
73    /// Unknown, null, removed, or another registry's handles return `None`.
74    pub fn get(&self, handle: StepHandle) -> Option<Arc<ToolState<T>>> {
75        self.entries
76            .lock()
77            .unwrap_or_else(|p| p.into_inner())
78            .as_ref()?
79            .get(&(handle as usize))
80            .cloned()
81    }
82
83    pub fn cancel(&self, handle: StepHandle) {
84        if let Some(entry) = self.get(handle) {
85            entry.cancel();
86        }
87    }
88
89    /// Idempotently retire the token. Drops happen outside the registry lock.
90    pub fn remove(&self, handle: StepHandle) -> Option<Arc<ToolState<T>>> {
91        let entry = self
92            .entries
93            .lock()
94            .unwrap_or_else(|p| p.into_inner())
95            .as_mut()?
96            .remove(&(handle as usize));
97        if let Some(entry) = &entry {
98            entry.cancel();
99        }
100        entry
101    }
102}
103
104#[cfg(test)]
105mod tests {
106    use super::*;
107
108    #[test]
109    fn invalid_retired_and_foreign_handles_are_rejected() {
110        let handles = ToolHandles::new();
111        let other = ToolHandles::new();
112        let first = handles.insert(1);
113        let second = other.insert(2);
114        assert!(handles.get(std::ptr::null_mut()).is_none());
115        assert!(handles.get(usize::MAX as StepHandle).is_none());
116        assert!(handles.get(second).is_none());
117        assert!(handles.remove(first).is_some());
118        assert!(handles.remove(first).is_none());
119        assert!(handles.get(first).is_none());
120        assert_ne!(handles.insert(3), first);
121    }
122
123    #[test]
124    fn cancellation_does_not_wait_for_the_state_lock() {
125        let handles = ToolHandles::new();
126        let handle = handles.insert(0);
127        let entry = handles.get(handle).unwrap();
128        let _guard = entry.lock();
129        let token = handle as usize;
130        std::thread::scope(|scope| {
131            scope
132                .spawn(|| handles.cancel(token as StepHandle))
133                .join()
134                .unwrap();
135        });
136        assert!(entry.cancelled.load(Ordering::SeqCst));
137    }
138
139    #[test]
140    fn removal_keeps_in_flight_state_alive_and_drops_it_once() {
141        struct CountDrop(Arc<AtomicUsize>);
142        impl Drop for CountDrop {
143            fn drop(&mut self) {
144                self.0.fetch_add(1, Ordering::SeqCst);
145            }
146        }
147        let drops = Arc::new(AtomicUsize::new(0));
148        let handles = ToolHandles::new();
149        let handle = handles.insert(CountDrop(drops.clone()));
150        let entry = handles.get(handle).unwrap();
151        drop(handles.remove(handle));
152        assert!(entry.cancelled.load(Ordering::SeqCst));
153        assert_eq!(drops.load(Ordering::SeqCst), 0);
154        drop(entry);
155        assert_eq!(drops.load(Ordering::SeqCst), 1);
156        assert!(handles.remove(handle).is_none());
157    }
158
159    #[test]
160    fn concurrent_polls_serialize_state_changes() {
161        let handles = ToolHandles::new();
162        let handle = handles.insert(0);
163        let entry = handles.get(handle).unwrap();
164        std::thread::scope(|scope| {
165            for _ in 0..4 {
166                let entry = entry.clone();
167                scope.spawn(move || {
168                    for _ in 0..100 {
169                        *entry.lock() += 1;
170                    }
171                });
172            }
173        });
174        assert_eq!(*entry.lock(), 400);
175    }
176
177    #[test]
178    fn removal_signals_the_workers_shared_cancel_flag() {
179        let handles = ToolHandles::new();
180        let cancelled = Arc::new(AtomicBool::new(false));
181        let handle = handles.insert_with_cancel((), cancelled.clone());
182        drop(handles.remove(handle));
183        assert!(cancelled.load(Ordering::SeqCst));
184    }
185}