We can't find the internet
Attempting to reconnect
Something went wrong!
Attempting to reconnect
← Macros, FFI and Type-Driven Design step 27 of 28
Opaque handles and bounded C formatting
Fix two bugs in the supplied C interoperability code: the callback records individual events instead of running totals, and the formatter misses one truncation boundary.
pub fn accumulate(events: Vec<i32>) -> Vec<i64>
pub fn format_pairs(items: Vec<(i32, String)>) -> Vec<String>
pub fn truncated(items: Vec<(i32, String)>) -> Vec<bool>
Keep the opaque-handle callback and snprintf calls. The purpose is to
understand and repair those interfaces.
Required results
accumulate returns the total after each event. For example,
[5, -2, -10, 7] produces [5, 3, -7, 0]; empty input produces an
empty vector. Each call starts at zero. Inputs used here have running totals
that fit in i64.
For each pair (n, text), format_pairs formats n-text using a
32-byte C buffer. That allows 31 output bytes plus the terminating null byte.
truncated reports whether the full output would exceed those 31 bytes.
| Input pair | Formatted output | Truncated? |
|---|---|---|
(7, "ok") |
"7-ok" |
No |
(-3, "x") |
"-3-x" |
No |
(1, "b" repeated 29 times) |
"1-" plus all 29 letters |
No |
(1, "c" repeated 30 times) |
"1-" plus the first 29 letters |
Yes |
Preserve the supplied error behavior: embedded null bytes in text, or
a negative snprintf result, produce an empty string and false.
These cases are errors, so false does not mean formatting succeeded.
The limit counts bytes, not Unicode characters. If truncation splits a UTF-8
character, the supplied to_string_lossy conversion replaces the incomplete
sequence with a replacement character. The returned Rust string can therefore
have a different byte length from the truncated C buffer.
Passing state through an opaque handle
Many C callback APIs accept a context pointer alongside a function pointer. The context identifies the state that a closure would otherwise capture.
Here, accumulate creates a Box<State>, converts it to a raw pointer,
and passes it as *mut Counter. Each call through c_api::counter_push
casts the handle back to State and updates it. After all calls finish,
Box::from_raw reconstructs the owning box exactly once.
Both sides of this callback are implemented in Rust; the declaration and exported function use the C ABI. This exercises the calling interface without requiring a separate C callback library.
Counter is a distinct handle type whose private fields prevent ordinary
construction outside its module. Its marker fields suppress Send, Sync,
and Unpin. They do not validate raw pointers, enforce thread affinity at
runtime, or pin the allocation automatically. The boxed State remains in
place because this code leaves it allocated until the calls are finished.
Do not dereference the handle as though it contained a real Counter.
It points to a State. Different handle types help avoid mixing APIs,
but the unsafe implementation must still recover the correct pointee type.
A cast alone does not access memory. The subsequent reference creation is where the pointer must be aligned, live, and valid for exclusive access. Concurrent or reentrant accesses could violate that requirement. Real asynchronous callback APIs also need a way to stop callbacks before freeing their context.
Matching variadic arguments
snprintf has fixed buffer, size, and format parameters followed by
.... Rust checks the fixed arguments and rejects some unsupported
variadic argument types. It does not verify that each extra argument matches
a conversion in the format string.
For this call, %d needs a C int and %s needs a pointer to a live,
null-terminated C string. The supported target uses i32 for c_int.
CString supplies the string storage; keep it alive throughout the call.
C's default argument promotions also matter. Rust requires explicit casts
for types such as f32 and i8 in variadic positions: use f64 and
c_int, respectively. The compiler reports E0617 for the unpromoted types.
A successful compile still does not establish that the format and arguments
agree.
Keep the format fixed as c"%d-%s" and pass user text as an argument.
Treating user text as a format could make C consume arguments that were never
supplied.
Detecting truncation
On success, snprintf returns the byte count that the full output would
require, excluding its terminator. Check for a negative error result before
converting that count to usize.
With this 32-byte buffer, a result below 32 fits; a result of 32 or more means truncation. To allocate enough space for a retry, you would need the reported count plus one for the terminator.
The starter compares with >, so it misses the case where the reported
count equals 32. Correct that comparison and make the callback append the
updated total.
Reviewing a C boundary
Check the declaration against the actual C signature and target ABI. Document who owns each allocation, when pointers remain valid, and which matching function releases them. Aliasing raw pointers is not automatically invalid, but accesses must honor Rust reference rules and the C API's own restrictions.
Plan for panic behavior too. An unwinding Rust panic that escapes an
extern "C" function aborts the process. catch_unwind can handle an
unwinding Rust panic before that boundary, but cannot catch an aborting panic.
Permitting unwinding requires a compatible ABI and agreement on both sides.
Generated bindings still need review of these contracts. Likewise, Miri can model some foreign operations through built-in shims, but cannot generally interpret arbitrary native library code. Passing tests and Clippy is useful evidence, not a proof that an FFI boundary is sound.
References: Rust FFI guidance, variadic argument promotions, and Miri limitations.
Your code, saved here
Sign in to access your accepted solutions and saved draft history. You can keep reading the lesson without an account.
Sign in to your accountStuck?
Rust reference solution
Sign in to track your attempts toward unlocking the reference solution.