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 self.as_str_lossy().into_owned()
44 }
45
46 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 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 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 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 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 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 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 let _ = owner.into_raw();
135 }
136 }
137}
138
139pub unsafe extern "C" fn free_string(value: StbString) {
154 if value.is_empty() || value.ptr.is_null() {
155 return;
156 }
157 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
167pub unsafe extern "C" fn free_step_result(result: StepResult) {
174 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 unsafe { free_string(text) };
184}
185
186#[derive(Debug, PartialEq, Eq)]
188pub enum Step {
189 Pending(String),
190 Done(String),
191 Err(String),
192}
193
194pub unsafe fn take_step_result(result: StepResult) -> Step {
198 unsafe { take_step_result_with(result, free_string) }
200}
201
202pub unsafe fn take_step_result_with(result: StepResult, free: FreeStringFn) -> Step {
209 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 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#[macro_export]
238macro_rules! take_step_result {
239 ($result:expr) => {{
240 $crate::callbacks::take_step_result($result)
241 }};
242}
243
244#[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 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 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 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#[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 let $params = unsafe { $crate::callbacks::OwnedString::from_raw(params, free) };
316 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 assert_eq!(
635 unsafe { OwnedString::from_raw(initial, Some(free_string)) }.to_string_lossy(),
636 "host-owned"
637 );
638 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 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}