Skip to Content
Course content

69: Foreign Function Interface with C

Click on the "Edit" button in the top corner of the screen to edit your slide content.

I've seen a lot of developers jump into FFI (Foreign Function Interface) thinking it's just about marking a block as unsafe and calling a function. But the real danger isn't the unsafe keyword—it's the fundamental disagreement between how Rust and C view memory. Let's look at a snippet I reviewed recently from a dev trying to pass a configuration string to a legacy C library.

// The C side (libconfig.c)
// void print_config(const char* s) { 
//     printf("Config: %s\n", s); 
// }

extern "C" {
    fn print_config(s: *const std::os::raw::c_char);
}

fn main() {
    let config = "timeout=30;retry=3";
    unsafe {
        // This looks correct, but it's a disaster waiting to happen
        print_config(config.as_ptr() as *const std::os::raw::c_char);
    }
}

If you run this, you might get lucky and see "Config: timeout=30;retry=3". Or, more likely, you'll see "Config: timeout=30;retry=3&@#%^!" followed by a segmentation fault. The C function printf doesn't know how long the Rust string is; it just keeps reading memory until it happens to hit a null byte (\0). Since Rust strings are length-prefixed and not null-terminated, you're essentially asking C to read past the end of your buffer into random parts of your RAM.

The Missing Null Terminator

The core issue here is that Rust's &str and String are designed for efficiency and safety within the Rust ecosystem. They store a pointer and a length. C, however, relies on the "sentinel value" pattern. To a C function, a string is just a sequence of bytes that ends whenever it sees a zero.

When we used config.as_ptr(), we gave C the starting address, but we didn't give it a stopping point. This is the most common source of memory corruption in FFI. You aren't just passing data; you're passing a contract, and in this case, you broke the C contract.

Bridging the Gap with CString

To fix this, we need to ensure the data we pass to C is explicitly null-terminated. Rust provides std::ffi::CString for exactly this purpose. Unlike a standard String, CString guarantees there is a \0 at the end of the allocation.

use std::ffi::CString;
use std::os::raw::c_char;

extern "C" {
    fn print_config(s: *const c_char);
}

fn main() {
    let config_str = "timeout=30;retry=3";
    
    // CString::new creates a null-terminated string. 
    // It returns a Result because it fails if the input contains an internal null byte.
    let c_config = CString::new(config_str).expect("CString failed");

    unsafe {
        // .as_ptr() on a CString gives us exactly what C expects
        print_config(c_config.as_ptr());
    }
    // IMPORTANT: c_config must live until the C function returns.
    // If print_config stored this pointer for later, we'd have a use-after-free.
}

Notice that I used CString::new().expect(). I do this because if your Rust string contains a null byte in the middle, CString will refuse to create the object. Why? Because if it did, the C function would stop reading at that first internal null, effectively truncating your data without warning. Rust forces you to handle this possibility upfront.

Managing the Lifetime of FFI Data

One thing that often trips people up is the ownership of the pointer. In the example above, c_config is owned by the Rust main function. As soon as main ends (or c_config goes out of scope), that memory is freed.

If the C library just reads the string and returns, you're fine. But if the C library saves that pointer to a global variable or a long-lived struct, your Rust program will crash the next time the C library tries to access it. If you need the C side to "own" the memory, you'll need to use CString::into_raw(). This tells Rust: "Stop managing this memory; I'm handing it over to C." Just remember that you'll eventually have to bring it back into Rust using CString::from_raw() to free it, otherwise, you've just created a memory leak.




📋 Practical Task

Implementing a C-based Fast-Inverse-Square-Root Wrapper

In high-performance graphics, there's a famous (and strange) C implementation of the "Fast Inverse Square Root." Your task is to create a small C file and a Rust wrapper to use it.

Requirements:

  • Create a file named fast_math.c containing:
    float fast_inv_sqrt(float number) {
            long i;
            float x2, y;
            const float threehalfs = 1.5F;
            x2 = number * 0.5F;
            y  = number;
            i  = *(long *) &y;
            i  = 0x5f3759df - (i >> 1);
            y  = *(float *) &i;
            y  = y * (threehalfs - (x2 * y * y));
            return y;
        }
        
  • Write a Rust program that declares this function using an extern "C" block.
  • Implement a Rust function calculate_inv_sqrt(val: f32) -> f32 that wraps the unsafe C call.
  • In your main function, pass a value (e.g., 16.0) to the function and print the result.
  • Ensure your build process links the C file (you can use a build.rs script with the cc crate or simply compile and link manually via gcc and rustc).
Rating
0 0

There are no comments for now.

to be the first to leave a comment.