#[macro_export]
macro_rules! with_c_str {
($ptr:ident) => {
if $ptr.is_null() {
::std::result::Result::Err($crate::ffi::invalid_argument(concat!(
stringify!($ptr),
" is null"
)))
} else {
$crate::ffi::c_str_utf8($ptr)
}
};
}
#[macro_export]
macro_rules! export_handle {
(
$(#[$handle_meta:meta])*
handle $handle:ident { $field:ident: $inner:ty },
$(#[$free_meta:meta])*
free $free_fn:ident $(,)?
) => {
$(#[$handle_meta])*
#[repr(C)]
pub struct $handle {
$field: $inner,
}
$(#[$free_meta])*
#[no_mangle]
pub unsafe extern "C" fn $free_fn(doc: *mut $handle) {
if !doc.is_null() {
let _ = unsafe { ::std::boxed::Box::from_raw(doc) };
}
}
};
}
#[macro_export]
macro_rules! export_free_string {
($(#[$meta:meta])* $free_fn:ident) => {
$(#[$meta])*
#[no_mangle]
pub unsafe extern "C" fn $free_fn(s: *mut ::std::ffi::c_char) {
if !s.is_null() {
let _ = unsafe { ::std::ffi::CString::from_raw(s) };
}
}
};
}
#[macro_export]
macro_rules! export_free_bytes {
($(#[$meta:meta])* $free_fn:ident) => {
$(#[$meta])*
#[no_mangle]
pub unsafe extern "C" fn $free_fn(data: *mut u8, len: usize) {
if !data.is_null() && len > 0 {
let _ = unsafe {
::std::boxed::Box::from_raw(::std::ptr::slice_from_raw_parts_mut(
data, len,
))
};
}
}
};
}
#[macro_export]
macro_rules! export_string_getter {
(
$(#[$meta:meta])*
$slot:path,
$name:ident($handle:ident: $handle_ty:ty $(, $arg:ident: $arg_ty:ty)* $(,)?),
$body:block
) => {
$(#[$meta])*
#[no_mangle]
pub unsafe extern "C" fn $name(
$handle: *const $handle_ty,
$($arg: $arg_ty,)*
) -> *mut ::std::ffi::c_char {
$slot.with(|slot| $crate::ffi::LastErrorSlot::clear(slot));
if $handle.is_null() {
$slot.with(|slot| {
$crate::ffi::LastErrorSlot::set_error(
slot,
&$crate::ffi::invalid_argument("document is null"),
)
});
return ::std::ptr::null_mut();
}
let produced: ::std::result::Result<::std::string::String, $crate::ffi::FfiError> =
$crate::ffi::catch(|| $body);
match produced {
::std::result::Result::Ok(text) => match ::std::ffi::CString::new(text) {
::std::result::Result::Ok(owned) => owned.into_raw(),
::std::result::Result::Err(_) => {
$slot.with(|slot| {
$crate::ffi::LastErrorSlot::set_error(
slot,
&$crate::ffi::invalid_output(),
)
});
::std::ptr::null_mut()
}
},
::std::result::Result::Err(error) => {
$slot.with(|slot| $crate::ffi::LastErrorSlot::set_error(slot, &error));
::std::ptr::null_mut()
}
}
}
};
}
#[macro_export]
macro_rules! export_optional_string_getter {
(
$(#[$meta:meta])*
$slot:path,
$name:ident($handle:ident: $handle_ty:ty $(, $arg:ident: $arg_ty:ty)* $(,)?),
$body:block
) => {
$(#[$meta])*
#[no_mangle]
pub unsafe extern "C" fn $name(
$handle: *const $handle_ty,
$($arg: $arg_ty,)*
) -> *mut ::std::ffi::c_char {
$slot.with(|slot| $crate::ffi::LastErrorSlot::clear(slot));
if $handle.is_null() {
$slot.with(|slot| {
$crate::ffi::LastErrorSlot::set_error(
slot,
&$crate::ffi::invalid_argument("document is null"),
)
});
return ::std::ptr::null_mut();
}
let produced: ::std::result::Result<
::std::option::Option<::std::string::String>,
$crate::ffi::FfiError,
> = $crate::ffi::catch(|| $body);
match produced {
::std::result::Result::Ok(::std::option::Option::Some(text)) => {
match ::std::ffi::CString::new(text) {
::std::result::Result::Ok(owned) => owned.into_raw(),
::std::result::Result::Err(_) => {
$slot.with(|slot| {
$crate::ffi::LastErrorSlot::set_error(
slot,
&$crate::ffi::invalid_output(),
)
});
::std::ptr::null_mut()
}
}
}
::std::result::Result::Ok(::std::option::Option::None) => {
::std::ptr::null_mut()
}
::std::result::Result::Err(error) => {
$slot.with(|slot| $crate::ffi::LastErrorSlot::set_error(slot, &error));
::std::ptr::null_mut()
}
}
}
};
}
#[macro_export]
macro_rules! export_count_getter {
(
$(#[$meta:meta])*
$slot:path,
$name:ident($handle:ident: $handle_ty:ty $(, $arg:ident: $arg_ty:ty)* $(,)?),
$body:block
) => {
$(#[$meta])*
#[no_mangle]
pub unsafe extern "C" fn $name(
$handle: *const $handle_ty,
$($arg: $arg_ty,)*
) -> ::std::ffi::c_int {
$slot.with(|slot| $crate::ffi::LastErrorSlot::clear(slot));
if $handle.is_null() {
$slot.with(|slot| {
$crate::ffi::LastErrorSlot::set_error(
slot,
&$crate::ffi::invalid_argument("document is null"),
)
});
return -1;
}
let counted: ::std::result::Result<::std::ffi::c_int, $crate::ffi::FfiError> =
$crate::ffi::catch(|| $body);
match counted {
::std::result::Result::Ok(count) => count,
::std::result::Result::Err(error) => {
$slot.with(|slot| $crate::ffi::LastErrorSlot::set_error(slot, &error));
-1
}
}
}
};
}
#[macro_export]
macro_rules! export_bytes_getter {
(
$(#[$meta:meta])*
$slot:path,
$name:ident($handle:ident: $handle_ty:ty, $key:ident, out $out_len:ident $(,)?),
$body:block
) => {
$(#[$meta])*
#[no_mangle]
pub unsafe extern "C" fn $name(
$handle: *const $handle_ty,
$key: *const ::std::ffi::c_char,
$out_len: *mut usize,
) -> *mut u8 {
$slot.with(|slot| $crate::ffi::LastErrorSlot::clear(slot));
if $handle.is_null() {
$slot.with(|slot| {
$crate::ffi::LastErrorSlot::set_error(
slot,
&$crate::ffi::invalid_argument("document is null"),
)
});
return ::std::ptr::null_mut();
}
if $key.is_null() {
$slot.with(|slot| {
$crate::ffi::LastErrorSlot::set_error(
slot,
&$crate::ffi::invalid_argument(concat!(
stringify!($key),
" is null"
)),
)
});
return ::std::ptr::null_mut();
}
if $out_len.is_null() {
$slot.with(|slot| {
$crate::ffi::LastErrorSlot::set_error(
slot,
&$crate::ffi::invalid_argument(concat!(
stringify!($out_len),
" is null"
)),
)
});
return ::std::ptr::null_mut();
}
let produced: ::std::result::Result<
::std::vec::Vec<u8>,
$crate::ffi::FfiError,
> = $crate::ffi::catch(|| $body);
match produced {
::std::result::Result::Ok(data) => {
let length = data.len();
let raw =
::std::boxed::Box::into_raw(data.into_boxed_slice()) as *mut u8;
unsafe { *$out_len = length };
raw
}
::std::result::Result::Err(error) => {
$slot.with(|slot| $crate::ffi::LastErrorSlot::set_error(slot, &error));
unsafe { *$out_len = 0 };
::std::ptr::null_mut()
}
}
}
};
}
#[cfg(test)]
mod tests {
use std::ffi::{c_char, CString};
use crate::ffi::FfiError;
unsafe fn read(ptr: *const c_char) -> Result<&'static str, FfiError> {
unsafe { crate::with_c_str!(ptr) }
}
#[test]
fn a_null_c_string_argument_is_named_in_its_own_message() {
let failure = unsafe { read(std::ptr::null()) }.unwrap_err();
assert_eq!(failure.0, crate::kind::INVALID_ARGUMENT);
assert_eq!(
failure.1, "ptr is null",
"the message is built from the argument's own name"
);
}
#[test]
fn a_valid_c_string_argument_reads_through() {
let text = CString::new("document.hwp").unwrap();
assert_eq!(unsafe { read(text.as_ptr()) }.unwrap(), "document.hwp");
}
#[test]
fn a_non_utf8_c_string_argument_is_an_invalid_argument() {
let raw = [0xFFu8, 0x00];
let failure = unsafe { read(raw.as_ptr() as *const c_char) }.unwrap_err();
assert_eq!(failure.0, crate::kind::INVALID_ARGUMENT);
assert!(
!failure.1.is_empty(),
"the conversion error must survive as a message"
);
}
use std::collections::HashMap;
use crate::ffi::LastErrorSlot;
#[derive(Default)]
struct Demo {
body: String,
title: Option<String>,
blobs: HashMap<String, Vec<u8>>,
}
thread_local! {
static DEMO_ERROR: LastErrorSlot = const { LastErrorSlot::new() };
}
crate::export_last_error_abi!(DEMO_ERROR, demo_last_error, demo_last_error_kind);
crate::export_handle! {
handle DemoDocument { inner: Demo },
free demo_free_document,
}
crate::export_free_string!(
demo_free_string
);
crate::export_free_bytes!(
demo_free_bytes
);
fn demo(inner: Demo) -> *mut DemoDocument {
Box::into_raw(Box::new(DemoDocument { inner }))
}
#[test]
fn a_handle_round_trips_through_its_free_function() {
let doc = demo(Demo::default());
assert!(!doc.is_null());
unsafe { demo_free_document(doc) };
}
#[test]
fn every_free_function_accepts_null() {
unsafe {
demo_free_document(std::ptr::null_mut());
demo_free_string(std::ptr::null_mut());
demo_free_bytes(std::ptr::null_mut(), 0);
}
}
#[test]
fn free_bytes_ignores_a_zero_length_buffer() {
let mut byte = 7u8;
unsafe { demo_free_bytes(&mut byte, 0) };
assert_eq!(byte, 7);
}
crate::export_string_getter!(
DEMO_ERROR,
demo_body(doc: DemoDocument),
{ Ok(unsafe { (*doc).inner.body.clone() }) }
);
crate::export_string_getter!(
DEMO_ERROR,
demo_body_repeated(doc: DemoDocument, times: ::std::ffi::c_int),
{ Ok(unsafe { (*doc).inner.body.repeat(times.max(0) as usize) }) }
);
crate::export_string_getter!(
DEMO_ERROR,
demo_body_or_fail(doc: DemoDocument),
{
let body = unsafe { &(*doc).inner.body };
if body.is_empty() {
return Err((crate::kind::IO, "no body".to_string()));
}
Ok(body.clone())
}
);
crate::export_string_getter!(
#[allow(unreachable_code)]
DEMO_ERROR,
demo_body_panics(doc: DemoDocument),
{ panic!("deliberate") }
);
fn owned(text: *mut c_char) -> String {
assert!(!text.is_null());
let owned = unsafe { std::ffi::CStr::from_ptr(text) }
.to_string_lossy()
.into_owned();
unsafe { demo_free_string(text) };
owned
}
fn demo_with_body(body: &str) -> *mut DemoDocument {
demo(Demo {
body: body.to_string(),
..Demo::default()
})
}
#[test]
fn a_string_getter_returns_its_value_and_reports_success() {
let doc = demo_with_body("hello");
assert_eq!(owned(unsafe { demo_body(doc) }), "hello");
assert_eq!(demo_last_error_kind(), crate::kind::NONE);
unsafe { demo_free_document(doc) };
}
#[test]
fn a_string_getter_passes_its_extra_arguments_to_the_body() {
let doc = demo_with_body("ab");
assert_eq!(owned(unsafe { demo_body_repeated(doc, 3) }), "ababab");
unsafe { demo_free_document(doc) };
}
#[test]
fn a_null_handle_is_an_invalid_argument_named_document() {
assert!(unsafe { demo_body(std::ptr::null()) }.is_null());
assert_eq!(demo_last_error_kind(), crate::kind::INVALID_ARGUMENT);
let message = unsafe { std::ffi::CStr::from_ptr(demo_last_error()) }
.to_string_lossy()
.into_owned();
assert_eq!(
message, "document is null",
"the family reports this identically, so the wording is fixed here"
);
}
#[test]
fn a_successful_call_clears_a_previous_failure() {
assert!(unsafe { demo_body(std::ptr::null()) }.is_null());
assert_ne!(demo_last_error_kind(), crate::kind::NONE);
let doc = demo_with_body("fresh");
assert_eq!(owned(unsafe { demo_body(doc) }), "fresh");
assert_eq!(demo_last_error_kind(), crate::kind::NONE);
unsafe { demo_free_document(doc) };
}
#[test]
fn a_classified_body_failure_reaches_the_slot() {
let doc = demo_with_body("");
assert!(unsafe { demo_body_or_fail(doc) }.is_null());
assert_eq!(demo_last_error_kind(), crate::kind::IO);
unsafe { demo_free_document(doc) };
}
#[test]
fn a_value_holding_an_interior_nul_is_reported_not_truncated() {
let doc = demo_with_body("before\0after");
assert!(unsafe { demo_body(doc) }.is_null());
assert_eq!(demo_last_error_kind(), crate::kind::INVALID_OUTPUT);
unsafe { demo_free_document(doc) };
}
#[test]
fn a_panic_in_the_body_does_not_cross_the_boundary() {
let previous = std::panic::take_hook();
std::panic::set_hook(Box::new(|_| {}));
let doc = demo_with_body("x");
let returned = unsafe { demo_body_panics(doc) };
std::panic::set_hook(previous);
assert!(returned.is_null());
assert_eq!(demo_last_error_kind(), crate::kind::PANIC);
unsafe { demo_free_document(doc) };
}
crate::export_optional_string_getter!(
DEMO_ERROR,
demo_title(doc: DemoDocument),
{ Ok(unsafe { (*doc).inner.title.clone() }) }
);
fn demo_with_title(title: Option<&str>) -> *mut DemoDocument {
demo(Demo {
title: title.map(str::to_string),
..Demo::default()
})
}
#[test]
fn a_present_optional_value_is_handed_out() {
let doc = demo_with_title(Some("Quarterly Report"));
assert_eq!(owned(unsafe { demo_title(doc) }), "Quarterly Report");
assert_eq!(demo_last_error_kind(), crate::kind::NONE);
unsafe { demo_free_document(doc) };
}
#[test]
fn an_absent_optional_value_is_null_and_still_success() {
let doc = demo_with_title(None);
assert!(unsafe { demo_title(doc) }.is_null());
assert_eq!(demo_last_error_kind(), crate::kind::NONE);
unsafe { demo_free_document(doc) };
}
#[test]
fn an_unrepresentable_optional_value_is_not_reported_as_absent() {
let doc = demo_with_title(Some("has\0interior nul"));
assert!(unsafe { demo_title(doc) }.is_null());
assert_eq!(demo_last_error_kind(), crate::kind::INVALID_OUTPUT);
unsafe { demo_free_document(doc) };
}
#[test]
fn an_optional_getter_rejects_a_null_handle() {
assert!(unsafe { demo_title(std::ptr::null()) }.is_null());
assert_eq!(demo_last_error_kind(), crate::kind::INVALID_ARGUMENT);
}
crate::export_count_getter!(
DEMO_ERROR,
demo_blob_count(doc: DemoDocument),
{ Ok(unsafe { (*doc).inner.blobs.len() } as ::std::ffi::c_int) }
);
fn demo_with_blob(id: &str, data: &[u8]) -> *mut DemoDocument {
let mut blobs = HashMap::new();
blobs.insert(id.to_string(), data.to_vec());
demo(Demo {
blobs,
..Demo::default()
})
}
#[test]
fn a_count_getter_returns_its_count() {
let doc = demo_with_blob("logo.png", b"\x89PNG");
assert_eq!(unsafe { demo_blob_count(doc) }, 1);
assert_eq!(demo_last_error_kind(), crate::kind::NONE);
unsafe { demo_free_document(doc) };
}
#[test]
fn a_count_getter_returns_minus_one_for_a_null_handle() {
assert_eq!(unsafe { demo_blob_count(std::ptr::null()) }, -1);
assert_eq!(demo_last_error_kind(), crate::kind::INVALID_ARGUMENT);
}
#[test]
fn a_count_getter_clears_a_previous_failure() {
assert_eq!(unsafe { demo_blob_count(std::ptr::null()) }, -1);
assert_ne!(demo_last_error_kind(), crate::kind::NONE);
let doc = demo(Demo::default());
assert_eq!(unsafe { demo_blob_count(doc) }, 0);
assert_eq!(
demo_last_error_kind(),
crate::kind::NONE,
"a count of zero is a success, not a leftover failure"
);
unsafe { demo_free_document(doc) };
}
crate::export_bytes_getter!(
DEMO_ERROR,
demo_blob(doc: DemoDocument, id, out out_len),
{
let id = unsafe { crate::ffi::c_str_utf8(id) }?;
match unsafe { (*doc).inner.blobs.get(id) } {
Some(data) => Ok(data.clone()),
None => Err((crate::kind::OTHER, format!("no blob {id}"))),
}
}
);
#[test]
fn a_bytes_getter_hands_over_the_buffer_and_its_length() {
let doc = demo_with_blob("logo.png", b"\x89PNG\r\n");
let id = CString::new("logo.png").unwrap();
let mut out_len: usize = 0;
let data = unsafe { demo_blob(doc, id.as_ptr(), &mut out_len) };
assert!(!data.is_null());
assert_eq!(out_len, 6);
assert_eq!(
unsafe { std::slice::from_raw_parts(data, out_len) },
b"\x89PNG\r\n"
);
unsafe {
demo_free_bytes(data, out_len);
demo_free_document(doc);
}
}
#[test]
fn a_rejected_argument_leaves_out_len_untouched() {
let doc = demo_with_blob("logo.png", b"\x89PNG");
let id = CString::new("logo.png").unwrap();
const SEEDED: usize = 0xDEAD;
let mut out_len: usize = SEEDED;
assert!(unsafe { demo_blob(std::ptr::null(), id.as_ptr(), &mut out_len) }.is_null());
assert_eq!(out_len, SEEDED, "a null handle must not write out_len");
assert_eq!(demo_last_error_kind(), crate::kind::INVALID_ARGUMENT);
assert!(unsafe { demo_blob(doc, std::ptr::null(), &mut out_len) }.is_null());
assert_eq!(out_len, SEEDED, "a null key must not write out_len");
assert_eq!(demo_last_error_kind(), crate::kind::INVALID_ARGUMENT);
let missing = CString::new("absent.png").unwrap();
assert!(unsafe { demo_blob(doc, missing.as_ptr(), &mut out_len) }.is_null());
assert_eq!(out_len, 0, "a lookup that failed reports zero length");
assert_eq!(demo_last_error_kind(), crate::kind::OTHER);
unsafe { demo_free_document(doc) };
}
#[test]
fn a_null_out_len_is_rejected_and_named() {
let doc = demo_with_blob("logo.png", b"\x89PNG");
let id = CString::new("logo.png").unwrap();
assert!(unsafe { demo_blob(doc, id.as_ptr(), std::ptr::null_mut()) }.is_null());
let message = unsafe { std::ffi::CStr::from_ptr(demo_last_error()) }
.to_string_lossy()
.into_owned();
assert_eq!(message, "out_len is null");
unsafe { demo_free_document(doc) };
}
#[test]
fn a_non_utf8_key_is_classified_and_zeroes_the_length() {
let doc = demo_with_blob("logo.png", b"\x89PNG");
let raw = [0xFFu8, 0x00];
let mut out_len: usize = 0xBEEF;
let data = unsafe { demo_blob(doc, raw.as_ptr() as *const c_char, &mut out_len) };
assert!(data.is_null());
assert_eq!(demo_last_error_kind(), crate::kind::INVALID_ARGUMENT);
assert_eq!(
out_len, 0,
"the conversion happens inside the closure, so the failure path runs"
);
unsafe { demo_free_document(doc) };
}
#[test]
fn an_empty_buffer_is_handed_over_as_a_non_null_pointer_with_zero_length() {
let doc = demo_with_blob("empty.bin", b"");
let id = CString::new("empty.bin").unwrap();
let mut out_len: usize = 9;
let data = unsafe { demo_blob(doc, id.as_ptr(), &mut out_len) };
assert!(
!data.is_null(),
"an empty blob is a success, not an absent one"
);
assert_eq!(out_len, 0);
unsafe {
demo_free_bytes(data, out_len);
demo_free_document(doc);
}
}
}