use crate::{FreeStringFn, StbString, StepResult, StepResultTag};
pub struct OwnedString {
raw: StbString,
free: Option<FreeStringFn>,
}
impl OwnedString {
pub fn from_string(text: String) -> Self {
Self {
raw: StbString::from_string(text),
free: Some(free_string),
}
}
pub unsafe fn from_raw(raw: StbString, free: Option<FreeStringFn>) -> Self {
Self { raw, free }
}
pub fn to_string_lossy(&self) -> String {
unsafe { self.raw.to_string_lossy() }
}
pub fn into_raw(self) -> StbString {
let owner = std::mem::ManuallyDrop::new(self);
owner.raw
}
}
impl Drop for OwnedString {
fn drop(&mut self) {
unsafe { self.raw.free_with(self.free) };
}
}
pub struct Output<'a> {
slot: Option<&'a mut StbString>,
written: Option<OwnedString>,
}
impl<'a> Output<'a> {
pub unsafe fn from_raw(slot: *mut StbString) -> Self {
Self {
slot: unsafe { slot.as_mut() },
written: None,
}
}
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;
};
let next = text.map(OwnedString::from_string);
*slot = next
.as_ref()
.map(|owner| owner.raw)
.unwrap_or_else(StbString::empty);
self.written = next;
true
}
}
impl Drop for Output<'_> {
fn drop(&mut self) {
if let Some(owner) = self.written.take() {
let _ = owner.into_raw();
}
}
}
pub unsafe 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 unsafe 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,
}
};
unsafe { 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 {
unsafe { take_step_result_with(result, free_string) }
}
pub unsafe fn take_step_result_with(result: StepResult, free: FreeStringFn) -> Step {
let raw = unsafe {
match result.tag {
StepResultTag::Pending => result.pending_payload().progress,
StepResultTag::Done => result.done_payload().result,
StepResultTag::Err => result.err_payload().message,
}
};
let owned = unsafe { OwnedString::from_raw(raw, Some(free)) };
let copy = owned.to_string_lossy();
match result.tag {
StepResultTag::Pending => Step::Pending(copy),
StepResultTag::Done => Step::Done(copy),
StepResultTag::Err => Step::Err(copy),
}
}
#[macro_export]
macro_rules! take_step_result {
($result:expr) => {{
$crate::callbacks::take_step_result($result)
}};
}
#[macro_export]
macro_rules! string_handler {
($(#[$attr:meta])* $vis:vis fn $name:ident(($first:ident, $second:ident), $out:ident, $data:ident) $body:block) => {
$(#[$attr])*
$vis extern "C" fn $name(first: $crate::StbStringRef, second: $crate::StbStringRef,
slot: *mut $crate::StbString, $data: *mut std::ffi::c_void) -> i32 {
$crate::guard_or(1, || {
// SAFETY: the host keeps both input buffers and the exclusive
// output slot valid until this ABI invocation returns.
let first_text = unsafe { first.as_str() }.to_owned();
let second_text = unsafe { second.as_str() }.to_owned();
let $first = first_text.as_str();
let $second = second_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($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, || {
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 {
($(#[$attr:meta])* fn $name:ident($id:ident, $params:ident) $body:block) => {
$(#[$attr])*
extern "C" fn $name(
raw: $crate::StbStringRef,
params: $crate::StbString,
free: Option<$crate::FreeStringFn>,
) -> $crate::StepHandle {
$crate::guard_or(std::ptr::null_mut(), || {
let $params = unsafe { $crate::callbacks::OwnedString::from_raw(params, free) };
let text = unsafe { raw.as_str() }.to_owned();
let $id = text.as_str();
$body
})
}
};
($(#[$attr:meta])* fn $name:ident($id:ident, $params:ident, $free:ident) $body:block) => {
$(#[$attr])*
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;
thread_local! {
static OWNER_FREES: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
}
unsafe extern "C" fn counted_free(value: StbString) {
if !value.is_empty() {
OWNER_FREES.with(|count| count.set(count.get() + 1));
}
unsafe { free_string(value) };
}
execute_handler! { fn refusing_execute(_id, params) {
assert_eq!(params.to_string_lossy(), "{}");
std::ptr::null_mut()
} }
#[test]
fn execute_trampoline_reclaims_parameters_on_refusal_and_invalid_id() {
OWNER_FREES.with(|count| count.set(0));
assert!(refusing_execute(
StbStringRef::from_str("call"),
StbString::from_string("{}".into()),
Some(counted_free)
)
.is_null());
assert_eq!(OWNER_FREES.with(std::cell::Cell::get), 1);
let bytes = [0xff];
let id = StbStringRef {
ptr: bytes.as_ptr().cast(),
len: bytes.len(),
};
assert!(
refusing_execute(id, StbString::from_string("{}".into()), Some(counted_free)).is_null()
);
assert_eq!(OWNER_FREES.with(std::cell::Cell::get), 2);
}
#[test]
fn owned_strings_free_once_on_move_and_unwinding() {
OWNER_FREES.with(|count| count.set(0));
let raw = StbString::from_string("live".into());
let owner = unsafe { OwnedString::from_raw(raw, Some(counted_free)) };
assert_eq!(owner.to_string_lossy(), "live");
let moved = owner;
assert_eq!(moved.to_string_lossy(), "live");
drop(moved);
assert_eq!(OWNER_FREES.with(std::cell::Cell::get), 1);
let result = std::panic::catch_unwind(|| {
let raw = StbString::from_string("unwind".into());
let _owner = unsafe { OwnedString::from_raw(raw, Some(counted_free)) };
panic!("host parsing failed");
});
assert!(result.is_err());
assert_eq!(OWNER_FREES.with(std::cell::Cell::get), 2);
}
#[test]
fn into_raw_transfers_cleanup_to_the_receiver() {
let owner = OwnedString::from_string("transferred".into());
let raw = owner.into_raw();
let received = unsafe { OwnedString::from_raw(raw, Some(free_string)) };
assert_eq!(received.to_string_lossy(), "transferred");
drop(received);
}
#[test]
fn step_result_uses_the_supplied_producer_for_each_variant() {
OWNER_FREES.with(|count| count.set(0));
for (raw, expected) in [
(
StepResult::pending(StbString::from_string("progress".into())),
Step::Pending("progress".into()),
),
(
StepResult::done(StbString::from_string("done".into())),
Step::Done("done".into()),
),
(
StepResult::err(StbString::from_string("error".into())),
Step::Err("error".into()),
),
] {
assert_eq!(
unsafe { take_step_result_with(raw, counted_free) },
expected
);
}
assert_eq!(OWNER_FREES.with(std::cell::Cell::get), 3);
}
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
} }
string_handler! { fn discover((cwd, reason), output, _data) {
i32::from(!output.write(format!("{cwd}:{reason}")))
} }
#[test]
fn discovery_copies_both_inputs_and_handles_missing_output() {
let mut output = StbString::empty();
let cwd = StbStringRef::from_str("/项目");
let reason = StbStringRef::from_str("reload");
assert_eq!(discover(cwd, reason, &mut output, std::ptr::null_mut()), 0);
assert_eq!(
unsafe { OwnedString::from_raw(output, Some(free_string)) }.to_string_lossy(),
"/项目:reload"
);
assert_eq!(
discover(cwd, reason, std::ptr::null_mut(), std::ptr::null_mut()),
1
);
}
#[test]
fn malformed_second_discovery_input_does_not_write_output() {
let bytes = [0xff];
let reason = StbStringRef {
ptr: bytes.as_ptr().cast(),
len: bytes.len(),
};
let mut output = StbString::empty();
assert_eq!(
discover(
StbStringRef::from_str("/tmp"),
reason,
&mut output,
std::ptr::null_mut()
),
1
);
assert!(output.is_empty());
}
#[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 malformed_utf8_is_rejected_before_the_callback_writes_output() {
let bytes = [0xff, 0xfe];
let input = StbStringRef {
ptr: bytes.as_ptr().cast(),
len: bytes.len(),
};
let mut output = StbString::empty();
assert_eq!(echo(input, &mut output, std::ptr::null_mut()), 1);
assert!(output.is_empty());
}
#[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!(
unsafe { OwnedString::from_raw(output, Some(free_string)) }.to_string_lossy(),
"你好"
);
output = StbString::empty();
assert_eq!(
replace_twice(
StbStringRef::from_str("first"),
&mut output,
std::ptr::null_mut()
),
0
);
assert_eq!(
unsafe { OwnedString::from_raw(output, Some(free_string)) }.to_string_lossy(),
"second"
);
}
#[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!(
unsafe { OwnedString::from_raw(output, Some(free_string)) }.to_string_lossy(),
"valid output before panic"
);
}
#[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!(
unsafe { OwnedString::from_raw(output, Some(free_string)) }.to_string_lossy(),
"17:{}"
);
}
#[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!(
unsafe { OwnedString::from_raw(initial, Some(free_string)) }.to_string_lossy(),
"host-owned"
);
assert_eq!(
unsafe { OwnedString::from_raw(output, Some(free_string)) }.to_string_lossy(),
"plugin-owned"
);
}
#[test]
fn every_step_result_variant_can_be_reclaimed() {
unsafe {
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())
);
}
}
}