1#![allow(unsafe_code)]
25
26use core::{marker::PhantomData, ptr, slice};
27
28#[repr(C)]
35#[derive(Clone, Copy)]
36pub struct BorrowedStr<'a> {
37 pub ptr: *const u8,
38 pub len: usize,
39 _phantom: PhantomData<&'a [u8]>,
40}
41
42unsafe impl Send for BorrowedStr<'_> {}
46unsafe impl Sync for BorrowedStr<'_> {}
48
49impl<'a> BorrowedStr<'a> {
50 #[must_use]
52 pub const fn empty() -> Self {
53 Self {
54 ptr: ptr::null(),
55 len: 0,
56 _phantom: PhantomData,
57 }
58 }
59
60 #[must_use]
62 pub const fn from_str(s: &'a str) -> Self {
63 Self {
64 ptr: s.as_ptr(),
65 len: s.len(),
66 _phantom: PhantomData,
67 }
68 }
69
70 #[must_use]
77 pub unsafe fn as_str(&self) -> &'a str {
78 if self.ptr.is_null() || self.len == 0 {
79 return "";
80 }
81 let bytes = unsafe { slice::from_raw_parts(self.ptr, self.len) };
83 unsafe { core::str::from_utf8_unchecked(bytes) }
85 }
86
87 pub unsafe fn try_as_str(&self) -> Result<&'a str, core::str::Utf8Error> {
100 if self.ptr.is_null() || self.len == 0 {
101 return Ok("");
102 }
103 let bytes = unsafe { slice::from_raw_parts(self.ptr, self.len) };
105 core::str::from_utf8(bytes)
106 }
107
108 #[must_use]
115 pub unsafe fn to_string_lossy(&self) -> String {
116 if self.ptr.is_null() || self.len == 0 {
117 return String::new();
118 }
119 let bytes = unsafe { slice::from_raw_parts(self.ptr, self.len) };
121 String::from_utf8_lossy(bytes).into_owned()
122 }
123}
124
125impl core::fmt::Debug for BorrowedStr<'_> {
126 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
127 let s = unsafe { self.to_string_lossy() };
132 write!(f, "BorrowedStr({s:?})")
133 }
134}
135
136#[repr(C)]
141#[derive(Clone, Copy)]
142pub struct Slice<'a, T> {
143 pub ptr: *const T,
144 pub len: usize,
145 _phantom: PhantomData<&'a [T]>,
146}
147
148unsafe impl<T: Sync> Send for Slice<'_, T> {}
150unsafe impl<T: Sync> Sync for Slice<'_, T> {}
152
153impl<'a, T> Slice<'a, T> {
154 #[must_use]
156 pub const fn empty() -> Self {
157 Self {
158 ptr: ptr::null(),
159 len: 0,
160 _phantom: PhantomData,
161 }
162 }
163
164 #[must_use]
166 pub const fn from_slice(s: &'a [T]) -> Self {
167 Self {
168 ptr: s.as_ptr(),
169 len: s.len(),
170 _phantom: PhantomData,
171 }
172 }
173
174 #[must_use]
180 pub unsafe fn as_slice(&self) -> &'a [T] {
181 if self.ptr.is_null() || self.len == 0 {
182 return &[];
183 }
184 unsafe { slice::from_raw_parts(self.ptr, self.len) }
186 }
187}
188
189#[repr(u32)]
193#[derive(Clone, Copy, Debug, PartialEq, Eq)]
194pub enum PluginErrorCode {
195 Ok = 0,
196 Generic = 1,
197 Panic = 2,
198 InvalidArgument = 3,
199 NotImplemented = 4,
200 AbiMismatch = 5,
201 SerializationFailed = 6,
202}
203
204#[repr(C)]
211pub struct OwnedBytes {
212 pub ptr: *mut u8,
213 pub len: usize,
214 pub cap: usize,
215 pub drop_fn: Option<unsafe extern "C" fn(ptr: *mut u8, len: usize, cap: usize)>,
216}
217
218unsafe impl Send for OwnedBytes {}
221
222impl OwnedBytes {
223 #[must_use]
225 pub const fn empty() -> Self {
226 Self {
227 ptr: ptr::null_mut(),
228 len: 0,
229 cap: 0,
230 drop_fn: None,
231 }
232 }
233
234 #[must_use]
236 pub fn is_empty(&self) -> bool {
237 self.len == 0 || self.ptr.is_null()
238 }
239
240 #[must_use]
251 pub fn from_vec(v: Vec<u8>) -> Self {
252 let (ptr, len, cap) = v.into_raw_parts();
253
254 Self {
255 ptr,
256 len,
257 cap,
258 drop_fn: Some(drop_owned_bytes),
259 }
260 }
261
262 #[must_use]
268 pub unsafe fn as_bytes(&self) -> &[u8] {
269 if self.is_empty() {
270 return &[];
271 }
272 unsafe { slice::from_raw_parts(self.ptr, self.len) }
274 }
275}
276
277impl Drop for OwnedBytes {
278 fn drop(&mut self) {
279 if let Some(f) = self.drop_fn.take()
280 && !self.ptr.is_null()
281 {
282 unsafe { f(self.ptr, self.len, self.cap) };
285 self.ptr = ptr::null_mut();
286 self.len = 0;
287 self.cap = 0;
288 }
289 }
290}
291
292pub unsafe extern "C" fn drop_owned_bytes(ptr: *mut u8, len: usize, cap: usize) {
300 if ptr.is_null() {
301 return;
302 }
303
304 unsafe {
306 let _ = Vec::from_raw_parts(ptr, len, cap);
307 }
308}
309
310#[repr(C)]
315pub struct PluginError {
316 pub code: PluginErrorCode,
317 pub message: OwnedBytes,
318}
319
320impl PluginError {
321 #[must_use]
323 pub fn generic(message: impl AsRef<str>) -> Self {
324 Self {
325 code: PluginErrorCode::Generic,
326 message: OwnedBytes::from_vec(message.as_ref().as_bytes().to_vec()),
327 }
328 }
329
330 #[must_use]
332 pub fn new(code: PluginErrorCode, message: impl AsRef<str>) -> Self {
333 Self {
334 code,
335 message: OwnedBytes::from_vec(message.as_ref().as_bytes().to_vec()),
336 }
337 }
338
339 #[must_use]
341 pub fn panic(message: impl AsRef<str>) -> Self {
342 Self::new(PluginErrorCode::Panic, message)
343 }
344
345 #[must_use]
347 pub fn message_string(&self) -> String {
348 let bytes = unsafe { self.message.as_bytes() };
350 String::from_utf8_lossy(bytes).into_owned()
351 }
352}
353
354impl core::fmt::Debug for PluginError {
355 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
356 f.debug_struct(stringify!(PluginError))
357 .field("code", &self.code)
358 .field("message", &self.message_string())
359 .finish()
360 }
361}
362
363#[repr(C, u8)]
368pub enum PluginResult<T> {
369 Ok(T),
370 Err(PluginError),
371}
372
373impl<T> PluginResult<T> {
374 pub fn into_result(self) -> Result<T, PluginError> {
381 match self {
382 Self::Ok(t) => Ok(t),
383 Self::Err(e) => Err(e),
384 }
385 }
386
387 pub fn from_result(r: Result<T, PluginError>) -> Self {
389 match r {
390 Ok(t) => Self::Ok(t),
391 Err(e) => Self::Err(e),
392 }
393 }
394}
395
396#[cfg(test)]
397mod tests {
398 use std::sync::atomic::{AtomicUsize, Ordering};
399
400 use rstest::rstest;
401
402 use super::*;
403
404 #[rstest]
405 #[case::ascii("hello")]
406 #[case::empty("")]
407 #[case::utf8("héllo wörld")]
408 #[case::multibyte("\u{1F600}\u{1F4A9}")]
409 fn borrowed_str_round_trips(#[case] s: &str) {
410 let b = BorrowedStr::from_str(s);
411 let back = unsafe { b.as_str() };
413 assert_eq!(back, s);
414 }
415
416 #[rstest]
417 fn borrowed_str_try_as_str_rejects_invalid_utf8() {
418 static INVALID_UTF8: [u8; 1] = [0xFF];
419 let mut b = BorrowedStr::empty();
420 b.ptr = INVALID_UTF8.as_ptr();
421 b.len = INVALID_UTF8.len();
422
423 let result = unsafe { b.try_as_str() };
425
426 assert!(result.is_err());
427 }
428
429 #[rstest]
430 fn borrowed_str_to_string_lossy_replaces_invalid_utf8() {
431 static INVALID_UTF8: [u8; 1] = [0xFF];
432 let mut b = BorrowedStr::empty();
433 b.ptr = INVALID_UTF8.as_ptr();
434 b.len = INVALID_UTF8.len();
435
436 let rendered = unsafe { b.to_string_lossy() };
438
439 assert_eq!(rendered, "\u{FFFD}");
440 }
441
442 #[rstest]
443 fn slice_round_trips() {
444 let data: [u32; 3] = [1, 2, 3];
445 let s = Slice::from_slice(&data);
446 let back = unsafe { s.as_slice() };
448 assert_eq!(back, &[1u32, 2, 3]);
449 }
450
451 #[rstest]
452 fn empty_slice_returns_empty() {
453 let s: Slice<u8> = Slice::empty();
454 let back = unsafe { s.as_slice() };
456 assert!(back.is_empty());
457 }
458
459 #[rstest]
460 fn owned_bytes_round_trip_and_drop() {
461 let payload = b"hello world".to_vec();
462 let owned = OwnedBytes::from_vec(payload.clone());
463 let view = unsafe { owned.as_bytes() }.to_vec();
465 assert_eq!(view, payload);
466 drop(owned);
467 }
468
469 #[rstest]
470 fn owned_bytes_drop_fn_runs_exactly_once() {
471 static COUNTER: AtomicUsize = AtomicUsize::new(0);
472 unsafe extern "C" fn counting_drop(ptr: *mut u8, len: usize, cap: usize) {
473 if !ptr.is_null() {
474 COUNTER.fetch_add(1, Ordering::SeqCst);
475 unsafe {
477 let _ = Vec::from_raw_parts(ptr, len, cap);
478 }
479 }
480 }
481
482 COUNTER.store(0, Ordering::SeqCst);
483 let (ptr, len, cap) = vec![1u8, 2, 3, 4].into_raw_parts();
484
485 let owned = OwnedBytes {
486 ptr,
487 len,
488 cap,
489 drop_fn: Some(counting_drop),
490 };
491 assert_eq!(COUNTER.load(Ordering::SeqCst), 0);
492 drop(owned);
493 assert_eq!(COUNTER.load(Ordering::SeqCst), 1);
494 }
495
496 #[rstest]
497 fn plugin_error_carries_message() {
498 let err = PluginError::generic("bad input");
499 assert_eq!(err.code, PluginErrorCode::Generic);
500 assert_eq!(err.message_string(), "bad input");
501 }
502
503 #[rstest]
504 fn plugin_result_round_trips() {
505 let ok: PluginResult<u32> = PluginResult::Ok(42);
506 let r = ok.into_result();
507 assert_eq!(r.unwrap(), 42);
508
509 let err: PluginResult<u32> = PluginResult::Err(PluginError::generic("nope"));
510 let r = err.into_result();
511 assert!(r.is_err());
512 }
513
514 #[rstest]
515 fn plugin_result_from_result_round_trips() {
516 let r: PluginResult<u32> = PluginResult::from_result(Ok(7));
517 assert_eq!(r.into_result().unwrap(), 7);
518
519 let r: PluginResult<u32> = PluginResult::from_result(Err(PluginError::generic("x")));
520 let e = r.into_result().unwrap_err();
521 assert_eq!(e.code, PluginErrorCode::Generic);
522 assert_eq!(e.message_string(), "x");
523 }
524
525 #[rstest]
526 fn borrowed_str_empty_is_empty_when_borrowed_back() {
527 let b = BorrowedStr::empty();
528 assert!(b.ptr.is_null());
529 assert_eq!(b.len, 0);
530 assert_eq!(unsafe { b.as_str() }, "");
532 }
533
534 #[rstest]
535 fn borrowed_str_debug_prints_contents() {
536 let b = BorrowedStr::from_str("hello");
537 let rendered = format!("{b:?}");
538 assert!(rendered.contains("hello"));
539 }
540
541 #[rstest]
542 fn slice_empty_descriptor_is_null_and_zero_len() {
543 let s: Slice<u32> = Slice::empty();
544 assert!(s.ptr.is_null());
545 assert_eq!(s.len, 0);
546 }
547
548 #[rstest]
549 fn owned_bytes_empty_is_empty() {
550 let owned = OwnedBytes::empty();
551 assert!(owned.is_empty());
552 assert!(owned.ptr.is_null());
553 assert_eq!(owned.len, 0);
554 assert_eq!(owned.cap, 0);
555 assert!(owned.drop_fn.is_none());
556 assert!(unsafe { owned.as_bytes() }.is_empty());
558 }
559
560 #[rstest]
561 fn owned_bytes_is_empty_for_zero_length_buffer() {
562 let owned = OwnedBytes::from_vec(Vec::new());
563 assert!(owned.is_empty());
564 drop(owned);
565 }
566
567 #[rstest]
568 fn owned_bytes_drop_with_null_ptr_short_circuits() {
569 let owned = OwnedBytes {
570 ptr: ptr::null_mut(),
571 len: 0,
572 cap: 0,
573 drop_fn: Some(drop_owned_bytes),
574 };
575 drop(owned);
577 }
578
579 #[rstest]
580 fn drop_owned_bytes_handles_null_ptr_without_panic() {
581 unsafe {
583 drop_owned_bytes(ptr::null_mut(), 0, 0);
584 };
585 }
586
587 #[rstest]
588 fn drop_owned_bytes_frees_vec_leaked_with_from_vec_layout() {
589 let (ptr, len, cap) = vec![1u8, 2, 3, 4, 5].into_raw_parts();
590 unsafe {
594 drop_owned_bytes(ptr, len, cap);
595 };
596 }
597
598 #[rstest]
599 fn plugin_error_new_carries_code_and_message() {
600 let err = PluginError::new(PluginErrorCode::InvalidArgument, "bad arg");
601 assert_eq!(err.code, PluginErrorCode::InvalidArgument);
602 assert_eq!(err.message_string(), "bad arg");
603 }
604
605 #[rstest]
606 fn plugin_error_panic_sets_panic_code() {
607 let err = PluginError::panic("oops");
608 assert_eq!(err.code, PluginErrorCode::Panic);
609 assert_eq!(err.message_string(), "oops");
610 }
611
612 #[rstest]
613 fn plugin_error_debug_renders_code_and_message() {
614 let err = PluginError::new(PluginErrorCode::NotImplemented, "todo");
615 let rendered = format!("{err:?}");
616 assert!(rendered.contains("NotImplemented"));
617 assert!(rendered.contains("todo"));
618 }
619
620 #[rstest]
621 #[case::ok(PluginErrorCode::Ok, 0u32)]
622 #[case::generic(PluginErrorCode::Generic, 1)]
623 #[case::panic(PluginErrorCode::Panic, 2)]
624 #[case::invalid_argument(PluginErrorCode::InvalidArgument, 3)]
625 #[case::not_implemented(PluginErrorCode::NotImplemented, 4)]
626 #[case::abi_mismatch(PluginErrorCode::AbiMismatch, 5)]
627 #[case::serialization_failed(PluginErrorCode::SerializationFailed, 6)]
628 fn plugin_error_code_has_stable_discriminant(
629 #[case] code: PluginErrorCode,
630 #[case] expected: u32,
631 ) {
632 assert_eq!(code as u32, expected);
633 }
634}