Skip to main content

export_bytes_getter

Macro export_bytes_getter 

Source
macro_rules! export_bytes_getter {
    (
        $(#[$meta:meta])*
        $slot:path,
        $name:ident($handle:ident: $handle_ty:ty, $key:ident, out $out_len:ident $(,)?),
        $body:block
    ) => { ... };
}
Expand description

Declare an entry point that hands out an owned byte buffer selected by a C-string key.

The signature is fixed — (handle, key, out_len) -> *mut u8 — because that is the shape every consumer of this ABI already exports, and only the names come from the call site. An entry point needing a different shape is written by hand.

$body evaluates to Result<Vec<u8>, FfiError>. The macro moves that into a Box<[u8]>, writes its length through out_len, and hands over the pointer; the matching export_free_bytes! reclaims it from the same two values.

An empty Vec is a success like any other: it is handed over as a non-null pointer with out_len set to 0, distinct from the null this returns on failure. A caller that treats a zero length as absence will misread an empty result as one that was never produced.

§Why every argument is checked before the closure

The failure path writes *out_len = 0, so when an argument is rejected is observable. All three null checks therefore happen before catch runs, and a rejected argument leaves the caller’s length variable exactly as they left it — “not attempted” stays distinguishable from “attempted and produced nothing”.

That is also why $body reads the key with ffi::c_str_utf8 rather than with_c_str!: the null check is already done, and the UTF-8 conversion belongs inside the closure, where its failure does zero the length.

unparser_shared::export_bytes_getter!(
    /// The bytes of the entry named `name`.
    ///
    /// # Safety
    ///
    /// - `doc` must be a valid handle.
    /// - `name` must be a valid null-terminated UTF-8 string.
    /// - `out_len` must point to storage for the length.
    /// - The returned pointer must be freed with `demo5_free_bytes`.
    LAST_ERROR,
    demo5_entry(doc: Demo5Document, name, out out_len),
    {
        let name = unsafe { unparser_shared::ffi::c_str_utf8(name) }?;
        match unsafe { (*doc).inner.entries.get(name) } {
            Some(bytes) => Ok(bytes.clone()),
            None => Err((unparser_shared::kind::OTHER, format!("no entry {name}"))),
        }
    }
);

let mut entries = HashMap::new();
entries.insert("a.bin".to_string(), vec![1u8, 2, 3]);
let doc = Box::into_raw(Box::new(Demo5Document { inner: Archive { entries } }));
let name = CString::new("a.bin").unwrap();

let mut out_len = 0usize;
let bytes = unsafe { demo5_entry(doc, name.as_ptr(), &mut out_len) };
assert_eq!(out_len, 3);
assert_eq!(unsafe { std::slice::from_raw_parts(bytes, out_len) }, [1, 2, 3]);

unsafe {
    demo5_free_bytes(bytes, out_len);
    demo5_free_document(doc);
}