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::{FreeStringFn, StbString, StepResult, StepResultTag};
7
8/// Rust-side ownership of an ABI string and its producer's deallocator.
9/// Not `Copy` or `Clone`: moving the value transfers cleanup responsibility.
10///
11/// ```compile_fail
12/// use rpi_plugin_sdk::callbacks::OwnedString;
13/// let owner = OwnedString::from_string("text".into());
14/// let copy = owner;
15/// drop(owner); // Ownership moved; a second release cannot be expressed safely.
16/// ```
17pub struct OwnedString {
18    raw: StbString,
19    free: Option<FreeStringFn>,
20}
21
22impl OwnedString {
23    pub fn from_string(text: String) -> Self {
24        Self {
25            raw: StbString::from_string(text),
26            free: Some(free_string),
27        }
28    }
29
30    /// Adopt an owning string at an ABI boundary.
31    ///
32    /// # Safety
33    /// The caller transfers unique ownership of a live buffer satisfying
34    /// `StbString::to_string_lossy`'s requirements. No raw copy may subsequently
35    /// be read, changed or freed while this owner exists. `free`, when present,
36    /// must be this buffer's producer-side deallocator, callable from any thread.
37    /// When absent, the allocation will not be reclaimed automatically.
38    pub unsafe fn from_raw(raw: StbString, free: Option<FreeStringFn>) -> Self {
39        Self { raw, free }
40    }
41
42    pub fn to_string_lossy(&self) -> String {
43        self.as_str_lossy().into_owned()
44    }
45
46    /// Read the buffer without copying valid UTF-8. Invalid UTF-8 is replaced,
47    /// matching [`Self::to_string_lossy`]. The view cannot outlive its owner.
48    ///
49    /// ```compile_fail
50    /// use rpi_plugin_sdk::callbacks::OwnedString;
51    /// let text = {
52    ///     let owner = OwnedString::from_string("text".into());
53    ///     owner.as_str_lossy()
54    /// };
55    /// println!("{text}");
56    /// ```
57    pub fn as_str_lossy(&self) -> std::borrow::Cow<'_, str> {
58        if self.raw.len == 0 || self.raw.ptr.is_null() {
59            return std::borrow::Cow::Borrowed("");
60        }
61        // SAFETY: construction established exclusive ownership of a live buffer.
62        // This borrow keeps the owner (and its producer-side deallocator) alive.
63        let bytes = unsafe { std::slice::from_raw_parts(self.raw.ptr.cast::<u8>(), self.raw.len) };
64        String::from_utf8_lossy(bytes)
65    }
66
67    /// Transfer ownership back across the ABI, without freeing the buffer.
68    /// The receiver must use the same producer's deallocator exactly once.
69    pub fn into_raw(self) -> StbString {
70        let owner = std::mem::ManuallyDrop::new(self);
71        owner.raw
72    }
73}
74
75impl Drop for OwnedString {
76    fn drop(&mut self) {
77        // SAFETY: this non-copyable owner transfers its live allocation once.
78        unsafe { self.raw.free_with(self.free) };
79    }
80}
81
82pub struct Output<'a> {
83    slot: Option<&'a mut StbString>,
84    written: Option<OwnedString>,
85}
86
87impl<'a> Output<'a> {
88    /// # Safety
89    /// A non-null `slot` must be aligned, initialized, writable and exclusively borrowed
90    /// for `'a`. The host reclaims a written string through this plugin's
91    /// `free_string` callback after the handler returns.
92    pub unsafe fn from_raw(slot: *mut StbString) -> Self {
93        Self {
94            slot: unsafe { slot.as_mut() },
95            written: None,
96        }
97    }
98
99    pub fn is_available(&self) -> bool {
100        self.slot.is_some()
101    }
102
103    /// Allocate only when a slot exists. Repeated writes reclaim only outputs
104    /// this view itself produced, never a value initially supplied by the host.
105    pub fn write(&mut self, text: String) -> bool {
106        self.replace(Some(text))
107    }
108
109    pub fn clear(&mut self) -> bool {
110        self.replace(None)
111    }
112
113    fn replace(&mut self, text: Option<String>) -> bool {
114        let Some(slot) = self.slot.as_deref_mut() else {
115            return false;
116        };
117        let next = text.map(OwnedString::from_string);
118        *slot = next
119            .as_ref()
120            .map(|owner| owner.raw)
121            .unwrap_or_else(StbString::empty);
122        // Replacing the owner reclaims only a previous SDK output, never an
123        // initial host value. Raw slot aliases are inaccessible during this view.
124        self.written = next;
125        true
126    }
127}
128
129impl Drop for Output<'_> {
130    fn drop(&mut self) {
131        if let Some(owner) = self.written.take() {
132            // The final output belongs to the ABI receiver, even when the
133            // handler panics. Replaced outputs were reclaimed during writes.
134            let _ = owner.into_raw();
135        }
136    }
137}
138
139/// ABI deallocation callback for strings produced by this SDK instance.
140///
141/// Install this as `FreeStringFn` in the plugin's registrars. The ABI caller
142/// must pass ownership of a live `StbString::from_*` boxed-slice allocation
143/// exactly once. Never use it for a borrowed input or another producer's string.
144///
145/// # Safety
146/// Transfer a live SDK-owned boxed-slice allocation exactly once. Neither
147/// another raw copy nor a borrowed input may be freed through this callback.
148///
149/// ```compile_fail
150/// use rpi_plugin_sdk::{callbacks, StbString};
151/// callbacks::free_string(StbString::from_string("text".into()));
152/// ```
153pub unsafe extern "C" fn free_string(value: StbString) {
154    if value.is_empty() || value.ptr.is_null() {
155        return;
156    }
157    // SAFETY: the ABI caller transfers the original boxed-slice allocation
158    // exactly once; construction and this callback use the same allocator.
159    unsafe {
160        drop(Box::from_raw(std::ptr::slice_from_raw_parts_mut(
161            value.ptr as *mut u8,
162            value.len,
163        )));
164    }
165}
166
167/// ABI cleanup callback for a step result produced by this SDK instance.
168/// Its live union arm and string allocation must follow the `StepResult` ABI.
169///
170/// # Safety
171/// The tag must identify the initialized payload, whose live SDK-owned string
172/// allocation is transferred exactly once.
173pub unsafe extern "C" fn free_step_result(result: StepResult) {
174    // SAFETY: the producer initializes the union arm indicated by its tag.
175    let text = unsafe {
176        match result.tag {
177            StepResultTag::Pending => result.pending_payload().progress,
178            StepResultTag::Done => result.done_payload().result,
179            StepResultTag::Err => result.err_payload().message,
180        }
181    };
182    // SAFETY: the caller transfers the live SDK-produced payload exactly once.
183    unsafe { free_string(text) };
184}
185
186/// An owned copy of a plugin-produced step result, without ABI pointers.
187#[derive(Debug, PartialEq, Eq)]
188pub enum Step {
189    Pending(String),
190    Done(String),
191    Err(String),
192}
193
194/// # Safety
195/// The result must have a valid live union arm and transfer ownership of a
196/// string produced by this SDK instance exactly once.
197pub unsafe fn take_step_result(result: StepResult) -> Step {
198    // SAFETY: the caller transfers an owned result from this SDK instance.
199    unsafe { take_step_result_with(result, free_string) }
200}
201
202/// Copy and reclaim a result through the producing plugin's deallocator.
203///
204/// # Safety
205/// The tag must select the initialized payload, which must transfer a live
206/// owned string exactly once. `free` must be that string's producer-side
207/// deallocator, callable from any thread.
208pub unsafe fn take_step_result_with(result: StepResult, free: FreeStringFn) -> Step {
209    // SAFETY: the producer's tag identifies its initialized payload.
210    let raw = unsafe {
211        match result.tag {
212            StepResultTag::Pending => result.pending_payload().progress,
213            StepResultTag::Done => result.done_payload().result,
214            StepResultTag::Err => result.err_payload().message,
215        }
216    };
217    // SAFETY: the caller transfers the selected allocation and its producer.
218    let owned = unsafe { OwnedString::from_raw(raw, Some(free)) };
219    let copy = owned.to_string_lossy();
220    match result.tag {
221        StepResultTag::Pending => Step::Pending(copy),
222        StepResultTag::Done => Step::Done(copy),
223        StepResultTag::Err => Step::Err(copy),
224    }
225}
226
227/// Copy and reclaim a result returned by a plugin's tool callback. Use only
228/// with a live, owned result allocated by this SDK instance, not host memory.
229/// The invocation requires an explicit unsafe block; the macro does not prove
230/// validity or ownership of an arbitrary expression.
231///
232/// ```compile_fail
233/// use rpi_plugin_sdk::{StbString, StepResult};
234/// let raw = StepResult::done(StbString::from_string("text".into()));
235/// let owned = rpi_plugin_sdk::take_step_result!(raw);
236/// ```
237#[macro_export]
238macro_rules! take_step_result {
239    ($result:expr) => {{
240        $crate::callbacks::take_step_result($result)
241    }};
242}
243
244/// Define a command or prompt callback with a `&str` input and `&mut Output`.
245/// The four-argument form also supports runtime-action trampolines. A tuple
246/// input `(cwd, reason)` supports resource-discovery callbacks. All forms copy
247/// inputs inside the ABI boundary and contain handler panics.
248#[macro_export]
249macro_rules! string_handler {
250    ($(#[$attr:meta])* $vis:vis fn $name:ident(($first:ident, $second:ident), $out:ident, $data:ident) $body:block) => {
251        $(#[$attr])*
252        $vis extern "C" fn $name(first: $crate::StbStringRef, second: $crate::StbStringRef,
253            slot: *mut $crate::StbString, $data: *mut std::ffi::c_void) -> i32 {
254            $crate::guard_or(1, || {
255                // SAFETY: the host keeps both input buffers and the exclusive
256                // output slot valid until this ABI invocation returns.
257                let first_text = unsafe { first.as_str() }.to_owned();
258                let second_text = unsafe { second.as_str() }.to_owned();
259                let $first = first_text.as_str();
260                let $second = second_text.as_str();
261                let mut output = unsafe { $crate::callbacks::Output::from_raw(slot) };
262                let $out = &mut output;
263                $body
264            })
265        }
266    };
267    ($(#[$attr:meta])* $vis:vis fn $name:ident($input:ident, $out:ident, $data:ident) $body:block) => {
268        $(#[$attr])*
269        $vis extern "C" fn $name(raw: $crate::StbStringRef, slot: *mut $crate::StbString,
270            $data: *mut std::ffi::c_void) -> i32 {
271            $crate::guard_or(1, || {
272                // SAFETY: the host keeps its input buffer and exclusive output
273                // slot valid until this ABI invocation returns.
274                let text = unsafe { raw.as_str() }.to_owned();
275                let $input = text.as_str();
276                let mut output = unsafe { $crate::callbacks::Output::from_raw(slot) };
277                let $out = &mut output;
278                $body
279            })
280        }
281    };
282    ($(#[$attr:meta])* $vis:vis fn $name:ident($action:ident, $input:ident, $out:ident, $data:ident) $body:block) => {
283        $(#[$attr])*
284        $vis extern "C" fn $name($action: u32, raw: $crate::StbStringRef, slot: *mut $crate::StbString,
285            $data: *mut std::ffi::c_void) -> i32 {
286            $crate::guard_or(1, || {
287                // SAFETY: as in the three-argument ABI trampoline above.
288                let text = unsafe { raw.as_str() }.to_owned();
289                let $input = text.as_str();
290                let mut output = unsafe { $crate::callbacks::Output::from_raw(slot) };
291                let $out = &mut output;
292                $body
293            })
294        }
295    };
296}
297
298/// Define a tool execute callback whose call id is an ordinary borrowed str.
299/// The two-argument form gives the body an OwnedString that reclaims parameters
300/// through the host's supplied deallocator. The three-argument form exposes
301/// raw parameters and their deallocator for callers implementing their own ABI.
302#[macro_export]
303macro_rules! execute_handler {
304    ($(#[$attr:meta])* fn $name:ident($id:ident, $params:ident) $body:block) => {
305        $(#[$attr])*
306        extern "C" fn $name(
307            raw: $crate::StbStringRef,
308            params: $crate::StbString,
309            free: Option<$crate::FreeStringFn>,
310        ) -> $crate::StepHandle {
311            $crate::guard_or(std::ptr::null_mut(), || {
312                // SAFETY: execute transfers live host parameters and their
313                // producer callback. Adopt before decoding the borrowed id so
314                // a malformed id cannot leak the owned input.
315                let $params = unsafe { $crate::callbacks::OwnedString::from_raw(params, free) };
316                // SAFETY: the host keeps its borrowed call id live for this call.
317                let text = unsafe { raw.as_str() }.to_owned();
318                let $id = text.as_str();
319                $body
320            })
321        }
322    };
323    ($(#[$attr:meta])* fn $name:ident($id:ident, $params:ident, $free:ident) $body:block) => {
324        $(#[$attr])*
325        extern "C" fn $name(
326            raw: $crate::StbStringRef,
327            $params: $crate::StbString,
328            $free: Option<$crate::FreeStringFn>,
329        ) -> $crate::StepHandle {
330            $crate::guard_or(std::ptr::null_mut(), || {
331                // SAFETY: host input is borrowed for this ABI call only.
332                let text = unsafe { raw.as_str() }.to_owned();
333                let $id = text.as_str();
334                $body
335            })
336        }
337    };
338}
339
340#[cfg(test)]
341mod tests {
342    use super::*;
343    use crate::StbStringRef;
344
345    thread_local! {
346        static OWNER_FREES: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
347    }
348
349    unsafe extern "C" fn counted_free(value: StbString) {
350        if !value.is_empty() {
351            OWNER_FREES.with(|count| count.set(count.get() + 1));
352        }
353        // SAFETY: these tests transfer their live SDK allocation exactly once.
354        unsafe { free_string(value) };
355    }
356
357    execute_handler! { fn refusing_execute(_id, params) {
358        assert_eq!(params.to_string_lossy(), "{}");
359        std::ptr::null_mut()
360    } }
361
362    #[test]
363    fn execute_trampoline_reclaims_parameters_on_refusal_and_invalid_id() {
364        OWNER_FREES.with(|count| count.set(0));
365        assert!(refusing_execute(
366            StbStringRef::from_str("call"),
367            StbString::from_string("{}".into()),
368            Some(counted_free)
369        )
370        .is_null());
371        assert_eq!(OWNER_FREES.with(std::cell::Cell::get), 1);
372        let bytes = [0xff];
373        let id = StbStringRef {
374            ptr: bytes.as_ptr().cast(),
375            len: bytes.len(),
376        };
377        assert!(
378            refusing_execute(id, StbString::from_string("{}".into()), Some(counted_free)).is_null()
379        );
380        assert_eq!(OWNER_FREES.with(std::cell::Cell::get), 2);
381    }
382
383    #[test]
384    fn string_views_borrow_valid_utf8_and_keep_lossy_fallback_and_cleanup() {
385        OWNER_FREES.with(|count| count.set(0));
386        let raw = StbString::from_string("hello 🐈".into());
387        let pointer = raw.ptr.cast::<u8>();
388        // SAFETY: each fresh allocation is transferred to exactly one owner.
389        let owner = unsafe { OwnedString::from_raw(raw, Some(counted_free)) };
390        let view = owner.as_str_lossy();
391        assert!(matches!(view, std::borrow::Cow::Borrowed(_)));
392        assert_eq!(view.as_ptr(), pointer);
393        assert_eq!(view, "hello 🐈");
394        assert_eq!(OWNER_FREES.with(std::cell::Cell::get), 0);
395        drop(view);
396        drop(owner);
397        assert_eq!(OWNER_FREES.with(std::cell::Cell::get), 1);
398
399        let raw = StbString::from_vec(vec![b'a', 0xff, b'b']);
400        // SAFETY: invalid UTF-8 is allowed by the adopted buffer's read contract.
401        let owner = unsafe { OwnedString::from_raw(raw, Some(counted_free)) };
402        assert!(matches!(owner.as_str_lossy(), std::borrow::Cow::Owned(_)));
403        assert_eq!(owner.as_str_lossy(), "a\u{fffd}b");
404        assert_eq!(owner.to_string_lossy(), "a\u{fffd}b");
405        drop(owner);
406        assert_eq!(OWNER_FREES.with(std::cell::Cell::get), 2);
407
408        // SAFETY: null/zero is the ABI's valid empty buffer.
409        let empty = unsafe { OwnedString::from_raw(StbString::empty(), Some(counted_free)) };
410        assert!(matches!(
411            empty.as_str_lossy(),
412            std::borrow::Cow::Borrowed("")
413        ));
414        drop(empty);
415        assert_eq!(OWNER_FREES.with(std::cell::Cell::get), 2);
416    }
417
418    #[test]
419    fn owned_strings_free_once_on_move_and_unwinding() {
420        OWNER_FREES.with(|count| count.set(0));
421        let raw = StbString::from_string("live".into());
422        // SAFETY: this fresh SDK allocation transfers to counted_free once.
423        let owner = unsafe { OwnedString::from_raw(raw, Some(counted_free)) };
424        assert_eq!(owner.to_string_lossy(), "live");
425        let moved = owner;
426        assert_eq!(moved.to_string_lossy(), "live");
427        drop(moved);
428        assert_eq!(OWNER_FREES.with(std::cell::Cell::get), 1);
429        let result = std::panic::catch_unwind(|| {
430            let raw = StbString::from_string("unwind".into());
431            // SAFETY: another fresh allocation transfers to the same producer.
432            let _owner = unsafe { OwnedString::from_raw(raw, Some(counted_free)) };
433            panic!("host parsing failed");
434        });
435        assert!(result.is_err());
436        assert_eq!(OWNER_FREES.with(std::cell::Cell::get), 2);
437    }
438
439    #[test]
440    fn into_raw_transfers_cleanup_to_the_receiver() {
441        let owner = OwnedString::from_string("transferred".into());
442        let raw = owner.into_raw();
443        // SAFETY: into_raw transferred the live allocation without freeing it.
444        let received = unsafe { OwnedString::from_raw(raw, Some(free_string)) };
445        assert_eq!(received.to_string_lossy(), "transferred");
446        drop(received);
447    }
448
449    #[test]
450    fn step_result_uses_the_supplied_producer_for_each_variant() {
451        OWNER_FREES.with(|count| count.set(0));
452        for (raw, expected) in [
453            (
454                StepResult::pending(StbString::from_string("progress".into())),
455                Step::Pending("progress".into()),
456            ),
457            (
458                StepResult::done(StbString::from_string("done".into())),
459                Step::Done("done".into()),
460            ),
461            (
462                StepResult::err(StbString::from_string("error".into())),
463                Step::Err("error".into()),
464            ),
465        ] {
466            // SAFETY: each tagged result transfers one live SDK allocation.
467            assert_eq!(
468                unsafe { take_step_result_with(raw, counted_free) },
469                expected
470            );
471        }
472        assert_eq!(OWNER_FREES.with(std::cell::Cell::get), 3);
473    }
474
475    string_handler! { fn echo(input, output, _data) {
476        i32::from(!output.write(input.to_owned()))
477    } }
478    string_handler! { fn replace_twice(input, output, _data) {
479        output.write(input.to_owned());
480        output.write("second".into());
481        0
482    } }
483    string_handler! { fn panicking(_input, output, _data) {
484        output.write("valid output before panic".into());
485        panic!("callback failed");
486    } }
487    string_handler! { fn action(action, input, output, _data) {
488        output.write(format!("{action}:{input}"));
489        0
490    } }
491    string_handler! { fn discover((cwd, reason), output, _data) {
492        i32::from(!output.write(format!("{cwd}:{reason}")))
493    } }
494
495    #[test]
496    fn discovery_copies_both_inputs_and_handles_missing_output() {
497        let mut output = StbString::empty();
498        let cwd = StbStringRef::from_str("/项目");
499        let reason = StbStringRef::from_str("reload");
500        assert_eq!(discover(cwd, reason, &mut output, std::ptr::null_mut()), 0);
501        // SAFETY: discover wrote a live SDK allocation; it has not been freed.
502        assert_eq!(
503            unsafe { OwnedString::from_raw(output, Some(free_string)) }.to_string_lossy(),
504            "/项目:reload"
505        );
506        assert_eq!(
507            discover(cwd, reason, std::ptr::null_mut(), std::ptr::null_mut()),
508            1
509        );
510    }
511
512    #[test]
513    fn malformed_second_discovery_input_does_not_write_output() {
514        let bytes = [0xff];
515        let reason = StbStringRef {
516            ptr: bytes.as_ptr().cast(),
517            len: bytes.len(),
518        };
519        let mut output = StbString::empty();
520        assert_eq!(
521            discover(
522                StbStringRef::from_str("/tmp"),
523                reason,
524                &mut output,
525                std::ptr::null_mut()
526            ),
527            1
528        );
529        assert!(output.is_empty());
530    }
531
532    #[test]
533    fn missing_output_is_reported_without_a_pointer_write() {
534        assert_eq!(
535            echo(
536                StbStringRef::from_str("text"),
537                std::ptr::null_mut(),
538                std::ptr::null_mut()
539            ),
540            1
541        );
542    }
543
544    #[test]
545    fn malformed_utf8_is_rejected_before_the_callback_writes_output() {
546        let bytes = [0xff, 0xfe];
547        let input = StbStringRef {
548            ptr: bytes.as_ptr().cast(),
549            len: bytes.len(),
550        };
551        let mut output = StbString::empty();
552        assert_eq!(echo(input, &mut output, std::ptr::null_mut()), 1);
553        assert!(output.is_empty());
554    }
555
556    #[test]
557    fn strings_are_copied_and_repeated_output_writes_are_reclaimed() {
558        let mut output = StbString::empty();
559        assert_eq!(
560            echo(
561                StbStringRef::from_str("你好"),
562                &mut output,
563                std::ptr::null_mut()
564            ),
565            0
566        );
567        // SAFETY: the handler produced live SDK output, freed after this assertion.
568        assert_eq!(
569            unsafe { OwnedString::from_raw(output, Some(free_string)) }.to_string_lossy(),
570            "你好"
571        );
572        output = StbString::empty();
573        assert_eq!(
574            replace_twice(
575                StbStringRef::from_str("first"),
576                &mut output,
577                std::ptr::null_mut()
578            ),
579            0
580        );
581        // SAFETY: the handler produced live SDK output, freed after this assertion.
582        assert_eq!(
583            unsafe { OwnedString::from_raw(output, Some(free_string)) }.to_string_lossy(),
584            "second"
585        );
586    }
587
588    #[test]
589    fn panics_leave_a_valid_reclaimable_output() {
590        let mut output = StbString::empty();
591        assert_eq!(
592            panicking(StbStringRef::empty(), &mut output, std::ptr::null_mut()),
593            1
594        );
595        // SAFETY: the handler produced live SDK output, freed after this assertion.
596        assert_eq!(
597            unsafe { OwnedString::from_raw(output, Some(free_string)) }.to_string_lossy(),
598            "valid output before panic"
599        );
600    }
601
602    #[test]
603    fn runtime_action_signature_is_preserved() {
604        let mut output = StbString::empty();
605        assert_eq!(
606            action(
607                17,
608                StbStringRef::from_str("{}"),
609                &mut output,
610                std::ptr::null_mut()
611            ),
612            0
613        );
614        // SAFETY: the handler produced live SDK output, freed after this assertion.
615        assert_eq!(
616            unsafe { OwnedString::from_raw(output, Some(free_string)) }.to_string_lossy(),
617            "17:{}"
618        );
619    }
620
621    #[test]
622    fn writing_does_not_reclaim_an_initial_host_owned_value() {
623        let initial = StbString::from_string("host-owned".into());
624        let mut output = initial;
625        assert_eq!(
626            echo(
627                StbStringRef::from_str("plugin-owned"),
628                &mut output,
629                std::ptr::null_mut()
630            ),
631            0
632        );
633        // SAFETY: initial is a live SDK allocation, freed after this assertion.
634        assert_eq!(
635            unsafe { OwnedString::from_raw(initial, Some(free_string)) }.to_string_lossy(),
636            "host-owned"
637        );
638        // SAFETY: the handler produced live SDK output, freed after this assertion.
639        assert_eq!(
640            unsafe { OwnedString::from_raw(output, Some(free_string)) }.to_string_lossy(),
641            "plugin-owned"
642        );
643    }
644
645    #[test]
646    fn every_step_result_variant_can_be_reclaimed() {
647        // SAFETY: each initialized payload transfers its fresh SDK allocation.
648        unsafe {
649            free_step_result(StepResult::pending(StbString::from_string(
650                "progress".into(),
651            )));
652            free_step_result(StepResult::done(StbString::from_string("done".into())));
653            free_step_result(StepResult::err(StbString::from_string("error".into())));
654            free_string(StbString::empty());
655            assert_eq!(
656                take_step_result!(StepResult::err(StbString::from_string("cancelled".into()))),
657                Step::Err("cancelled".into())
658            );
659        }
660    }
661}