use crate::{StbString, StepResult, StepResultTag};
pub struct Output<'a> {
slot: Option<&'a mut StbString>,
written: bool,
}
impl<'a> Output<'a> {
pub unsafe fn from_raw(slot: *mut StbString) -> Self {
Self {
slot: unsafe { slot.as_mut() },
written: false,
}
}
pub fn is_available(&self) -> bool {
self.slot.is_some()
}
pub fn write(&mut self, text: String) -> bool {
self.replace(Some(text))
}
pub fn clear(&mut self) -> bool {
self.replace(None)
}
fn replace(&mut self, text: Option<String>) -> bool {
let Some(slot) = self.slot.as_deref_mut() else {
return false;
};
if self.written {
free_string(*slot);
}
*slot = text
.map(StbString::from_string)
.unwrap_or_else(StbString::empty);
self.written = true;
true
}
}
pub extern "C" fn free_string(value: StbString) {
if value.is_empty() || value.ptr.is_null() {
return;
}
unsafe {
drop(Box::from_raw(std::ptr::slice_from_raw_parts_mut(
value.ptr as *mut u8,
value.len,
)));
}
}
pub extern "C" fn free_step_result(result: StepResult) {
let text = unsafe {
match result.tag {
StepResultTag::Pending => result.pending_payload().progress,
StepResultTag::Done => result.done_payload().result,
StepResultTag::Err => result.err_payload().message,
}
};
free_string(text);
}
#[derive(Debug, PartialEq, Eq)]
pub enum Step {
Pending(String),
Done(String),
Err(String),
}
pub unsafe fn take_step_result(result: StepResult) -> Step {
let copy = unsafe {
match result.tag {
StepResultTag::Pending => {
Step::Pending(result.pending_payload().progress.to_string_lossy())
}
StepResultTag::Done => Step::Done(result.done_payload().result.to_string_lossy()),
StepResultTag::Err => Step::Err(result.err_payload().message.to_string_lossy()),
}
};
free_step_result(result);
copy
}
#[macro_export]
macro_rules! take_step_result {
($result:expr) => {{
unsafe { $crate::callbacks::take_step_result($result) }
}};
}
#[macro_export]
macro_rules! string_handler {
($(#[$attr:meta])* $vis:vis fn $name:ident($input:ident, $out:ident, $data:ident) $body:block) => {
$(#[$attr])*
$vis extern "C" fn $name(raw: $crate::StbStringRef, slot: *mut $crate::StbString,
$data: *mut std::ffi::c_void) -> i32 {
$crate::guard_or(1, || {
// SAFETY: the host keeps its input buffer and exclusive output
// slot valid until this ABI invocation returns.
let text = unsafe { raw.as_str() }.to_owned();
let $input = text.as_str();
let mut output = unsafe { $crate::callbacks::Output::from_raw(slot) };
let $out = &mut output;
$body
})
}
};
($(#[$attr:meta])* $vis:vis fn $name:ident($action:ident, $input:ident, $out:ident, $data:ident) $body:block) => {
$(#[$attr])*
$vis extern "C" fn $name($action: u32, raw: $crate::StbStringRef, slot: *mut $crate::StbString,
$data: *mut std::ffi::c_void) -> i32 {
$crate::guard_or(1, || {
let text = unsafe { raw.as_str() }.to_owned();
let $input = text.as_str();
let mut output = unsafe { $crate::callbacks::Output::from_raw(slot) };
let $out = &mut output;
$body
})
}
};
}
#[macro_export]
macro_rules! execute_handler {
(fn $name:ident($id:ident, $params:ident, $free:ident) $body:block) => {
extern "C" fn $name(
raw: $crate::StbStringRef,
$params: $crate::StbString,
$free: Option<$crate::FreeStringFn>,
) -> $crate::StepHandle {
$crate::guard_or(std::ptr::null_mut(), || {
let text = unsafe { raw.as_str() }.to_owned();
let $id = text.as_str();
$body
})
}
};
}
#[cfg(test)]
mod tests {
use super::*;
use crate::StbStringRef;
string_handler! { fn echo(input, output, _data) {
i32::from(!output.write(input.to_owned()))
} }
string_handler! { fn replace_twice(input, output, _data) {
output.write(input.to_owned());
output.write("second".into());
0
} }
string_handler! { fn panicking(_input, output, _data) {
output.write("valid output before panic".into());
panic!("callback failed");
} }
string_handler! { fn action(action, input, output, _data) {
output.write(format!("{action}:{input}"));
0
} }
#[test]
fn missing_output_is_reported_without_a_pointer_write() {
assert_eq!(
echo(
StbStringRef::from_str("text"),
std::ptr::null_mut(),
std::ptr::null_mut()
),
1
);
}
#[test]
fn strings_are_copied_and_repeated_output_writes_are_reclaimed() {
let mut output = StbString::empty();
assert_eq!(
echo(
StbStringRef::from_str("你好"),
&mut output,
std::ptr::null_mut()
),
0
);
assert_eq!(output.to_string_lossy(), "你好");
free_string(output);
output = StbString::empty();
assert_eq!(
replace_twice(
StbStringRef::from_str("first"),
&mut output,
std::ptr::null_mut()
),
0
);
assert_eq!(output.to_string_lossy(), "second");
free_string(output);
}
#[test]
fn panics_leave_a_valid_reclaimable_output() {
let mut output = StbString::empty();
assert_eq!(
panicking(StbStringRef::empty(), &mut output, std::ptr::null_mut()),
1
);
assert_eq!(output.to_string_lossy(), "valid output before panic");
free_string(output);
}
#[test]
fn runtime_action_signature_is_preserved() {
let mut output = StbString::empty();
assert_eq!(
action(
17,
StbStringRef::from_str("{}"),
&mut output,
std::ptr::null_mut()
),
0
);
assert_eq!(output.to_string_lossy(), "17:{}");
free_string(output);
}
#[test]
fn writing_does_not_reclaim_an_initial_host_owned_value() {
let initial = StbString::from_string("host-owned".into());
let mut output = initial;
assert_eq!(
echo(
StbStringRef::from_str("plugin-owned"),
&mut output,
std::ptr::null_mut()
),
0
);
assert_eq!(initial.to_string_lossy(), "host-owned");
assert_eq!(output.to_string_lossy(), "plugin-owned");
free_string(initial);
free_string(output);
}
#[test]
fn every_step_result_variant_can_be_reclaimed() {
free_step_result(StepResult::pending(StbString::from_string(
"progress".into(),
)));
free_step_result(StepResult::done(StbString::from_string("done".into())));
free_step_result(StepResult::err(StbString::from_string("error".into())));
free_string(StbString::empty());
assert_eq!(
take_step_result!(StepResult::err(StbString::from_string("cancelled".into()))),
Step::Err("cancelled".into())
);
}
}