Skip to content

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

Hard End-to-End

Designing safe wrappers around unsafe FFI

Build three safe functions around the supplied C declarations for strtoll, memcmp and qsort. Keep the raw declarations in sys, establish their preconditions in the wrappers, and remove the two unused public raw-pointer helpers.

pub fn parse_int(s: &str) -> Result<i64, String>
pub fn bytes_equal(a: &[u8], b: &[u8]) -> bool
pub fn sorted(v: Vec<i32>) -> Vec<i32>

The harness calls all three functions for each case. Use the C functions to practice the wrapper boundary; output tests alone cannot distinguish every equivalent implementation.

Parse a complete decimal integer

parse_int first trims Rust whitespace from both ends. Apply these checks in order, returning the exact error text shown:

Condition Error
Trimmed text is empty empty input
Trimmed text contains a NUL byte interior nul byte
C consumes no numeric prefix no digits
C consumes only a prefix trailing junk after N bytes
Complete number does not fit in i64 out of range

Parse in base ten. A leading plus or minus is accepted. The exercise uses the ordinary C-locale decimal syntax: ASCII digits, with no separators or decimal point. Count consumed bytes from the start of the trimmed text, including a consumed sign, not from the original input.

For example, " +007 " returns Ok(7), "12abc" reports two consumed bytes, and "+" reports no digits. Both i64 limits are valid. A numeric prefix followed by junk reports trailing junk even if that prefix is too large to fit; the range check comes later.

Copy the trimmed bytes into a buffer with a final zero byte. Keep that buffer alive through the C call and while interpreting its end pointer. strtoll writes the address of the first unconsumed byte through endptr. Its contract relates that pointer to the input buffer, which justifies computing the number of consumed bytes.

Rejecting interior NUL is this API's chosen behavior. Passing such text unchanged as a C string would make C see only a prefix. It is not the only possible safe design, but silent truncation would violate this contract.

Validate range without relying on stale errno

C reports range errors through errno; returning an extreme value alone cannot distinguish a valid boundary value from overflow. Correct errno use requires clearing it before the call and inspecting it according to the function's contract. Its access mechanism is platform-specific and commonly thread-local, not a single process-global variable.

In this exercise, after confirming complete consumption, parse the trimmed string with Rust's parse::<i64>(). Accept it when parsing succeeds and its value agrees with C; otherwise return out of range. This deliberately duplicates conversion to avoid platform-specific errno bindings. A different production wrapper may use a platform library's errno support.

Compare valid byte ranges

bytes_equal returns false for different lengths and true for two empty slices. For equal nonzero lengths, call memcmp over that many bytes and test whether its result is zero. Embedded zero bytes are ordinary data here.

An empty slice may have a pointer that does not refer to an allocation. Returning early avoids passing such a pointer to C. Do not generalize this into a rule that every empty slice is dangling or that every C API treats zero-length buffers identically.

Sort without a panicking comparator

sorted returns the input values in ascending order, preserving duplicates. Empty and one-element vectors can return immediately. For larger vectors, pass qsort the mutable buffer pointer, element count and size_of::<i32>(). The buffer must remain live and exclusively available during sorting.

Its unsafe extern "C" comparator receives pointers to valid elements. Read them as i32 and compare with cmp, returning a negative integer, zero or a positive integer. Do not subtract the values to compare them: the subtraction can overflow at the integer limits. Use a consistent ordering.

A Rust panic reaching the non-unwinding extern "C" boundary aborts the process. It is not a recoverable sorting error. This differs from a foreign exception crossing into Rust through an incompatible non-unwinding boundary, which is undefined behavior. Keep this comparator's operations non-panicking under qsort's documented pointer contract.

Remove the unnecessary raw-pointer API

Delete parse_int_unchecked and bytes_equal_raw from the starter. The safe bytes_equal_raw accepts pointers whose validity it cannot establish and trusts the caller in a comment. A comment cannot impose unsafe obligations on safe callers. The other helper is declared unsafe but lacks a documented safety contract. Both are unnecessary for the requested interface.

Unsafe public functions can be appropriate when callers must establish preconditions the wrapper cannot check. Adding unsafe only to silence a lint is not a substitute for deciding whether that interface is needed. Keep unsafe operations inside these wrappers and explain their preconditions in the safety comments. The raw foreign declarations remain in sys.

What the wrapper must establish

Raw declarations can be wrong: verify their ABI, parameter types and target assumptions against the foreign API. For each call, account for pointer validity, alignment, length, initialization, lifetime and access permissions as applicable. These are contracts to check, not properties established by putting code in a module named sys.

A resource-owning wrapper can release a foreign resource in Drop, but forgetting it can still leak. Design ownership to prevent double release; Drop itself does not need to tolerate being called twice on the same owned value. Avoid cleanup panics, especially during unwinding.

A newtype can distinguish handle kinds. Add #[repr(transparent)] when you need the qualifying wrapper to share its field's layout and ABI; an ordinary one-field struct does not promise that representation. Runtime validity and ownership still need their own checks.

Passing requires compilation, correct results and Clippy with warnings treated as errors. Passing tests cannot prove the absence of undefined behavior; review the foreign contracts and wrapper invariants too.