1use crate::{FreeStringFn, StbString, StepResult, StepResultTag};
7
8pub 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 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 unsafe { self.raw.to_string_lossy() }
45 }
46
47 pub fn into_raw(self) -> StbString {
50 let owner = std::mem::ManuallyDrop::new(self);
51 owner.raw
52 }
53}
54
55impl Drop for OwnedString {
56 fn drop(&mut self) {
57 unsafe { self.raw.free_with(self.free) };
59 }
60}
61
62pub struct Output<'a> {
63 slot: Option<&'a mut StbString>,
64 written: Option<OwnedString>,
65}
66
67impl<'a> Output<'a> {
68 pub unsafe fn from_raw(slot: *mut StbString) -> Self {
73 Self {
74 slot: unsafe { slot.as_mut() },
75 written: None,
76 }
77 }
78
79 pub fn is_available(&self) -> bool {
80 self.slot.is_some()
81 }
82
83 pub fn write(&mut self, text: String) -> bool {
86 self.replace(Some(text))
87 }
88
89 pub fn clear(&mut self) -> bool {
90 self.replace(None)
91 }
92
93 fn replace(&mut self, text: Option<String>) -> bool {
94 let Some(slot) = self.slot.as_deref_mut() else {
95 return false;
96 };
97 let next = text.map(OwnedString::from_string);
98 *slot = next
99 .as_ref()
100 .map(|owner| owner.raw)
101 .unwrap_or_else(StbString::empty);
102 self.written = next;
105 true
106 }
107}
108
109impl Drop for Output<'_> {
110 fn drop(&mut self) {
111 if let Some(owner) = self.written.take() {
112 let _ = owner.into_raw();
115 }
116 }
117}
118
119pub unsafe extern "C" fn free_string(value: StbString) {
134 if value.is_empty() || value.ptr.is_null() {
135 return;
136 }
137 unsafe {
140 drop(Box::from_raw(std::ptr::slice_from_raw_parts_mut(
141 value.ptr as *mut u8,
142 value.len,
143 )));
144 }
145}
146
147pub unsafe extern "C" fn free_step_result(result: StepResult) {
154 let text = unsafe {
156 match result.tag {
157 StepResultTag::Pending => result.pending_payload().progress,
158 StepResultTag::Done => result.done_payload().result,
159 StepResultTag::Err => result.err_payload().message,
160 }
161 };
162 unsafe { free_string(text) };
164}
165
166#[derive(Debug, PartialEq, Eq)]
168pub enum Step {
169 Pending(String),
170 Done(String),
171 Err(String),
172}
173
174pub unsafe fn take_step_result(result: StepResult) -> Step {
178 unsafe { take_step_result_with(result, free_string) }
180}
181
182pub unsafe fn take_step_result_with(result: StepResult, free: FreeStringFn) -> Step {
189 let raw = unsafe {
191 match result.tag {
192 StepResultTag::Pending => result.pending_payload().progress,
193 StepResultTag::Done => result.done_payload().result,
194 StepResultTag::Err => result.err_payload().message,
195 }
196 };
197 let owned = unsafe { OwnedString::from_raw(raw, Some(free)) };
199 let copy = owned.to_string_lossy();
200 match result.tag {
201 StepResultTag::Pending => Step::Pending(copy),
202 StepResultTag::Done => Step::Done(copy),
203 StepResultTag::Err => Step::Err(copy),
204 }
205}
206
207#[macro_export]
218macro_rules! take_step_result {
219 ($result:expr) => {{
220 $crate::callbacks::take_step_result($result)
221 }};
222}
223
224#[macro_export]
229macro_rules! string_handler {
230 ($(#[$attr:meta])* $vis:vis fn $name:ident(($first:ident, $second:ident), $out:ident, $data:ident) $body:block) => {
231 $(#[$attr])*
232 $vis extern "C" fn $name(first: $crate::StbStringRef, second: $crate::StbStringRef,
233 slot: *mut $crate::StbString, $data: *mut std::ffi::c_void) -> i32 {
234 $crate::guard_or(1, || {
235 let first_text = unsafe { first.as_str() }.to_owned();
238 let second_text = unsafe { second.as_str() }.to_owned();
239 let $first = first_text.as_str();
240 let $second = second_text.as_str();
241 let mut output = unsafe { $crate::callbacks::Output::from_raw(slot) };
242 let $out = &mut output;
243 $body
244 })
245 }
246 };
247 ($(#[$attr:meta])* $vis:vis fn $name:ident($input:ident, $out:ident, $data:ident) $body:block) => {
248 $(#[$attr])*
249 $vis extern "C" fn $name(raw: $crate::StbStringRef, slot: *mut $crate::StbString,
250 $data: *mut std::ffi::c_void) -> i32 {
251 $crate::guard_or(1, || {
252 let text = unsafe { raw.as_str() }.to_owned();
255 let $input = text.as_str();
256 let mut output = unsafe { $crate::callbacks::Output::from_raw(slot) };
257 let $out = &mut output;
258 $body
259 })
260 }
261 };
262 ($(#[$attr:meta])* $vis:vis fn $name:ident($action:ident, $input:ident, $out:ident, $data:ident) $body:block) => {
263 $(#[$attr])*
264 $vis extern "C" fn $name($action: u32, raw: $crate::StbStringRef, slot: *mut $crate::StbString,
265 $data: *mut std::ffi::c_void) -> i32 {
266 $crate::guard_or(1, || {
267 let text = unsafe { raw.as_str() }.to_owned();
269 let $input = text.as_str();
270 let mut output = unsafe { $crate::callbacks::Output::from_raw(slot) };
271 let $out = &mut output;
272 $body
273 })
274 }
275 };
276}
277
278#[macro_export]
283macro_rules! execute_handler {
284 ($(#[$attr:meta])* fn $name:ident($id:ident, $params:ident) $body:block) => {
285 $(#[$attr])*
286 extern "C" fn $name(
287 raw: $crate::StbStringRef,
288 params: $crate::StbString,
289 free: Option<$crate::FreeStringFn>,
290 ) -> $crate::StepHandle {
291 $crate::guard_or(std::ptr::null_mut(), || {
292 let $params = unsafe { $crate::callbacks::OwnedString::from_raw(params, free) };
296 let text = unsafe { raw.as_str() }.to_owned();
298 let $id = text.as_str();
299 $body
300 })
301 }
302 };
303 ($(#[$attr:meta])* fn $name:ident($id:ident, $params:ident, $free:ident) $body:block) => {
304 $(#[$attr])*
305 extern "C" fn $name(
306 raw: $crate::StbStringRef,
307 $params: $crate::StbString,
308 $free: Option<$crate::FreeStringFn>,
309 ) -> $crate::StepHandle {
310 $crate::guard_or(std::ptr::null_mut(), || {
311 let text = unsafe { raw.as_str() }.to_owned();
313 let $id = text.as_str();
314 $body
315 })
316 }
317 };
318}
319
320#[cfg(test)]
321mod tests {
322 use super::*;
323 use crate::StbStringRef;
324
325 thread_local! {
326 static OWNER_FREES: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
327 }
328
329 unsafe extern "C" fn counted_free(value: StbString) {
330 if !value.is_empty() {
331 OWNER_FREES.with(|count| count.set(count.get() + 1));
332 }
333 unsafe { free_string(value) };
335 }
336
337 execute_handler! { fn refusing_execute(_id, params) {
338 assert_eq!(params.to_string_lossy(), "{}");
339 std::ptr::null_mut()
340 } }
341
342 #[test]
343 fn execute_trampoline_reclaims_parameters_on_refusal_and_invalid_id() {
344 OWNER_FREES.with(|count| count.set(0));
345 assert!(refusing_execute(
346 StbStringRef::from_str("call"),
347 StbString::from_string("{}".into()),
348 Some(counted_free)
349 )
350 .is_null());
351 assert_eq!(OWNER_FREES.with(std::cell::Cell::get), 1);
352 let bytes = [0xff];
353 let id = StbStringRef {
354 ptr: bytes.as_ptr().cast(),
355 len: bytes.len(),
356 };
357 assert!(
358 refusing_execute(id, StbString::from_string("{}".into()), Some(counted_free)).is_null()
359 );
360 assert_eq!(OWNER_FREES.with(std::cell::Cell::get), 2);
361 }
362
363 #[test]
364 fn owned_strings_free_once_on_move_and_unwinding() {
365 OWNER_FREES.with(|count| count.set(0));
366 let raw = StbString::from_string("live".into());
367 let owner = unsafe { OwnedString::from_raw(raw, Some(counted_free)) };
369 assert_eq!(owner.to_string_lossy(), "live");
370 let moved = owner;
371 assert_eq!(moved.to_string_lossy(), "live");
372 drop(moved);
373 assert_eq!(OWNER_FREES.with(std::cell::Cell::get), 1);
374 let result = std::panic::catch_unwind(|| {
375 let raw = StbString::from_string("unwind".into());
376 let _owner = unsafe { OwnedString::from_raw(raw, Some(counted_free)) };
378 panic!("host parsing failed");
379 });
380 assert!(result.is_err());
381 assert_eq!(OWNER_FREES.with(std::cell::Cell::get), 2);
382 }
383
384 #[test]
385 fn into_raw_transfers_cleanup_to_the_receiver() {
386 let owner = OwnedString::from_string("transferred".into());
387 let raw = owner.into_raw();
388 let received = unsafe { OwnedString::from_raw(raw, Some(free_string)) };
390 assert_eq!(received.to_string_lossy(), "transferred");
391 drop(received);
392 }
393
394 #[test]
395 fn step_result_uses_the_supplied_producer_for_each_variant() {
396 OWNER_FREES.with(|count| count.set(0));
397 for (raw, expected) in [
398 (
399 StepResult::pending(StbString::from_string("progress".into())),
400 Step::Pending("progress".into()),
401 ),
402 (
403 StepResult::done(StbString::from_string("done".into())),
404 Step::Done("done".into()),
405 ),
406 (
407 StepResult::err(StbString::from_string("error".into())),
408 Step::Err("error".into()),
409 ),
410 ] {
411 assert_eq!(
413 unsafe { take_step_result_with(raw, counted_free) },
414 expected
415 );
416 }
417 assert_eq!(OWNER_FREES.with(std::cell::Cell::get), 3);
418 }
419
420 string_handler! { fn echo(input, output, _data) {
421 i32::from(!output.write(input.to_owned()))
422 } }
423 string_handler! { fn replace_twice(input, output, _data) {
424 output.write(input.to_owned());
425 output.write("second".into());
426 0
427 } }
428 string_handler! { fn panicking(_input, output, _data) {
429 output.write("valid output before panic".into());
430 panic!("callback failed");
431 } }
432 string_handler! { fn action(action, input, output, _data) {
433 output.write(format!("{action}:{input}"));
434 0
435 } }
436 string_handler! { fn discover((cwd, reason), output, _data) {
437 i32::from(!output.write(format!("{cwd}:{reason}")))
438 } }
439
440 #[test]
441 fn discovery_copies_both_inputs_and_handles_missing_output() {
442 let mut output = StbString::empty();
443 let cwd = StbStringRef::from_str("/项目");
444 let reason = StbStringRef::from_str("reload");
445 assert_eq!(discover(cwd, reason, &mut output, std::ptr::null_mut()), 0);
446 assert_eq!(
448 unsafe { OwnedString::from_raw(output, Some(free_string)) }.to_string_lossy(),
449 "/项目:reload"
450 );
451 assert_eq!(
452 discover(cwd, reason, std::ptr::null_mut(), std::ptr::null_mut()),
453 1
454 );
455 }
456
457 #[test]
458 fn malformed_second_discovery_input_does_not_write_output() {
459 let bytes = [0xff];
460 let reason = StbStringRef {
461 ptr: bytes.as_ptr().cast(),
462 len: bytes.len(),
463 };
464 let mut output = StbString::empty();
465 assert_eq!(
466 discover(
467 StbStringRef::from_str("/tmp"),
468 reason,
469 &mut output,
470 std::ptr::null_mut()
471 ),
472 1
473 );
474 assert!(output.is_empty());
475 }
476
477 #[test]
478 fn missing_output_is_reported_without_a_pointer_write() {
479 assert_eq!(
480 echo(
481 StbStringRef::from_str("text"),
482 std::ptr::null_mut(),
483 std::ptr::null_mut()
484 ),
485 1
486 );
487 }
488
489 #[test]
490 fn malformed_utf8_is_rejected_before_the_callback_writes_output() {
491 let bytes = [0xff, 0xfe];
492 let input = StbStringRef {
493 ptr: bytes.as_ptr().cast(),
494 len: bytes.len(),
495 };
496 let mut output = StbString::empty();
497 assert_eq!(echo(input, &mut output, std::ptr::null_mut()), 1);
498 assert!(output.is_empty());
499 }
500
501 #[test]
502 fn strings_are_copied_and_repeated_output_writes_are_reclaimed() {
503 let mut output = StbString::empty();
504 assert_eq!(
505 echo(
506 StbStringRef::from_str("你好"),
507 &mut output,
508 std::ptr::null_mut()
509 ),
510 0
511 );
512 assert_eq!(
514 unsafe { OwnedString::from_raw(output, Some(free_string)) }.to_string_lossy(),
515 "你好"
516 );
517 output = StbString::empty();
518 assert_eq!(
519 replace_twice(
520 StbStringRef::from_str("first"),
521 &mut output,
522 std::ptr::null_mut()
523 ),
524 0
525 );
526 assert_eq!(
528 unsafe { OwnedString::from_raw(output, Some(free_string)) }.to_string_lossy(),
529 "second"
530 );
531 }
532
533 #[test]
534 fn panics_leave_a_valid_reclaimable_output() {
535 let mut output = StbString::empty();
536 assert_eq!(
537 panicking(StbStringRef::empty(), &mut output, std::ptr::null_mut()),
538 1
539 );
540 assert_eq!(
542 unsafe { OwnedString::from_raw(output, Some(free_string)) }.to_string_lossy(),
543 "valid output before panic"
544 );
545 }
546
547 #[test]
548 fn runtime_action_signature_is_preserved() {
549 let mut output = StbString::empty();
550 assert_eq!(
551 action(
552 17,
553 StbStringRef::from_str("{}"),
554 &mut output,
555 std::ptr::null_mut()
556 ),
557 0
558 );
559 assert_eq!(
561 unsafe { OwnedString::from_raw(output, Some(free_string)) }.to_string_lossy(),
562 "17:{}"
563 );
564 }
565
566 #[test]
567 fn writing_does_not_reclaim_an_initial_host_owned_value() {
568 let initial = StbString::from_string("host-owned".into());
569 let mut output = initial;
570 assert_eq!(
571 echo(
572 StbStringRef::from_str("plugin-owned"),
573 &mut output,
574 std::ptr::null_mut()
575 ),
576 0
577 );
578 assert_eq!(
580 unsafe { OwnedString::from_raw(initial, Some(free_string)) }.to_string_lossy(),
581 "host-owned"
582 );
583 assert_eq!(
585 unsafe { OwnedString::from_raw(output, Some(free_string)) }.to_string_lossy(),
586 "plugin-owned"
587 );
588 }
589
590 #[test]
591 fn every_step_result_variant_can_be_reclaimed() {
592 unsafe {
594 free_step_result(StepResult::pending(StbString::from_string(
595 "progress".into(),
596 )));
597 free_step_result(StepResult::done(StbString::from_string("done".into())));
598 free_step_result(StepResult::err(StbString::from_string("error".into())));
599 free_string(StbString::empty());
600 assert_eq!(
601 take_step_result!(StepResult::err(StbString::from_string("cancelled".into()))),
602 Step::Err("cancelled".into())
603 );
604 }
605 }
606}