Skip to content

← Macros, FFI and Type-Driven Design step 26 of 28

Hard Framework

Nullable pointer optimization, callbacks and qsort

Complete a size report and two sorting functions that call the C library's qsort. Keep the supplied foreign declaration and sorting wrappers, and fix their Rust callbacks.

Required results

  • sort_by_abs_desc(v: Vec<i32>) -> Vec<i32>: sort by descending absolute value, breaking equal-magnitude ties by ascending signed value. [3, -5, 5, -3, 0] becomes [-5, 5, -3, 3, 0].
  • sort_pairs(v: Vec<(i32, i32)>) -> Vec<(i32, i32)>: sort by the second field ascending, then the first field ascending. [(3, 1), (1, 2), (2, 1)] becomes [(2, 1), (3, 1), (1, 2)].
  • niche_report() -> Vec<(String, usize)>: return the ten named size measurements in the order shown below, using size_of.

Preserve duplicates. Empty and single-element inputs are unchanged. Sorting must handle the full i32 range, including i32::MIN.

Measure Option representation

A niche is a representation that cannot be a valid value of a type. Rust guarantees that certain Option types use such a representation for None without increasing the size. For example, a function pointer cannot be null, and NonZeroU32 cannot be zero.

Report key Type Expected bytes on the exercise's 64-bit target
extern_fn extern "C" fn() 8
option_extern_fn Option<extern "C" fn()> 8
ref_u8 &u8 8
option_ref_u8 Option<&u8> 8
box_u8 Box<u8> 8
option_box_u8 Option<Box<u8>> 8
nonzero_u32 NonZeroU32 4
option_nonzero_u32 Option<NonZeroU32> 4
const_ptr_u8 *const u8 8
option_const_ptr_u8 Option<*const u8> 16

These are five type/Option pairs. The first four have the documented same-size guarantee. A raw pointer can already be null, so Some(null) and None must remain distinct. Its Option does not have the same guarantee; the 16-byte measurement is target-specific.

A nullable C callback can be represented by Option<extern "C" fn(...)>, with the correct argument and return types. Use an unsafe function-pointer type if calling it requires the caller to uphold unsafe preconditions. Our qsort callback is required, so the declaration uses a bare unsafe extern "C" fn and always passes a valid function.

Equal size alone does not prove ABI compatibility. A general Option<*const T> is not a C nullable-pointer representation, and not every type with a niche is automatically suitable for FFI. Check the documented representation and the ownership and validity rules of the specific boundary.

The qsort contract

The wrapper passes a live vector buffer, an element count, an element size in bytes, and a comparator. The buffer must remain valid and available for qsort to rearrange throughout the call. Here i32 and Pair are plain Copy values, and the callback only reads them.

The comparator receives pointers to two elements. Return a negative value when the first should come before the second, zero when they compare equal, and a positive value when it should come after. qsort does not require exactly -1 and 1, but casting Rust's Ordering to c_int provides those values conveniently.

Comparisons must be consistent across the sort. Equal comparison results for distinct elements are allowed: sorting only by magnitude is a valid ordering, but it does not satisfy this exercise's signed tie-break. qsort is not stable, so it does not promise the original relative order of elements the comparator considers equal. An inconsistent comparator violates the sorting contract and must not be used.

Compare without overflow

Do not implement the ordering by subtracting values. Subtraction can overflow, and wrapping can produce the wrong comparison sign.

Use unsigned_abs() for magnitudes. The magnitude of i32::MIN is 2,147,483,648, which fits in u32 but not in i32. Compare magnitudes in reverse order for descending output, then compare the original signed values in ascending order.

For pairs, compare (first.b, first.a) with (second.b, second.a). Tuple comparison is lexicographic: it considers the second component only when the first components are equal.

The callbacks must not unwind across this extern "C" boundary. An escaping Rust panic aborts the process. The supplied pointer reads rely on qsort providing valid, aligned pointers to the declared element type; the rest of each comparator can use non-panicking comparisons.

Callback ABI and pair layout

A bare function pointer has no captured environment. An ordinary non-capturing closure can coerce to a Rust fn pointer, but that does not give it the required C ABI. Use the supplied unsafe extern "C" fn callbacks. APIs that support stateful callbacks usually provide a separate context pointer; standard qsort does not. The opaque-pointer lesson covers passing context explicitly.

The wrapper converts tuples into a #[repr(C)] Pair buffer and passes size_of::<Pair>(). Keep that representation and size in agreement. However, qsort itself treats elements as opaque bytes: it does not read the Pair fields. A repr(C) layout would be essential if C code also interpreted those fields. Sorting opaque Rust tuples through matching Rust callbacks is not inherently forbidden merely because tuples lack a C field-layout guarantee.

Keep the len() > 1 guards. They avoid an unnecessary foreign call and keep empty-vector pointer considerations outside this wrapper.

The starter's magnitude ordering is ascending and lacks the requested tie-break; its pair ordering starts with the wrong field. Fix these comparisons and replace the zero placeholders in the report with actual measurements. Passing outputs is evidence for these cases, not a general proof of FFI safety.