unparser_shared/scaffold.rs
1//! Macros that assemble a C entry point out of the primitives in [`crate::ffi`].
2//!
3//! # What is repeated, and why a macro
4//!
5//! [`crate::ffi`] hands out the *materials* — a last-error slot, a panic guard, the two
6//! boundary failure reasons. Assembling them is five steps, and only one of them says
7//! anything about the library:
8//!
9//! 1. clear the slot, so a caller polling it does not read a previous failure. The slot is
10//! cleared on entry, not on success: a body that itself calls another entry point on
11//! this thread leaves that call's failure visible
12//! 2. reject a null handle with [`crate::ffi::invalid_argument`] and return the sentinel
13//! 3. run the body inside [`crate::ffi::catch`] ← the only step with domain content
14//! 4. move the result across the ABI, reporting [`crate::ffi::invalid_output`] if it cannot
15//! 5. record a failure and return the sentinel
16//!
17//! Steps 1, 2, 4 and 5 are about eighteen lines with no library content in them, and the
18//! sentinel in steps 2 and 5 differs by return type — null for a pointer, `-1` for a
19//! count. That is why these are several macros rather than one: a single general macro
20//! would have to be told the sentinel, and being told is how the wrong one gets used.
21//!
22//! # Names are spelled out, not built
23//!
24//! Every macro takes the exported function name in full, following
25//! [`crate::export_last_error_abi!`]: `macro_rules!` cannot concatenate identifiers, and
26//! an exported symbol appearing literally in the source is what someone greps for.
27//! Doc comments are passed through from the call site, because what an entry point means
28//! is not something this crate knows.
29
30/// Read a nullable C string argument as `&str`, classifying both ways it can fail.
31///
32/// Expands to a `Result`, so the usual form is `with_c_str!(path)?` inside a
33/// [`catch`](crate::ffi::catch) closure. The null message is built from the argument's own
34/// name — `with_c_str!(resource_id)` reports `"resource_id is null"`, which is the wording
35/// the family already uses.
36///
37/// Use this where the entry point has no out-parameter, so that classifying a null
38/// argument inside the closure is indistinguishable from classifying it before. Where
39/// there *is* an out-parameter, check for null first and use
40/// [`ffi::c_str_utf8`](crate::ffi::c_str_utf8) here instead — see
41/// [`export_bytes_getter!`](crate::export_bytes_getter).
42///
43/// # Safety
44///
45/// `$ptr` must be null or point to a NUL-terminated string. This expands to an unsafe
46/// read, so it has to appear in an `unsafe` context.
47///
48/// ```
49/// # use std::ffi::{c_char, CString};
50/// # use unparser_shared::ffi::FfiError;
51/// unsafe fn name_length(path: *const c_char) -> Result<usize, FfiError> {
52/// let path = unsafe { unparser_shared::with_c_str!(path) }?;
53/// Ok(path.len())
54/// }
55///
56/// let path = CString::new("a.txt").unwrap();
57/// assert_eq!(unsafe { name_length(path.as_ptr()) }.unwrap(), 5);
58/// assert_eq!(
59/// unsafe { name_length(std::ptr::null()) }.unwrap_err().1,
60/// "path is null"
61/// );
62/// ```
63#[macro_export]
64macro_rules! with_c_str {
65 ($ptr:ident) => {
66 if $ptr.is_null() {
67 ::std::result::Result::Err($crate::ffi::invalid_argument(concat!(
68 stringify!($ptr),
69 " is null"
70 )))
71 } else {
72 $crate::ffi::c_str_utf8($ptr)
73 }
74 };
75}
76
77/// Declare an opaque document handle and the function that frees it.
78///
79/// The handle is what a C caller holds: a `#[repr(C)]` newtype around the library's own
80/// document type with one private field. The field name is given rather than invented,
81/// because every entry point projects through it (`(*doc).inner`) and that has to be
82/// readable in the consuming source.
83///
84/// `#[repr(C)]` is emitted because the three shipped libraries declare it. It says
85/// nothing useful about a Rust field, and it is kept only so that adopting this macro
86/// leaves the declared type identical.
87///
88/// ```
89/// # struct Document;
90/// unparser_shared::export_handle! {
91/// /// Opaque handle to a parsed document.
92/// handle DemoDoc { inner: Document },
93///
94/// /// Free a document handle.
95/// ///
96/// /// # Safety
97/// ///
98/// /// - `doc` must be a pointer a parse function returned, or null.
99/// /// - After this call the handle is invalid and must not be used.
100/// free demo_release_document,
101/// }
102/// ```
103#[macro_export]
104macro_rules! export_handle {
105 (
106 $(#[$handle_meta:meta])*
107 handle $handle:ident { $field:ident: $inner:ty },
108
109 $(#[$free_meta:meta])*
110 free $free_fn:ident $(,)?
111 ) => {
112 $(#[$handle_meta])*
113 #[repr(C)]
114 pub struct $handle {
115 $field: $inner,
116 }
117
118 $(#[$free_meta])*
119 #[no_mangle]
120 pub unsafe extern "C" fn $free_fn(doc: *mut $handle) {
121 if !doc.is_null() {
122 // Wrapped explicitly rather than relying on the unsafe fn body: this
123 // expands both in crates that deny `unsafe_op_in_unsafe_fn` and in crates
124 // that do not, and a block around a genuinely unsafe operation is correct
125 // under either.
126 let _ = unsafe { ::std::boxed::Box::from_raw(doc) };
127 }
128 }
129 };
130}
131
132/// Declare the release function for strings this library hands out.
133///
134/// Every entry point returning `*mut c_char` transfers ownership, so exactly one of these
135/// belongs beside them. Reclaiming the allocation has to happen in the library that made
136/// it, which is why it cannot be `free()` on the caller's side.
137///
138/// ```
139/// unparser_shared::export_free_string!(
140/// /// Free a string produced by this library.
141/// ///
142/// /// # Safety
143/// ///
144/// /// - `s` must be a pointer this library returned, or null.
145/// demo_release_string
146/// );
147/// ```
148#[macro_export]
149macro_rules! export_free_string {
150 ($(#[$meta:meta])* $free_fn:ident) => {
151 $(#[$meta])*
152 #[no_mangle]
153 pub unsafe extern "C" fn $free_fn(s: *mut ::std::ffi::c_char) {
154 if !s.is_null() {
155 let _ = unsafe { ::std::ffi::CString::from_raw(s) };
156 }
157 }
158 };
159}
160
161/// Declare the release function for byte buffers this library hands out.
162///
163/// The length is taken back because the buffer was a `Box<[u8]>`: reconstructing it needs
164/// the same length the producing call reported. A zero length is ignored rather than
165/// reclaimed — an empty boxed slice has no allocation to return.
166///
167/// ```
168/// unparser_shared::export_free_bytes!(
169/// /// Free a byte buffer produced by this library.
170/// ///
171/// /// # Safety
172/// ///
173/// /// - `data` must be a pointer this library returned, or null.
174/// /// - `len` must be the length that call reported.
175/// demo_release_bytes
176/// );
177/// ```
178#[macro_export]
179macro_rules! export_free_bytes {
180 ($(#[$meta:meta])* $free_fn:ident) => {
181 $(#[$meta])*
182 #[no_mangle]
183 pub unsafe extern "C" fn $free_fn(data: *mut u8, len: usize) {
184 if !data.is_null() && len > 0 {
185 let _ = unsafe {
186 ::std::boxed::Box::from_raw(::std::ptr::slice_from_raw_parts_mut(
187 data, len,
188 ))
189 };
190 }
191 }
192 };
193}
194
195/// Declare an entry point that hands out an owned string.
196///
197/// `$body` runs inside [`catch`](crate::ffi::catch) and evaluates to
198/// `Result<String, FfiError>`. Everything around it is the same in every such entry
199/// point: clear the slot, reject a null handle, move the string into a `CString`,
200/// report [`invalid_output`](crate::ffi::invalid_output) if it holds an interior NUL,
201/// record a failure and return null.
202///
203/// The handle parameter's name is taken from the call site so that `$body` can project
204/// through it — `{ Ok((*doc).inner.title()) }`. The handle is `*const`, since producing a
205/// string does not modify the document.
206///
207/// The null-handle message is `"document is null"`, fixed here so the family reports it
208/// identically — the same reason [`invalid_output`](crate::ffi::invalid_output)'s wording
209/// is fixed.
210///
211/// ```
212/// # use std::ffi::{c_int, CStr};
213/// # use unparser_shared::ffi::LastErrorSlot;
214/// # struct Report { lines: Vec<String> }
215/// # thread_local! { static LAST_ERROR: LastErrorSlot = const { LastErrorSlot::new() }; }
216/// # unparser_shared::export_last_error_abi!(LAST_ERROR, demo2_last_error, demo2_last_error_kind);
217/// # unparser_shared::export_handle! {
218/// # /// Handle.
219/// # handle Demo2Document { inner: Report },
220/// # /// Free.
221/// # ///
222/// # /// # Safety
223/// # /// `doc` must come from this library.
224/// # free demo2_free_document,
225/// # }
226/// # unparser_shared::export_free_string!(
227/// # /// Free.
228/// # ///
229/// # /// # Safety
230/// # /// `s` must come from this library.
231/// # demo2_free_string
232/// # );
233/// unparser_shared::export_string_getter!(
234/// /// The report's line at `index`, or a failure when it is out of range.
235/// ///
236/// /// # Safety
237/// ///
238/// /// - `doc` must be a valid handle.
239/// /// - The returned string must be freed with `demo2_free_string`.
240/// LAST_ERROR,
241/// demo2_line(doc: Demo2Document, index: c_int),
242/// {
243/// match unsafe { (&(*doc).inner.lines).get(index as usize) } {
244/// Some(line) => Ok(line.clone()),
245/// None => Err((unparser_shared::kind::OTHER, format!("no line {index}"))),
246/// }
247/// }
248/// );
249///
250/// let doc = Box::into_raw(Box::new(Demo2Document {
251/// inner: Report { lines: vec!["first".to_string()] },
252/// }));
253///
254/// let line = unsafe { demo2_line(doc, 0) };
255/// assert_eq!(unsafe { CStr::from_ptr(line) }.to_str().unwrap(), "first");
256/// unsafe { demo2_free_string(line) };
257///
258/// assert!(unsafe { demo2_line(doc, 9) }.is_null());
259/// assert_eq!(demo2_last_error_kind(), unparser_shared::kind::OTHER);
260///
261/// unsafe { demo2_free_document(doc) };
262/// ```
263#[macro_export]
264macro_rules! export_string_getter {
265 (
266 $(#[$meta:meta])*
267 $slot:path,
268 $name:ident($handle:ident: $handle_ty:ty $(, $arg:ident: $arg_ty:ty)* $(,)?),
269 $body:block
270 ) => {
271 $(#[$meta])*
272 #[no_mangle]
273 pub unsafe extern "C" fn $name(
274 $handle: *const $handle_ty,
275 $($arg: $arg_ty,)*
276 ) -> *mut ::std::ffi::c_char {
277 $slot.with(|slot| $crate::ffi::LastErrorSlot::clear(slot));
278
279 if $handle.is_null() {
280 $slot.with(|slot| {
281 $crate::ffi::LastErrorSlot::set_error(
282 slot,
283 &$crate::ffi::invalid_argument("document is null"),
284 )
285 });
286 return ::std::ptr::null_mut();
287 }
288
289 let produced: ::std::result::Result<::std::string::String, $crate::ffi::FfiError> =
290 $crate::ffi::catch(|| $body);
291
292 match produced {
293 ::std::result::Result::Ok(text) => match ::std::ffi::CString::new(text) {
294 ::std::result::Result::Ok(owned) => owned.into_raw(),
295 ::std::result::Result::Err(_) => {
296 $slot.with(|slot| {
297 $crate::ffi::LastErrorSlot::set_error(
298 slot,
299 &$crate::ffi::invalid_output(),
300 )
301 });
302 ::std::ptr::null_mut()
303 }
304 },
305 ::std::result::Result::Err(error) => {
306 $slot.with(|slot| $crate::ffi::LastErrorSlot::set_error(slot, &error));
307 ::std::ptr::null_mut()
308 }
309 }
310 }
311 };
312}
313
314/// Declare an entry point that hands out a string which may legitimately be absent.
315///
316/// `$body` evaluates to `Result<Option<String>, FfiError>`. `Ok(None)` returns null with
317/// the slot left at success, because absence is not a failure — document metadata that was
318/// never set is the case this exists for.
319///
320/// That makes null ambiguous on its own, and deliberately so: the kind channel is what
321/// separates the three outcomes a caller has to tell apart.
322///
323/// | `$body` | Returns | Kind |
324/// |---|---|---|
325/// | `Ok(Some(text))` | the string | success |
326/// | `Ok(None)` | null | success — there is nothing to give |
327/// | `Ok(Some(text))` holding an interior NUL | null | [`kind::INVALID_OUTPUT`](crate::kind::INVALID_OUTPUT) |
328/// | `Err(..)` | null | whatever the body classified |
329///
330/// Use [`export_string_getter!`](crate::export_string_getter) where absence cannot happen
331/// or is genuinely an empty string. Do not fold the two by defaulting absent to `""` —
332/// that tells a caller the field is set and blank.
333///
334/// ```
335/// # use unparser_shared::ffi::LastErrorSlot;
336/// # struct Meta { author: Option<String> }
337/// # thread_local! { static LAST_ERROR: LastErrorSlot = const { LastErrorSlot::new() }; }
338/// # unparser_shared::export_last_error_abi!(LAST_ERROR, demo3_last_error, demo3_last_error_kind);
339/// # unparser_shared::export_handle! {
340/// # /// Handle.
341/// # handle Demo3Document { inner: Meta },
342/// # /// Free.
343/// # ///
344/// # /// # Safety
345/// # /// `doc` must come from this library.
346/// # free demo3_free_document,
347/// # }
348/// unparser_shared::export_optional_string_getter!(
349/// /// The document author, or null when none is recorded.
350/// ///
351/// /// # Safety
352/// ///
353/// /// - `doc` must be a valid handle.
354/// /// - The returned string must be freed by this library.
355/// LAST_ERROR,
356/// demo3_author(doc: Demo3Document),
357/// { Ok(unsafe { (*doc).inner.author.clone() }) }
358/// );
359///
360/// let absent = Box::into_raw(Box::new(Demo3Document { inner: Meta { author: None } }));
361/// assert!(unsafe { demo3_author(absent) }.is_null());
362/// assert_eq!(demo3_last_error_kind(), unparser_shared::kind::NONE);
363/// unsafe { demo3_free_document(absent) };
364/// ```
365#[macro_export]
366macro_rules! export_optional_string_getter {
367 (
368 $(#[$meta:meta])*
369 $slot:path,
370 $name:ident($handle:ident: $handle_ty:ty $(, $arg:ident: $arg_ty:ty)* $(,)?),
371 $body:block
372 ) => {
373 $(#[$meta])*
374 #[no_mangle]
375 pub unsafe extern "C" fn $name(
376 $handle: *const $handle_ty,
377 $($arg: $arg_ty,)*
378 ) -> *mut ::std::ffi::c_char {
379 $slot.with(|slot| $crate::ffi::LastErrorSlot::clear(slot));
380
381 if $handle.is_null() {
382 $slot.with(|slot| {
383 $crate::ffi::LastErrorSlot::set_error(
384 slot,
385 &$crate::ffi::invalid_argument("document is null"),
386 )
387 });
388 return ::std::ptr::null_mut();
389 }
390
391 let produced: ::std::result::Result<
392 ::std::option::Option<::std::string::String>,
393 $crate::ffi::FfiError,
394 > = $crate::ffi::catch(|| $body);
395
396 match produced {
397 ::std::result::Result::Ok(::std::option::Option::Some(text)) => {
398 match ::std::ffi::CString::new(text) {
399 ::std::result::Result::Ok(owned) => owned.into_raw(),
400 ::std::result::Result::Err(_) => {
401 $slot.with(|slot| {
402 $crate::ffi::LastErrorSlot::set_error(
403 slot,
404 &$crate::ffi::invalid_output(),
405 )
406 });
407 ::std::ptr::null_mut()
408 }
409 }
410 }
411 ::std::result::Result::Ok(::std::option::Option::None) => {
412 ::std::ptr::null_mut()
413 }
414 ::std::result::Result::Err(error) => {
415 $slot.with(|slot| $crate::ffi::LastErrorSlot::set_error(slot, &error));
416 ::std::ptr::null_mut()
417 }
418 }
419 }
420 };
421}
422
423/// Declare an entry point that hands out a count.
424///
425/// `$body` evaluates to `Result<c_int, FfiError>`. Failure — including a null handle —
426/// returns `-1`, which is why this is its own macro rather than a sentinel argument to a
427/// general one: a sentinel that can be passed in is a sentinel that can be passed wrong.
428///
429/// The slot is cleared on entry like everywhere else, and here that is load-bearing rather
430/// than tidy. A count getter returns a value instead of a pointer, so a caller polling
431/// `<lib>_last_error_kind` after it has no other way to know the recorded failure is not
432/// someone else's.
433///
434/// ```
435/// # use unparser_shared::ffi::LastErrorSlot;
436/// # struct Book { pages: Vec<u8> }
437/// # thread_local! { static LAST_ERROR: LastErrorSlot = const { LastErrorSlot::new() }; }
438/// # unparser_shared::export_last_error_abi!(LAST_ERROR, demo4_last_error, demo4_last_error_kind);
439/// # unparser_shared::export_handle! {
440/// # /// Handle.
441/// # handle Demo4Document { inner: Book },
442/// # /// Free.
443/// # ///
444/// # /// # Safety
445/// # /// `doc` must come from this library.
446/// # free demo4_free_document,
447/// # }
448/// unparser_shared::export_count_getter!(
449/// /// The number of pages, or -1 on failure.
450/// ///
451/// /// # Safety
452/// ///
453/// /// - `doc` must be a valid handle.
454/// LAST_ERROR,
455/// demo4_page_count(doc: Demo4Document),
456/// { Ok(unsafe { (*doc).inner.pages.len() } as std::ffi::c_int) }
457/// );
458///
459/// assert_eq!(unsafe { demo4_page_count(std::ptr::null()) }, -1);
460/// assert_eq!(demo4_last_error_kind(), unparser_shared::kind::INVALID_ARGUMENT);
461///
462/// let doc = Box::into_raw(Box::new(Demo4Document { inner: Book { pages: vec![1, 2] } }));
463/// assert_eq!(unsafe { demo4_page_count(doc) }, 2);
464/// assert_eq!(demo4_last_error_kind(), unparser_shared::kind::NONE);
465/// unsafe { demo4_free_document(doc) };
466/// ```
467#[macro_export]
468macro_rules! export_count_getter {
469 (
470 $(#[$meta:meta])*
471 $slot:path,
472 $name:ident($handle:ident: $handle_ty:ty $(, $arg:ident: $arg_ty:ty)* $(,)?),
473 $body:block
474 ) => {
475 $(#[$meta])*
476 #[no_mangle]
477 pub unsafe extern "C" fn $name(
478 $handle: *const $handle_ty,
479 $($arg: $arg_ty,)*
480 ) -> ::std::ffi::c_int {
481 $slot.with(|slot| $crate::ffi::LastErrorSlot::clear(slot));
482
483 if $handle.is_null() {
484 $slot.with(|slot| {
485 $crate::ffi::LastErrorSlot::set_error(
486 slot,
487 &$crate::ffi::invalid_argument("document is null"),
488 )
489 });
490 return -1;
491 }
492
493 let counted: ::std::result::Result<::std::ffi::c_int, $crate::ffi::FfiError> =
494 $crate::ffi::catch(|| $body);
495
496 match counted {
497 ::std::result::Result::Ok(count) => count,
498 ::std::result::Result::Err(error) => {
499 $slot.with(|slot| $crate::ffi::LastErrorSlot::set_error(slot, &error));
500 -1
501 }
502 }
503 }
504 };
505}
506
507/// Declare an entry point that hands out an owned byte buffer selected by a C-string key.
508///
509/// The signature is fixed — `(handle, key, out_len) -> *mut u8` — because that is the
510/// shape every consumer of this ABI already exports, and only the names come from the call
511/// site. An entry point needing a different shape is written by hand.
512///
513/// `$body` evaluates to `Result<Vec<u8>, FfiError>`. The macro moves that into a
514/// `Box<[u8]>`, writes its length through `out_len`, and hands over the pointer; the
515/// matching [`export_free_bytes!`](crate::export_free_bytes) reclaims it from the same two
516/// values.
517///
518/// An empty `Vec` is a success like any other: it is handed over as a non-null pointer with
519/// `out_len` set to `0`, distinct from the null this returns on failure. A caller that
520/// treats a zero length as absence will misread an empty result as one that was never
521/// produced.
522///
523/// # Why every argument is checked before the closure
524///
525/// The failure path writes `*out_len = 0`, so *when* an argument is rejected is
526/// observable. All three null checks therefore happen before
527/// [`catch`](crate::ffi::catch) runs, and a rejected argument leaves the caller's length
528/// variable exactly as they left it — "not attempted" stays distinguishable from
529/// "attempted and produced nothing".
530///
531/// That is also why `$body` reads the key with
532/// [`ffi::c_str_utf8`](crate::ffi::c_str_utf8) rather than
533/// [`with_c_str!`](crate::with_c_str): the null check is already done, and the UTF-8
534/// conversion belongs inside the closure, where its failure does zero the length.
535///
536/// ```
537/// # use std::collections::HashMap;
538/// # use std::ffi::CString;
539/// # use unparser_shared::ffi::LastErrorSlot;
540/// # struct Archive { entries: HashMap<String, Vec<u8>> }
541/// # thread_local! { static LAST_ERROR: LastErrorSlot = const { LastErrorSlot::new() }; }
542/// # unparser_shared::export_last_error_abi!(LAST_ERROR, demo5_last_error, demo5_last_error_kind);
543/// # unparser_shared::export_handle! {
544/// # /// Handle.
545/// # handle Demo5Document { inner: Archive },
546/// # /// Free.
547/// # ///
548/// # /// # Safety
549/// # /// `doc` must come from this library.
550/// # free demo5_free_document,
551/// # }
552/// # unparser_shared::export_free_bytes!(
553/// # /// Free.
554/// # ///
555/// # /// # Safety
556/// # /// `data` and `len` must come from this library.
557/// # demo5_free_bytes
558/// # );
559/// unparser_shared::export_bytes_getter!(
560/// /// The bytes of the entry named `name`.
561/// ///
562/// /// # Safety
563/// ///
564/// /// - `doc` must be a valid handle.
565/// /// - `name` must be a valid null-terminated UTF-8 string.
566/// /// - `out_len` must point to storage for the length.
567/// /// - The returned pointer must be freed with `demo5_free_bytes`.
568/// LAST_ERROR,
569/// demo5_entry(doc: Demo5Document, name, out out_len),
570/// {
571/// let name = unsafe { unparser_shared::ffi::c_str_utf8(name) }?;
572/// match unsafe { (*doc).inner.entries.get(name) } {
573/// Some(bytes) => Ok(bytes.clone()),
574/// None => Err((unparser_shared::kind::OTHER, format!("no entry {name}"))),
575/// }
576/// }
577/// );
578///
579/// let mut entries = HashMap::new();
580/// entries.insert("a.bin".to_string(), vec![1u8, 2, 3]);
581/// let doc = Box::into_raw(Box::new(Demo5Document { inner: Archive { entries } }));
582/// let name = CString::new("a.bin").unwrap();
583///
584/// let mut out_len = 0usize;
585/// let bytes = unsafe { demo5_entry(doc, name.as_ptr(), &mut out_len) };
586/// assert_eq!(out_len, 3);
587/// assert_eq!(unsafe { std::slice::from_raw_parts(bytes, out_len) }, [1, 2, 3]);
588///
589/// unsafe {
590/// demo5_free_bytes(bytes, out_len);
591/// demo5_free_document(doc);
592/// }
593/// ```
594#[macro_export]
595macro_rules! export_bytes_getter {
596 (
597 $(#[$meta:meta])*
598 $slot:path,
599 $name:ident($handle:ident: $handle_ty:ty, $key:ident, out $out_len:ident $(,)?),
600 $body:block
601 ) => {
602 $(#[$meta])*
603 #[no_mangle]
604 pub unsafe extern "C" fn $name(
605 $handle: *const $handle_ty,
606 $key: *const ::std::ffi::c_char,
607 $out_len: *mut usize,
608 ) -> *mut u8 {
609 $slot.with(|slot| $crate::ffi::LastErrorSlot::clear(slot));
610
611 // Checked before the closure: the failure path below writes through
612 // `$out_len`, so a rejected argument must leave it as the caller left it.
613 if $handle.is_null() {
614 $slot.with(|slot| {
615 $crate::ffi::LastErrorSlot::set_error(
616 slot,
617 &$crate::ffi::invalid_argument("document is null"),
618 )
619 });
620 return ::std::ptr::null_mut();
621 }
622
623 if $key.is_null() {
624 $slot.with(|slot| {
625 $crate::ffi::LastErrorSlot::set_error(
626 slot,
627 &$crate::ffi::invalid_argument(concat!(
628 stringify!($key),
629 " is null"
630 )),
631 )
632 });
633 return ::std::ptr::null_mut();
634 }
635
636 if $out_len.is_null() {
637 $slot.with(|slot| {
638 $crate::ffi::LastErrorSlot::set_error(
639 slot,
640 &$crate::ffi::invalid_argument(concat!(
641 stringify!($out_len),
642 " is null"
643 )),
644 )
645 });
646 return ::std::ptr::null_mut();
647 }
648
649 let produced: ::std::result::Result<
650 ::std::vec::Vec<u8>,
651 $crate::ffi::FfiError,
652 > = $crate::ffi::catch(|| $body);
653
654 match produced {
655 ::std::result::Result::Ok(data) => {
656 let length = data.len();
657 let raw =
658 ::std::boxed::Box::into_raw(data.into_boxed_slice()) as *mut u8;
659 // The null check above is what makes this write sound; wrapped
660 // explicitly so the expansion compiles under
661 // `unsafe_op_in_unsafe_fn` too.
662 unsafe { *$out_len = length };
663 raw
664 }
665 ::std::result::Result::Err(error) => {
666 $slot.with(|slot| $crate::ffi::LastErrorSlot::set_error(slot, &error));
667 unsafe { *$out_len = 0 };
668 ::std::ptr::null_mut()
669 }
670 }
671 }
672 };
673}
674
675#[cfg(test)]
676mod tests {
677 use std::ffi::{c_char, CString};
678
679 use crate::ffi::FfiError;
680
681 unsafe fn read(ptr: *const c_char) -> Result<&'static str, FfiError> {
682 unsafe { crate::with_c_str!(ptr) }
683 }
684
685 #[test]
686 fn a_null_c_string_argument_is_named_in_its_own_message() {
687 let failure = unsafe { read(std::ptr::null()) }.unwrap_err();
688 assert_eq!(failure.0, crate::kind::INVALID_ARGUMENT);
689 assert_eq!(
690 failure.1, "ptr is null",
691 "the message is built from the argument's own name"
692 );
693 }
694
695 #[test]
696 fn a_valid_c_string_argument_reads_through() {
697 let text = CString::new("document.hwp").unwrap();
698 assert_eq!(unsafe { read(text.as_ptr()) }.unwrap(), "document.hwp");
699 }
700
701 #[test]
702 fn a_non_utf8_c_string_argument_is_an_invalid_argument() {
703 // 0xFF is not a valid UTF-8 lead byte.
704 let raw = [0xFFu8, 0x00];
705 let failure = unsafe { read(raw.as_ptr() as *const c_char) }.unwrap_err();
706 assert_eq!(failure.0, crate::kind::INVALID_ARGUMENT);
707 assert!(
708 !failure.1.is_empty(),
709 "the conversion error must survive as a message"
710 );
711 }
712
713 use std::collections::HashMap;
714
715 use crate::ffi::LastErrorSlot;
716
717 /// Stands in for a library's document. Deliberately not a document: the fields are
718 /// only the *shapes* an entry point returns — a string, an absent string, a count,
719 /// a byte buffer keyed by name.
720 #[derive(Default)]
721 struct Demo {
722 body: String,
723 title: Option<String>,
724 blobs: HashMap<String, Vec<u8>>,
725 }
726
727 thread_local! {
728 static DEMO_ERROR: LastErrorSlot = const { LastErrorSlot::new() };
729 }
730
731 crate::export_last_error_abi!(DEMO_ERROR, demo_last_error, demo_last_error_kind);
732
733 crate::export_handle! {
734 /// Opaque handle to a demo document.
735 handle DemoDocument { inner: Demo },
736
737 /// Free a demo handle.
738 ///
739 /// # Safety
740 ///
741 /// - `doc` must be a pointer this test module produced, or null.
742 /// - After this call the handle is invalid and must not be used.
743 free demo_free_document,
744 }
745
746 crate::export_free_string!(
747 /// Free a string produced by a demo entry point.
748 ///
749 /// # Safety
750 ///
751 /// - `s` must be a pointer a demo entry point returned, or null.
752 demo_free_string
753 );
754
755 crate::export_free_bytes!(
756 /// Free a byte buffer produced by a demo entry point.
757 ///
758 /// # Safety
759 ///
760 /// - `data` must be a pointer a demo entry point returned, or null.
761 /// - `len` must be the length that call reported.
762 demo_free_bytes
763 );
764
765 fn demo(inner: Demo) -> *mut DemoDocument {
766 Box::into_raw(Box::new(DemoDocument { inner }))
767 }
768
769 #[test]
770 fn a_handle_round_trips_through_its_free_function() {
771 let doc = demo(Demo::default());
772 assert!(!doc.is_null());
773 // A smoke test, deliberately: a stock `cargo test` run has no leak detector, so
774 // this cannot show the allocation was returned. What it does show is that the
775 // pair agrees on the pointer — a mismatched type or an extra indirection would
776 // abort here.
777 unsafe { demo_free_document(doc) };
778 }
779
780 #[test]
781 fn every_free_function_accepts_null() {
782 unsafe {
783 demo_free_document(std::ptr::null_mut());
784 demo_free_string(std::ptr::null_mut());
785 demo_free_bytes(std::ptr::null_mut(), 0);
786 }
787 }
788
789 /// A zero length must leave the caller's buffer alone.
790 ///
791 /// This pins the observable contract, not the guard: dropping a zero-length
792 /// `Box<[u8]>` would not reach the allocator either, since `Global::deallocate`
793 /// skips a zero-size layout. The `len > 0` guard earns its keep elsewhere — it means
794 /// no `Box` is ever reconstructed from a pointer at all, which is what a
795 /// provenance-checking run would object to.
796 #[test]
797 fn free_bytes_ignores_a_zero_length_buffer() {
798 let mut byte = 7u8;
799 unsafe { demo_free_bytes(&mut byte, 0) };
800 assert_eq!(byte, 7);
801 }
802
803 crate::export_string_getter!(
804 /// The document body.
805 ///
806 /// # Safety
807 ///
808 /// - `doc` must be a valid handle.
809 /// - The returned string must be freed with `demo_free_string`.
810 DEMO_ERROR,
811 demo_body(doc: DemoDocument),
812 { Ok(unsafe { (*doc).inner.body.clone() }) }
813 );
814
815 crate::export_string_getter!(
816 /// The document body, repeated `times` times.
817 ///
818 /// # Safety
819 ///
820 /// - `doc` must be a valid handle.
821 /// - The returned string must be freed with `demo_free_string`.
822 DEMO_ERROR,
823 demo_body_repeated(doc: DemoDocument, times: ::std::ffi::c_int),
824 { Ok(unsafe { (*doc).inner.body.repeat(times.max(0) as usize) }) }
825 );
826
827 crate::export_string_getter!(
828 /// The document body, or a classified failure when it is empty.
829 ///
830 /// # Safety
831 ///
832 /// - `doc` must be a valid handle.
833 DEMO_ERROR,
834 demo_body_or_fail(doc: DemoDocument),
835 {
836 let body = unsafe { &(*doc).inner.body };
837 if body.is_empty() {
838 return Err((crate::kind::IO, "no body".to_string()));
839 }
840 Ok(body.clone())
841 }
842 );
843
844 crate::export_string_getter!(
845 /// Always panics, to prove the guard is in place.
846 ///
847 /// # Safety
848 ///
849 /// - `doc` must be a valid handle.
850 // Passed through to the generated item, which also proves the macro forwards
851 // arbitrary attributes and not only doc comments.
852 #[allow(unreachable_code)]
853 DEMO_ERROR,
854 demo_body_panics(doc: DemoDocument),
855 { panic!("deliberate") }
856 );
857
858 fn owned(text: *mut c_char) -> String {
859 assert!(!text.is_null());
860 let owned = unsafe { std::ffi::CStr::from_ptr(text) }
861 .to_string_lossy()
862 .into_owned();
863 unsafe { demo_free_string(text) };
864 owned
865 }
866
867 fn demo_with_body(body: &str) -> *mut DemoDocument {
868 demo(Demo {
869 body: body.to_string(),
870 ..Demo::default()
871 })
872 }
873
874 #[test]
875 fn a_string_getter_returns_its_value_and_reports_success() {
876 let doc = demo_with_body("hello");
877 assert_eq!(owned(unsafe { demo_body(doc) }), "hello");
878 assert_eq!(demo_last_error_kind(), crate::kind::NONE);
879 unsafe { demo_free_document(doc) };
880 }
881
882 #[test]
883 fn a_string_getter_passes_its_extra_arguments_to_the_body() {
884 let doc = demo_with_body("ab");
885 assert_eq!(owned(unsafe { demo_body_repeated(doc, 3) }), "ababab");
886 unsafe { demo_free_document(doc) };
887 }
888
889 #[test]
890 fn a_null_handle_is_an_invalid_argument_named_document() {
891 assert!(unsafe { demo_body(std::ptr::null()) }.is_null());
892 assert_eq!(demo_last_error_kind(), crate::kind::INVALID_ARGUMENT);
893 let message = unsafe { std::ffi::CStr::from_ptr(demo_last_error()) }
894 .to_string_lossy()
895 .into_owned();
896 assert_eq!(
897 message, "document is null",
898 "the family reports this identically, so the wording is fixed here"
899 );
900 }
901
902 /// The reason a caller can poll the slot after a *successful* call: it was cleared on
903 /// entry, so a leftover failure cannot be attributed to this call.
904 #[test]
905 fn a_successful_call_clears_a_previous_failure() {
906 assert!(unsafe { demo_body(std::ptr::null()) }.is_null());
907 assert_ne!(demo_last_error_kind(), crate::kind::NONE);
908
909 let doc = demo_with_body("fresh");
910 assert_eq!(owned(unsafe { demo_body(doc) }), "fresh");
911 assert_eq!(demo_last_error_kind(), crate::kind::NONE);
912 unsafe { demo_free_document(doc) };
913 }
914
915 #[test]
916 fn a_classified_body_failure_reaches_the_slot() {
917 let doc = demo_with_body("");
918 assert!(unsafe { demo_body_or_fail(doc) }.is_null());
919 assert_eq!(demo_last_error_kind(), crate::kind::IO);
920 unsafe { demo_free_document(doc) };
921 }
922
923 /// An interior NUL is the one failure the macro raises by itself: the value exists and
924 /// is correct, it just cannot be a C string.
925 #[test]
926 fn a_value_holding_an_interior_nul_is_reported_not_truncated() {
927 let doc = demo_with_body("before\0after");
928 assert!(unsafe { demo_body(doc) }.is_null());
929 assert_eq!(demo_last_error_kind(), crate::kind::INVALID_OUTPUT);
930 unsafe { demo_free_document(doc) };
931 }
932
933 #[test]
934 fn a_panic_in_the_body_does_not_cross_the_boundary() {
935 let previous = std::panic::take_hook();
936 std::panic::set_hook(Box::new(|_| {}));
937 let doc = demo_with_body("x");
938 let returned = unsafe { demo_body_panics(doc) };
939 std::panic::set_hook(previous);
940
941 assert!(returned.is_null());
942 assert_eq!(demo_last_error_kind(), crate::kind::PANIC);
943 unsafe { demo_free_document(doc) };
944 }
945
946 crate::export_optional_string_getter!(
947 /// The document title.
948 ///
949 /// # Safety
950 ///
951 /// - `doc` must be a valid handle.
952 /// - Returns null when no title is set, leaving the kind at success.
953 /// - The returned string must be freed with `demo_free_string`.
954 DEMO_ERROR,
955 demo_title(doc: DemoDocument),
956 { Ok(unsafe { (*doc).inner.title.clone() }) }
957 );
958
959 fn demo_with_title(title: Option<&str>) -> *mut DemoDocument {
960 demo(Demo {
961 title: title.map(str::to_string),
962 ..Demo::default()
963 })
964 }
965
966 #[test]
967 fn a_present_optional_value_is_handed_out() {
968 let doc = demo_with_title(Some("Quarterly Report"));
969 assert_eq!(owned(unsafe { demo_title(doc) }), "Quarterly Report");
970 assert_eq!(demo_last_error_kind(), crate::kind::NONE);
971 unsafe { demo_free_document(doc) };
972 }
973
974 /// An absent value is not a failure. This is the whole reason this macro is separate
975 /// from `export_string_getter!`: collapsing absent to an empty string would tell a
976 /// caller there is a title and it is blank.
977 #[test]
978 fn an_absent_optional_value_is_null_and_still_success() {
979 let doc = demo_with_title(None);
980 assert!(unsafe { demo_title(doc) }.is_null());
981 assert_eq!(demo_last_error_kind(), crate::kind::NONE);
982 unsafe { demo_free_document(doc) };
983 }
984
985 /// The counterpart: a value that exists but cannot cross must not read as absent.
986 /// Both return null, so the kind is the only thing that separates "there is nothing"
987 /// from "we could not give it to you".
988 #[test]
989 fn an_unrepresentable_optional_value_is_not_reported_as_absent() {
990 let doc = demo_with_title(Some("has\0interior nul"));
991 assert!(unsafe { demo_title(doc) }.is_null());
992 assert_eq!(demo_last_error_kind(), crate::kind::INVALID_OUTPUT);
993 unsafe { demo_free_document(doc) };
994 }
995
996 #[test]
997 fn an_optional_getter_rejects_a_null_handle() {
998 assert!(unsafe { demo_title(std::ptr::null()) }.is_null());
999 assert_eq!(demo_last_error_kind(), crate::kind::INVALID_ARGUMENT);
1000 }
1001
1002 crate::export_count_getter!(
1003 /// The number of blobs in the document.
1004 ///
1005 /// # Safety
1006 ///
1007 /// - `doc` must be a valid handle.
1008 /// - Returns -1 on failure.
1009 DEMO_ERROR,
1010 demo_blob_count(doc: DemoDocument),
1011 { Ok(unsafe { (*doc).inner.blobs.len() } as ::std::ffi::c_int) }
1012 );
1013
1014 fn demo_with_blob(id: &str, data: &[u8]) -> *mut DemoDocument {
1015 let mut blobs = HashMap::new();
1016 blobs.insert(id.to_string(), data.to_vec());
1017 demo(Demo {
1018 blobs,
1019 ..Demo::default()
1020 })
1021 }
1022
1023 #[test]
1024 fn a_count_getter_returns_its_count() {
1025 let doc = demo_with_blob("logo.png", b"\x89PNG");
1026 assert_eq!(unsafe { demo_blob_count(doc) }, 1);
1027 assert_eq!(demo_last_error_kind(), crate::kind::NONE);
1028 unsafe { demo_free_document(doc) };
1029 }
1030
1031 #[test]
1032 fn a_count_getter_returns_minus_one_for_a_null_handle() {
1033 assert_eq!(unsafe { demo_blob_count(std::ptr::null()) }, -1);
1034 assert_eq!(demo_last_error_kind(), crate::kind::INVALID_ARGUMENT);
1035 }
1036
1037 /// A count getter returns a value, not a pointer, so a caller cannot tell from the
1038 /// return alone whether a recorded failure is theirs. Clearing on entry is what makes
1039 /// polling the slot after a count meaningful — the shipped ABI does this and it is
1040 /// easy to drop when the body is one line.
1041 #[test]
1042 fn a_count_getter_clears_a_previous_failure() {
1043 assert_eq!(unsafe { demo_blob_count(std::ptr::null()) }, -1);
1044 assert_ne!(demo_last_error_kind(), crate::kind::NONE);
1045
1046 let doc = demo(Demo::default());
1047 assert_eq!(unsafe { demo_blob_count(doc) }, 0);
1048 assert_eq!(
1049 demo_last_error_kind(),
1050 crate::kind::NONE,
1051 "a count of zero is a success, not a leftover failure"
1052 );
1053 unsafe { demo_free_document(doc) };
1054 }
1055
1056 crate::export_bytes_getter!(
1057 /// The bytes of the blob named `id`.
1058 ///
1059 /// # Safety
1060 ///
1061 /// - `doc` must be a valid handle.
1062 /// - `id` must be a valid null-terminated UTF-8 string.
1063 /// - `out_len` must point to storage for the length.
1064 /// - The returned pointer must be freed with `demo_free_bytes`.
1065 DEMO_ERROR,
1066 demo_blob(doc: DemoDocument, id, out out_len),
1067 {
1068 let id = unsafe { crate::ffi::c_str_utf8(id) }?;
1069 match unsafe { (*doc).inner.blobs.get(id) } {
1070 Some(data) => Ok(data.clone()),
1071 None => Err((crate::kind::OTHER, format!("no blob {id}"))),
1072 }
1073 }
1074 );
1075
1076 #[test]
1077 fn a_bytes_getter_hands_over_the_buffer_and_its_length() {
1078 let doc = demo_with_blob("logo.png", b"\x89PNG\r\n");
1079 let id = CString::new("logo.png").unwrap();
1080
1081 let mut out_len: usize = 0;
1082 let data = unsafe { demo_blob(doc, id.as_ptr(), &mut out_len) };
1083 assert!(!data.is_null());
1084 assert_eq!(out_len, 6);
1085 assert_eq!(
1086 unsafe { std::slice::from_raw_parts(data, out_len) },
1087 b"\x89PNG\r\n"
1088 );
1089
1090 unsafe {
1091 demo_free_bytes(data, out_len);
1092 demo_free_document(doc);
1093 }
1094 }
1095
1096 /// The behaviour this macro checks all three arguments before the closure for: a
1097 /// rejected argument leaves the caller's length variable exactly as they left it, so
1098 /// "not attempted" stays distinguishable from "attempted and produced nothing".
1099 #[test]
1100 fn a_rejected_argument_leaves_out_len_untouched() {
1101 let doc = demo_with_blob("logo.png", b"\x89PNG");
1102 let id = CString::new("logo.png").unwrap();
1103 const SEEDED: usize = 0xDEAD;
1104
1105 let mut out_len: usize = SEEDED;
1106 assert!(unsafe { demo_blob(std::ptr::null(), id.as_ptr(), &mut out_len) }.is_null());
1107 assert_eq!(out_len, SEEDED, "a null handle must not write out_len");
1108 assert_eq!(demo_last_error_kind(), crate::kind::INVALID_ARGUMENT);
1109
1110 assert!(unsafe { demo_blob(doc, std::ptr::null(), &mut out_len) }.is_null());
1111 assert_eq!(out_len, SEEDED, "a null key must not write out_len");
1112 assert_eq!(demo_last_error_kind(), crate::kind::INVALID_ARGUMENT);
1113
1114 // A key that is merely absent *is* attempted, so the length is zeroed.
1115 let missing = CString::new("absent.png").unwrap();
1116 assert!(unsafe { demo_blob(doc, missing.as_ptr(), &mut out_len) }.is_null());
1117 assert_eq!(out_len, 0, "a lookup that failed reports zero length");
1118 assert_eq!(demo_last_error_kind(), crate::kind::OTHER);
1119
1120 unsafe { demo_free_document(doc) };
1121 }
1122
1123 #[test]
1124 fn a_null_out_len_is_rejected_and_named() {
1125 let doc = demo_with_blob("logo.png", b"\x89PNG");
1126 let id = CString::new("logo.png").unwrap();
1127
1128 assert!(unsafe { demo_blob(doc, id.as_ptr(), std::ptr::null_mut()) }.is_null());
1129 let message = unsafe { std::ffi::CStr::from_ptr(demo_last_error()) }
1130 .to_string_lossy()
1131 .into_owned();
1132 assert_eq!(message, "out_len is null");
1133
1134 unsafe { demo_free_document(doc) };
1135 }
1136
1137 #[test]
1138 fn a_non_utf8_key_is_classified_and_zeroes_the_length() {
1139 let doc = demo_with_blob("logo.png", b"\x89PNG");
1140 let raw = [0xFFu8, 0x00];
1141
1142 let mut out_len: usize = 0xBEEF;
1143 let data = unsafe { demo_blob(doc, raw.as_ptr() as *const c_char, &mut out_len) };
1144 assert!(data.is_null());
1145 assert_eq!(demo_last_error_kind(), crate::kind::INVALID_ARGUMENT);
1146 assert_eq!(
1147 out_len, 0,
1148 "the conversion happens inside the closure, so the failure path runs"
1149 );
1150
1151 unsafe { demo_free_document(doc) };
1152 }
1153
1154 #[test]
1155 fn an_empty_buffer_is_handed_over_as_a_non_null_pointer_with_zero_length() {
1156 let doc = demo_with_blob("empty.bin", b"");
1157 let id = CString::new("empty.bin").unwrap();
1158
1159 let mut out_len: usize = 9;
1160 let data = unsafe { demo_blob(doc, id.as_ptr(), &mut out_len) };
1161 assert!(
1162 !data.is_null(),
1163 "an empty blob is a success, not an absent one"
1164 );
1165 assert_eq!(out_len, 0);
1166
1167 unsafe {
1168 demo_free_bytes(data, out_len);
1169 demo_free_document(doc);
1170 }
1171 }
1172}