We can't find the internet
Attempting to reconnect
Something went wrong!
Attempting to reconnect
← The Expert Edge: Idiom, Review and Capstones step 3 of 14
A macro-generated FFI binding layer
Write a macro_rules! macro that generates a C binding layer, and use it to
bind four libc functions.
pub fn run(xs: Vec<i32>, s: String) -> (i64, f64, usize, bool)
Macros exist to eliminate boilerplate. FFI binding layers are among the most
boilerplate-heavy code anyone writes in Rust: for every C function you want,
the same declaration, the same wrapper, the same unsafe block, the same
safety comment, over and over. Building the tool instead of using it is what
turns “I have seen macros” and “I have seen FFI” into one capability.
What the macro must generate
The harness invokes your macro itself, so its syntax is fixed:
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 produces two items:
-
an
unsafe extern "C"declaration of$name` with exactly that signature, and - a wrapper called `$wrap.
safe produces an ordinary pub fn whose body is the unsafe call.
raw produces a pub unsafe fn carrying a # Safety section in its docs.
The keyword is you making a claim, and you should only write safe when
the C function is genuinely total over its Rust parameter types.
::: question abs takes an int and returns an int. Why is it bound raw
rather than safe?
Because C’s abs is undefined behaviour at INT_MIN. -INT_MIN is not
representable in an int, and the C standard does not say the result wraps —
it says the behaviour is undefined. So abs has a precondition, therefore its
thin wrapper is unsafe, therefore something above it has to establish that
precondition before calling.
That is the whole discipline in one example. “Takes scalars, returns scalars” is not the test. “Is there any input for which this is not defined?” is.
sqrt passes that test: every f64 has defined behaviour, including negative
values (NaN) and infinities. It is bound safe.
:::
What run computes
-
sum — Σ|x| over
xs, asi64. Use the Cabsfor every value it is defined for, and handlei32::MINyourself. -
root —
sqrt(sum as f64)through the Csqrt. -
len — the length of
sas C sees it: bytes up to the first nul. C strings are nul-terminated, so you must build a nul-terminated buffer before you can hand a pointer tostrlen. Ifscontains an interior nul, C stops there and Rust does not — a difference worth internalising, because it is the root of a whole family of real security bugs. -
same — is
sequal to its own byte-reversal, decided by Cmemcmp? Compare lengths in Rust first;memcmptakes a single length and reading past the end of either buffer is UB. Zero-length is a special case worth thinking about.
The starter fails the lint gate, and the lint is the lesson
The starter compiles and passes every test. It still fails, on two clippy errors that are both about the same mistake.
clippy::macro_metavars_in_unsafe. The starter ships this “convenience”:
#[macro_export]
macro_rules! ffi {
($call:expr) => { unsafe { $call } };
}
Clippy’s note says exactly what is wrong: this allows the user of the macro
to write unsafe code outside of an unsafe block. ffi!(anything_at_all) runs
arbitrary caller-supplied code in an unsafe context, and the caller never
types the word unsafe. Every unsafe block is supposed to be a place a
reviewer stops and reads; this macro makes them invisible and unsearchable.
clippy::missing_safety_doc. A pub unsafe fn with no # Safety section
is an obligation you imposed on your callers without saying what it is.
A proper bind_c! fixes both at once, and it is worth understanding why
clippy accepts it. The lint’s own second suggestion is: or also expand
referenced metavariables in a safe context to require an unsafe block at
callsite. In bind_c!, $name` and `$arg appear in the extern declaration
and in the wrapper’s signature — safe positions — as well as inside the
unsafe block. The caller of bind_c! can only supply declarations,
never an arbitrary expression, so there is no way to smuggle code past a
reviewer. That is a real structural difference, not a lint-appeasement trick.
#[allow(clippy::macro_metavars_in_unsafe)] is the wrong answer here and
would be the wrong answer in review. Restructure.
Macro details that will bite
Item position. These bindings are items, not expressions. A macro used in
item position must expand to items, so $e:expr` fragments are no use — you
want `ident`, `ty` and raw token trees. The natural way to handle a list of
entries with two different leading keywords is a **TT muncher**: match one
entry plus `$($rest:tt)*`, emit the items, then recurse with `bind_c!($($rest)*);`.
Give it an empty `() => {};` base case or it will never terminate.
**Generating docs.** `#[doc = "..."]` is what `///` desugars to, and
`#[doc = concat!("Wraps C `", stringify!($name), ".”)]lets you build a doc line from a metavariable.///lines work too and can be mixed in. You need a real# Safetyheading in the generated docs for therawarm. **FFI-safe types only.** rustc'simproper_ctypeslint checks the *generated* declarations, so a macro that happily accepts&strorStringproduces code that fails the gate.c_int,c_double,c_char,c_void, raw pointers andusizeare fine; nothing with a Rust-defined layout is. **crate_in_macro_def.** Not needed here, but worth knowing: writingcrate::inside amacro_rules!body resolves at the *call* site, not the definition site, which is almost never what you meant.$crate::is the fix. ## One honest note on the safe layer Above the generated wrappers you write three small functions in ordinary Rust: thei32::MINguard forabs, the nul-terminated buffer forstrlen, the length check formemcmp. That is the shape every good binding crate has — a thin generatedsys` layer that mirrors the header exactly, and a hand-written
safe layer above it that does the thinking. Do not try to make the macro
clever enough to generate the safe layer. The whole point of the safe layer
is that a human decided what the precondition was and how to establish it,
and that reasoning does not come out of a token substitution.
Stuck?
Rust reference solution
Sign in to attempt this problem and reveal the reference solution.