Rust
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Ownership and Borrowing
-
Section 4: Structuring Data
-
Section 5: Collections and Error Handling
-
Section 6: Traits and Generics
-
Section 7: Concurrency
-
Section 8: Building for the Web
-
Section 9: Memory and Performance
-
Section 10: More Standard Library and Ecosystem
-
Section 11: Advanced Rust
-
Section 12: Rust for Systems and WebAssembly
-
Section 13: Tooling and Best Practices
-
Section 14: Data Structures and Algorithms in Rust
-
Section 15: Practical Projects
-
Section 16: Interview Practice
-
Section 17: std::collections In Depth
-
Section 18: std::io and std::fs In Depth
-
Section 19: std::net
-
Section 20: std::option and std::result In Depth
-
Section 21: std::iter In Depth
-
Section 22: std::sync In Depth
-
Section 23: std::string and std::str
-
Section 24: Cargo and Crates.io Ecosystem
-
Section 25: Popular Crates Ecosystem
-
Section 26: Rust Memory Model Deep Dive
-
Section 27: More Practice Exercises
-
Section 28: More Interview Practice
-
Section 29: Async Rust Deep Dive
-
Section 30: Tokio Ecosystem In Depth
-
Section 31: Error Handling Ecosystem Deep Dive
-
Section 32: Serde In Depth
-
Section 33: Web Development with Rust Deep Dive
-
Section 34: Database Access Ecosystem
-
Section 35: Rust for Embedded Systems Deep Dive
-
Section 36: Rust Macros In Depth
-
Section 37: Advanced Trait System
-
Section 38: Unsafe Rust In Depth
-
Section 39: Rust CLI Development
-
Section 40: Testing Ecosystem Deep Dive
-
Section 41: WebAssembly Deep Dive
-
Section 42: Rust Design Patterns
-
Section 43: More Data Structures in Rust
-
Section 44: Final Practice Projects
-
Section 45: Rust Performance Optimization
-
Section 46: Rust Ecosystem Tooling
-
Section 47: More Interview and Review
69: Foreign Function Interface with C
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.ccontaining: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) -> f32that wraps the unsafe C call. - In your
mainfunction, 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.rsscript with thecccrate or simply compile and link manually viagccandrustc).
There are no comments for now.