DEV Community

Cover image for Rust for C Programmers: What I Wish I Knew from Day One
Francesco Pio Nocerino
Francesco Pio Nocerino

Posted on

Rust for C Programmers: What I Wish I Knew from Day One

Rust for C Programmers: What I Wish I Knew from Day One

If you’re coming from C like I did, Rust can feel simultaneously familiar and alien. You understand pointers, you know about stack vs heap, you’ve debugged memory leaks at 3 AM. But then Rust hits you with ownership, borrowing, and lifetimes, and suddenly you’re fighting the compiler more than you’re writing code.

Here’s what I wish someone had told me on day one.

The Mental Shift: From “Trust the Programmer” to “Trust the Compiler”

In C, you’re trusted. You can cast anything to anything, dereference null pointers, and create dangling pointers. The compiler assumes you know what you’re doing.

In Rust, the compiler is your paranoid copilot. It assumes you’ll make mistakes, and it won’t let you compile until it’s convinced your code is safe.

This was my biggest hurdle: I kept thinking “I know this is safe, just let me do it!” But here’s the thing - the compiler was usually right to be suspicious.

Ownership: It’s Just RAII on Steroids

If you’ve used RAII in C++ (or manually managed resources in C), ownership will click faster than you think.

C Version:

void process_data() {
    char *buffer = malloc(1024);
    // ... do stuff ...
    free(buffer);  // You must remember this
}
Enter fullscreen mode Exit fullscreen mode

Rust Version:

fn process_data() {
    let buffer = Vec::with_capacity(1024);
    // ... do stuff ...
    // Automatically freed here
}
Enter fullscreen mode Exit fullscreen mode

The key insight: In C, cleanup is your responsibility. In Rust, cleanup happens automatically when the owner goes out of scope. It’s deterministic (like C), but automatic (like garbage collection).

Borrowing: Pointers With Rules

Coming from C, you’re used to passing pointers everywhere. Rust has references, which are basically safe pointers.

The Rules (that will save your sanity):

Rule 1: You can have either:

  • One mutable reference (&mut T), OR
  • Multiple immutable references (&T)

Never both at the same time.

Why This Matters (C vs Rust):

C - The Problem:

void modify_data(int *data) {
    *data = 42;
}

void read_data(int *data) {
    printf("%d\n", *data);
}

int main() {
    int x = 10;
    int *ptr1 = &x;
    int *ptr2 = &x;

    modify_data(ptr1);  // Modifies x
    read_data(ptr2);    // Reads x - but when?
    // Data race possible in multithreaded code
}
Enter fullscreen mode Exit fullscreen mode

Rust - The Solution:

fn modify_data(data: &mut i32) {
    *data = 42;
}

fn read_data(data: &i32) {
    println!("{}", data);
}

fn main() {
    let mut x = 10;

    modify_data(&mut x);
    read_data(&x);

    // This won't compile - can't borrow as immutable while mutable borrow exists:
    // let r1 = &mut x;
    // let r2 = &x;  // ERROR!
}
Enter fullscreen mode Exit fullscreen mode

The insight: Rust’s borrowing rules eliminate entire classes of bugs at compile time - no more data races, no more use-after-free.

Lifetimes: The Compiler Tracking Your Pointers

In C, this is a classic bug:

char* get_string() {
    char buffer[100] = "Hello";
    return buffer;  // BOOM - returning pointer to stack memory
}
Enter fullscreen mode Exit fullscreen mode

Rust prevents this with lifetimes:

// This won't compile
fn get_string() -> &str {
    let s = String::from("Hello");
    &s  // ERROR: s is dropped here
}

// This works - ownership transferred
fn get_string() -> String {
    String::from("Hello")
}

// This also works - reference to data that outlives the function
fn get_slice(data: &str) -> &str {
    &data[0..5]  // Reference has same lifetime as input
}
Enter fullscreen mode Exit fullscreen mode

The pattern: If you’re returning a reference, it must point to either:

  1. Data passed in as a parameter
  2. Static data
  3. (Or just return owned data instead)

Memory Layout: Familiar Territory

Here’s where your C knowledge shines. Rust’s memory model is basically identical to C’s:

Concept C Rust
Stack allocation int x = 5; let x: i32 = 5;
Heap allocation malloc() Box::new(), Vec, String
Pointers int *ptr *const i32, *mut i32 (raw pointers)
References N/A (just pointers) &i32, &mut i32 (safe references)
Size of types sizeof(T) std::mem::size_of::<T>()

The difference: Rust adds zero-cost abstractions on top. A Vec<T> is just a pointer, length, and capacity - same as you’d implement in C.

The unsafe Keyword: Your Escape Hatch

Rust trusts you to use unsafe correctly. Inside an unsafe block, you can:

  • Dereference raw pointers
  • Call unsafe functions
  • Access mutable statics
  • Implement unsafe traits
fn manual_pointer_stuff() {
    let x = 42;
    let ptr = &x as *const i32;

    unsafe {
        println!("{}", *ptr);  // Just like C
    }
}
Enter fullscreen mode Exit fullscreen mode

When to use it:

  • FFI (calling C libraries)
  • Building safe abstractions that need unsafe internals
  • Performance-critical code where you’ve proven safety yourself

My rule: Use unsafe only when you must, and keep it isolated in small, well-tested functions.

Practical Migration Strategy

When I rewrote my first C project in Rust, here’s what worked:

Step 1: Start with the data structures

// C struct
struct Point {
    int x;
    int y;
};

// Rust equivalent
struct Point {
    x: i32,
    y: i32,
}
Enter fullscreen mode Exit fullscreen mode

Step 2: Replace malloc/free with Box or Vec

// C
int *data = malloc(sizeof(int) * 100);
free(data);
Enter fullscreen mode Exit fullscreen mode
// Rust
let data = vec![0; 100];  // Auto-freed
Enter fullscreen mode Exit fullscreen mode

Step 3: Turn function pointers into closures

// C
void apply(int *data, int (*func)(int)) {
    *data = func(*data);
}
Enter fullscreen mode Exit fullscreen mode
// Rust
fn apply<F>(data: &mut i32, func: F) 
where F: Fn(i32) -> i32 
{
    *data = func(*data);
}
Enter fullscreen mode Exit fullscreen mode

Step 4: Fight the borrow checker, learn, adapt

This is where you’ll spend 80% of your time initially. But each error teaches you something about writing safer code.

Common “WTF” Moments (and solutions)

1. “Can’t move out of borrowed content”

// Problem
fn process(data: &Vec<i32>) {
    let item = data[0];  // Works for Copy types
    let vec = data;      // ERROR: can't move
}

// Solution
fn process(data: &Vec<i32>) {
    let item = data[0];
    let vec = data.clone();  // Explicit clone
}
Enter fullscreen mode Exit fullscreen mode

2. “Borrowed value does not live long enough”

// Problem
let r;
{
    let x = 5;
    r = &x;  // ERROR: x dropped here
}

// Solution
let x = 5;
let r = &x;  // r's lifetime contained in x's lifetime
Enter fullscreen mode Exit fullscreen mode

3. “Cannot borrow as mutable more than once”

// Problem
let mut v = vec![1, 2, 3];
let first = &mut v[0];
let second = &mut v[1];  // ERROR!

// Solution - split_at_mut
let mut v = vec![1, 2, 3];
let (left, right) = v.split_at_mut(1);
// Now you have two mutable slices
Enter fullscreen mode Exit fullscreen mode

Performance: Still in Control

One fear I had: “Is Rust slower than C?”

The truth: Rust gives you the same control as C. You can:

  • Control memory layout with #[repr(C)]
  • Inline functions with #[inline]
  • Use SIMD with unsafe intrinsics
  • Avoid allocations entirely (embedded Rust)

Zero-cost abstractions means iterators, closures, and other high-level features compile to the same assembly as manual loops.

Tools That Make Life Better

Coming from C, these tools will feel like superpowers:

  • cargo - build system that actually works (no Makefiles!)
  • clippy - linter that teaches you Rust idioms
  • rustfmt - consistent formatting (no more style debates)
  • rust-analyzer - LSP that makes IDEs amazing

The Payoff

After fighting the compiler for a few weeks, something clicks. You start thinking in terms of ownership. You catch bugs before running code. You refactor fearlessly.

My C projects: “Let me carefully trace this pointer… is it still valid here?”

My Rust projects: “If it compiles, it probably works.”

Is it perfect? No. But it’s caught so many bugs before they reached production that I can’t imagine going back for new systems projects.

Your Turn

What’s been your biggest challenge moving from C to Rust? Or what’s holding you back from trying it?

Drop your questions in the comments - I learned most of this the hard way, so maybe I can save you some time!

Top comments (0)