Skip to main content

rpi_plugin_sdk/
callbacks.rs

1//! SDK-owned C ABI trampolines for string inputs, output slots and deallocation.
2//!
3//! Raw validity and allocation ownership remain the ABI caller's contract.
4//! Plugin bodies use Rust strings and an exclusive, lifetime-bound output
5//! capability instead of manipulating pointers themselves.
6use crate::{StbString, StepResult, StepResultTag};
7
8pub struct Output<'a> {
9    slot: Option<&'a mut StbString>,
10    written: bool,
11}
12
13impl<'a> Output<'a> {
14    /// # Safety
15    /// A non-null `slot` must be aligned, writable and exclusively borrowed
16    /// for `'a`. The host reclaims a written string through this plugin's
17    /// `free_string` callback after the handler returns.
18    pub unsafe fn from_raw(slot: *mut StbString) -> Self {
19        Self {
20            slot: unsafe { slot.as_mut() },
21            written: false,
22        }
23    }
24
25    pub fn is_available(&self) -> bool {
26        self.slot.is_some()
27    }
28
29    /// Allocate only when a slot exists. Repeated writes reclaim only outputs
30    /// this view itself produced, never a value initially supplied by the host.
31    pub fn write(&mut self, text: String) -> bool {
32        self.replace(Some(text))
33    }
34
35    pub fn clear(&mut self) -> bool {
36        self.replace(None)
37    }
38
39    fn replace(&mut self, text: Option<String>) -> bool {
40        let Some(slot) = self.slot.as_deref_mut() else {
41            return false;
42        };
43        if self.written {
44            free_string(*slot);
45        }
46        *slot = text
47            .map(StbString::from_string)
48            .unwrap_or_else(StbString::empty);
49        self.written = true;
50        true
51    }
52}
53
54/// ABI deallocation callback for strings produced by this SDK instance.
55///
56/// Install this as `FreeStringFn` in the plugin's registrars. The ABI caller
57/// must pass ownership of a live `StbString::from_*` boxed-slice allocation
58/// exactly once. Never use it for a borrowed input or another producer's string.
59pub extern "C" fn free_string(value: StbString) {
60    if value.is_empty() || value.ptr.is_null() {
61        return;
62    }
63    // SAFETY: the ABI caller transfers the original boxed-slice allocation
64    // exactly once; construction and this callback use the same allocator.
65    unsafe {
66        drop(Box::from_raw(std::ptr::slice_from_raw_parts_mut(
67            value.ptr as *mut u8,
68            value.len,
69        )));
70    }
71}
72
73/// ABI cleanup callback for a step result produced by this SDK instance.
74/// Its live union arm and string allocation must follow the `StepResult` ABI.
75pub extern "C" fn free_step_result(result: StepResult) {
76    // SAFETY: the producer initializes the union arm indicated by its tag.
77    let text = unsafe {
78        match result.tag {
79            StepResultTag::Pending => result.pending_payload().progress,
80            StepResultTag::Done => result.done_payload().result,
81            StepResultTag::Err => result.err_payload().message,
82        }
83    };
84    free_string(text);
85}
86
87/// An owned copy of a plugin-produced step result, without ABI pointers.
88#[derive(Debug, PartialEq, Eq)]
89pub enum Step {
90    Pending(String),
91    Done(String),
92    Err(String),
93}
94
95/// # Safety
96/// The result must have a valid live union arm and transfer ownership of a
97/// string produced by this SDK instance exactly once.
98pub unsafe fn take_step_result(result: StepResult) -> Step {
99    // SAFETY: the producer's tag identifies its initialized payload.
100    let copy = unsafe {
101        match result.tag {
102            StepResultTag::Pending => {
103                Step::Pending(result.pending_payload().progress.to_string_lossy())
104            }
105            StepResultTag::Done => Step::Done(result.done_payload().result.to_string_lossy()),
106            StepResultTag::Err => Step::Err(result.err_payload().message.to_string_lossy()),
107        }
108    };
109    free_step_result(result);
110    copy
111}
112
113/// Copy and reclaim a result returned by a plugin's tool callback. Use only
114/// with a live, owned result allocated by this SDK instance, not host memory.
115#[macro_export]
116macro_rules! take_step_result {
117    ($result:expr) => {{
118        // SAFETY: this is the result boundary of a plugin-owned tool callback.
119        unsafe { $crate::callbacks::take_step_result($result) }
120    }};
121}
122
123/// Define a command or prompt callback with a `&str` input and `&mut Output`.
124/// The four-argument form also supports runtime-action trampolines. Both
125/// forms copy the input inside the ABI boundary and contain handler panics.
126#[macro_export]
127macro_rules! string_handler {
128    ($(#[$attr:meta])* $vis:vis fn $name:ident($input:ident, $out:ident, $data:ident) $body:block) => {
129        $(#[$attr])*
130        $vis extern "C" fn $name(raw: $crate::StbStringRef, slot: *mut $crate::StbString,
131            $data: *mut std::ffi::c_void) -> i32 {
132            $crate::guard_or(1, || {
133                // SAFETY: the host keeps its input buffer and exclusive output
134                // slot valid until this ABI invocation returns.
135                let text = unsafe { raw.as_str() }.to_owned();
136                let $input = text.as_str();
137                let mut output = unsafe { $crate::callbacks::Output::from_raw(slot) };
138                let $out = &mut output;
139                $body
140            })
141        }
142    };
143    ($(#[$attr:meta])* $vis:vis fn $name:ident($action:ident, $input:ident, $out:ident, $data:ident) $body:block) => {
144        $(#[$attr])*
145        $vis extern "C" fn $name($action: u32, raw: $crate::StbStringRef, slot: *mut $crate::StbString,
146            $data: *mut std::ffi::c_void) -> i32 {
147            $crate::guard_or(1, || {
148                // SAFETY: as in the three-argument ABI trampoline above.
149                let text = unsafe { raw.as_str() }.to_owned();
150                let $input = text.as_str();
151                let mut output = unsafe { $crate::callbacks::Output::from_raw(slot) };
152                let $out = &mut output;
153                $body
154            })
155        }
156    };
157}
158
159/// Define a tool execute callback whose call id is an ordinary borrowed str.
160/// Owned parameters still follow the host's `free_params` allocation contract.
161#[macro_export]
162macro_rules! execute_handler {
163    (fn $name:ident($id:ident, $params:ident, $free:ident) $body:block) => {
164        extern "C" fn $name(
165            raw: $crate::StbStringRef,
166            $params: $crate::StbString,
167            $free: Option<$crate::FreeStringFn>,
168        ) -> $crate::StepHandle {
169            $crate::guard_or(std::ptr::null_mut(), || {
170                // SAFETY: host input is borrowed for this ABI call only.
171                let text = unsafe { raw.as_str() }.to_owned();
172                let $id = text.as_str();
173                $body
174            })
175        }
176    };
177}
178
179#[cfg(test)]
180mod tests {
181    use super::*;
182    use crate::StbStringRef;
183
184    string_handler! { fn echo(input, output, _data) {
185        i32::from(!output.write(input.to_owned()))
186    } }
187    string_handler! { fn replace_twice(input, output, _data) {
188        output.write(input.to_owned());
189        output.write("second".into());
190        0
191    } }
192    string_handler! { fn panicking(_input, output, _data) {
193        output.write("valid output before panic".into());
194        panic!("callback failed");
195    } }
196    string_handler! { fn action(action, input, output, _data) {
197        output.write(format!("{action}:{input}"));
198        0
199    } }
200
201    #[test]
202    fn missing_output_is_reported_without_a_pointer_write() {
203        assert_eq!(
204            echo(
205                StbStringRef::from_str("text"),
206                std::ptr::null_mut(),
207                std::ptr::null_mut()
208            ),
209            1
210        );
211    }
212
213    #[test]
214    fn strings_are_copied_and_repeated_output_writes_are_reclaimed() {
215        let mut output = StbString::empty();
216        assert_eq!(
217            echo(
218                StbStringRef::from_str("你好"),
219                &mut output,
220                std::ptr::null_mut()
221            ),
222            0
223        );
224        assert_eq!(output.to_string_lossy(), "你好");
225        free_string(output);
226        output = StbString::empty();
227        assert_eq!(
228            replace_twice(
229                StbStringRef::from_str("first"),
230                &mut output,
231                std::ptr::null_mut()
232            ),
233            0
234        );
235        assert_eq!(output.to_string_lossy(), "second");
236        free_string(output);
237    }
238
239    #[test]
240    fn panics_leave_a_valid_reclaimable_output() {
241        let mut output = StbString::empty();
242        assert_eq!(
243            panicking(StbStringRef::empty(), &mut output, std::ptr::null_mut()),
244            1
245        );
246        assert_eq!(output.to_string_lossy(), "valid output before panic");
247        free_string(output);
248    }
249
250    #[test]
251    fn runtime_action_signature_is_preserved() {
252        let mut output = StbString::empty();
253        assert_eq!(
254            action(
255                17,
256                StbStringRef::from_str("{}"),
257                &mut output,
258                std::ptr::null_mut()
259            ),
260            0
261        );
262        assert_eq!(output.to_string_lossy(), "17:{}");
263        free_string(output);
264    }
265
266    #[test]
267    fn writing_does_not_reclaim_an_initial_host_owned_value() {
268        let initial = StbString::from_string("host-owned".into());
269        let mut output = initial;
270        assert_eq!(
271            echo(
272                StbStringRef::from_str("plugin-owned"),
273                &mut output,
274                std::ptr::null_mut()
275            ),
276            0
277        );
278        assert_eq!(initial.to_string_lossy(), "host-owned");
279        assert_eq!(output.to_string_lossy(), "plugin-owned");
280        free_string(initial);
281        free_string(output);
282    }
283
284    #[test]
285    fn every_step_result_variant_can_be_reclaimed() {
286        free_step_result(StepResult::pending(StbString::from_string(
287            "progress".into(),
288        )));
289        free_step_result(StepResult::done(StbString::from_string("done".into())));
290        free_step_result(StepResult::err(StbString::from_string("error".into())));
291        free_string(StbString::empty());
292        assert_eq!(
293            take_step_result!(StepResult::err(StbString::from_string("cancelled".into()))),
294            Step::Err("cancelled".into())
295        );
296    }
297}