Skip to content

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

Medium Framework

Calling C functions through safe Rust wrappers

Call C's toupper, abs and strlen through safe Rust functions. Fix the unsafe call in shout, then implement the other two wrappers.

pub fn shout(s: &str) -> String
pub fn abs_sum(xs: Vec<i32>) -> i64
pub fn c_strlen_of(bytes: Vec<u8>) -> usize

Required behavior

  • shout uppercases ASCII lowercase letters using C's toupper. Preserve other ASCII bytes and all non-ASCII UTF-8 bytes. For example, "héllo" becomes "HéLLO", not "HÉLLO". Empty text stays empty, and embedded zero bytes are preserved because this function processes individual bytes.
  • abs_sum sums the absolute values into an i64. Call C's abs for every value except i32::MIN, whose magnitude must be computed in Rust using i64. Empty input returns zero; duplicates contribute separately. Inputs are chosen so the total fits in i64.
  • c_strlen_of appends a zero byte and calls C's strlen. Return the number of bytes before the first zero, whether that zero was already present or was appended. Empty input returns zero. Bytes need not be valid UTF-8.

Keep all three wrapper functions safe. Establish each C function's preconditions inside the wrapper and explain them in a // SAFETY: comment. The output tests check results; they do not prove that an implementation actually called C or that every unsafe operation is sound.

Platform assumptions

This exercise targets hosted systems with a C runtime, eight-bit bytes and a 32-bit C int. It also assumes the initial "C" character locale remains unchanged. The three symbols used here are available through the C runtime in ordinary desktop Rust builds, so the starter needs no additional crate or build script on those targets.

This is not a guarantee for every Rust target or every C function. Embedded environments may have no C runtime, and other libraries may need explicit linking. In larger projects, generated bindings or the libc crate help supply platform-specific declarations instead of writing them by hand.

What an extern declaration promises

The starter declares the functions in an unsafe extern "C" block. The "C" ABI selects the target's C calling convention. Edition 2024 requires the block to be marked unsafe, acknowledging that Rust cannot verify these declarations against the foreign implementation.

Use c_int and c_char for the corresponding C types. These aliases follow the target; the signedness of C char, for example, varies by platform. The strlen declaration uses usize for C's size_t on the targets of this exercise.

A declaration must match the foreign function's ABI and argument/return types. The compiler does not automatically consult C headers. It may catch some inconsistencies, but accepting a declaration does not prove that a later call is valid.

Functions in an extern block are unsafe to call unless explicitly declared safe. The starter's call therefore produces E0133. This is not fixed by syntax alone: check the input contract, then use an unsafe block around the call. Unsafe foreign calls needed an unsafe context before edition 2024 too; the required unsafe block declaration is the edition change.

toupper: valid input is not the same as Unicode support

C's toupper accepts EOF or a value representable as unsigned char. A non-ASCII byte promoted from u8 to c_int is within that domain on these targets. The dangerous conversion is sign-extending a negative char value that is not EOF.

We restrict conversion to ASCII here to preserve the bytes of multibyte UTF-8 characters. toupper is locale-sensitive, not a Unicode uppercase operation; the unchanged "C" locale gives the ASCII behavior required above. Do not change the locale for this exercise.

This complete example safely converts one ASCII byte:

use std::ffi::c_int;

unsafe extern "C" {
    fn toupper(c: c_int) -> c_int;
}

fn main() {
    // SAFETY: an ASCII byte promoted to c_int is in unsigned char's range.
    let upper = unsafe { toupper(c_int::from(b'q')) };
    assert_eq!(upper, c_int::from(b'Q'));
}

abs: widen before handling the minimum

For a 32-bit C int, abs(-2147483648) has undefined behavior because 2147483648 cannot be represented in the return type. Converting the result to i64 after the C call would be too late.

Detect i32::MIN first and calculate its magnitude in i64. For the other inputs, C's abs has a representable result, which you can widen before adding it to the total. For example, [-2147483648, 1] sums to 2147483649.

strlen: keep a terminator within the live buffer

A Rust byte vector knows its length; C's strlen instead reads until the first zero. Appending zero ensures that even an empty or unterminated input has a terminator within its allocation. Keep that vector alive and unchanged throughout the call.

For [97, 98, 0, 99, 100], the answer is 2. The later bytes still exist but are outside the C string. Cast the buffer pointer to *const c_char when passing it to the supplied declaration.

Because the wrappers receive valid Rust strings, integers or owned byte vectors, they can establish these preconditions themselves. Their callers should not need an unsafe block. Marking a wrapper unsafe would also break the harness's safe calls; adding safety documentation alone would not fix it.

Submissions must compile, pass the tests and pass clippy -D warnings.