Skip to content

← The Expert Edge: Idiom, Review and Capstones step 3 of 14

Hard End-to-End

A macro-generated FFI binding layer

Use macro_rules! to generate C declarations and Rust wrappers. The macro removes repeated code; you still need to decide which calls are safe and establish the foreign functions' preconditions.

Your task

Complete bind_c! with safe and raw arms, remove the ffi! convenience macro, and replace the hand-written bindings with:

bind_c! {
    safe sqrt(x: c_double) -> c_double => c_sqrt;
    raw  abs(x: c_int) -> c_int => c_abs;
    raw  strlen(s: *const c_char) -> usize => c_strlen;
    raw  memcmp(a: *const c_void, b: *const c_void, n: usize) -> c_int => c_memcmp;
}

Each entry must generate an unsafe extern "C" declaration with the given name and types, plus a wrapper with the name after =>.

  • safe generates a pub fn that calls the foreign function inside an explicit unsafe block.
  • raw generates a pub unsafe fn, also with an explicit unsafe call in its body. Its documentation must include a # Safety section explaining the caller's obligations.

Keep the supplied safe helper functions and the run entry point. The harness also invokes the macro for a raw labs binding, so both macro arms must work. The incomplete starter cannot satisfy that harness.

Results to preserve

pub fn run(xs: Vec<i32>, s: String) -> (i64, f64, usize, bool)
Result Meaning
sum Sum of absolute values in xs, accumulated as i64.
root C sqrt(sum as f64).
len Number of bytes before the first NUL byte in s, or its full byte length if there is none.
same Whether all bytes of s equal their reversal, including any interior NUL bytes.

For xs = [-3, 4, -5] and s = "aba", the result is approximately (12, 3.4641016151377544, 3, true). For s = "ab\0ba", the C-string length is 2, but the palindrome comparison uses all five bytes. An empty string has length zero and is a byte palindrome. A Unicode character palindrome need not be a byte palindrome. The driver accepts either s or a valid UTF-8 s_bytes array, which lets fixtures represent interior NULs.

A scalar argument can still need a safety check

C's abs cannot represent the absolute value of its most-negative int. The helper handles c_int::MIN after widening to i64, without calling C for that input. labs has the corresponding restriction for long; the harness uses a raw wrapper and calls it with a known representable value.

The safe keyword is a claim made by the binding author, not a proof checked by the macro. For this thin wrapper, callers must not need extra safety conditions beyond supplying valid Rust arguments. Correct foreign signatures, linking and any global-state requirements still need review. A floating-point domain error, such as a negative square root, is not by itself undefined behavior; it is distinct from the integer overflow restriction above.

Keep pointer checks in the safe helpers

strlen_of copies the bytes into a buffer, appends a NUL and keeps the buffer alive through strlen. An interior NUL is allowed here because the requested result is the length of the first C-string prefix.

bytes_equal checks lengths, returns early for empty slices, and then passes valid readable ranges to memcmp. It compares bytes, not Rust strings, so the reversed buffer does not need to be valid UTF-8.

These helpers establish the concrete preconditions that a general binding macro cannot infer from argument types alone.

Generate items and document the contract

Match one entry followed by $($rest:tt)*, emit its declaration and wrapper, then process the remaining entries. An empty arm () => {}; handles the end of the list. Identifiers capture names; ty fragments capture types.

#[doc = concat!("Wraps C ", stringify!($name), ".")] can name the function in generated documentation. A # Safety heading satisfies the documentation structure, but its text must still explain the actual contract. For example, strlen needs a readable NUL-terminated sequence, whereas memcmp needs readable ranges of the supplied length.

The starter's ffi!($call) places arbitrary caller expressions inside an unsafe block. Clippy rejects this pattern and also rejects public unsafe functions without safety documentation. Generate a specific foreign call from the captured declaration instead of suppressing those lints. Passing Clippy does not establish that a binding is sound.

Match the target C ABI exactly. The supplied types use std::ffi aliases, pointers, and usize for size_t on the supported targets. *const c_void is an opaque pointer type; c_void is not a general by-value C argument. String and &str are not substitutes for C strings. FFI-compatible structs can use repr(C), but their fields must also have suitable representations.