Where C Shines, Where Rust Shines

Type This First

Save this as add.c:

// add.c
int add(int a, int b) {
    return a + b;
}

And this as main.rs:

extern "C" {
    fn add(a: i32, b: i32) -> i32;
}

fn main() {
    unsafe {
        println!("C says: {}", add(3, 4));
    }
}

Compile and link them together:

$ gcc -c -o add.o add.c
$ rustc main.rs -l static -L . --edition 2021
  ... wait, we need an archive:
$ ar rcs libadd.a add.o
$ rustc main.rs -l static=add -L . --edition 2021
$ ./main
C says: 7

C and Rust, cooperating. Same process. Same address space. Same binary.


This Is Not a Flame War

Both C and Rust compile to native machine code. Both give you direct memory access. Both have zero runtime overhead — no garbage collector, no virtual machine.

The question is not "which is better." The question is: which tradeoffs fit your project?


The Honest Comparison

+--------------------+---------------------------+----------------------------------+
| Dimension          | C                         | Rust                             |
+--------------------+---------------------------+----------------------------------+
| Kernel / OS dev    | THE standard              | Growing (Linux modules, Redox)   |
|                    | (Linux, Windows, macOS)   |                                  |
+--------------------+---------------------------+----------------------------------+
| ABI stability      | C ABI is THE universal    | No stable ABI; FFI goes          |
|                    | interface between langs   | through the C ABI                |
+--------------------+---------------------------+----------------------------------+
| Legacy codebases   | 50+ years of code         | Excellent C interop              |
|                    |                           | (bindgen, extern "C")            |
+--------------------+---------------------------+----------------------------------+
| Compile speed      | Fast                      | Slower (borrow checker,          |
|                    |                           | monomorphization, LLVM)          |
+--------------------+---------------------------+----------------------------------+
| Runtime overhead   | Zero                      | Zero (same as C, no GC)          |
+--------------------+---------------------------+----------------------------------+
| Memory safety      | Programmer discipline     | Compiler-enforced                |
+--------------------+---------------------------+----------------------------------+
| Concurrency safety | Discipline + sanitizers   | Compiler-enforced (Send/Sync)    |
+--------------------+---------------------------+----------------------------------+
| Tooling            | make, cmake, varied       | cargo (build+test+doc+publish    |
|                    | editors, gdb              | unified), clippy, rustfmt        |
+--------------------+---------------------------+----------------------------------+
| Embedded (common)  | Everywhere, mature        | Great and growing (Embassy,      |
|                    |                           | probe-rs, RTIC)                  |
+--------------------+---------------------------+----------------------------------+
| Embedded (exotic   | Often the only option     | Needs LLVM target support;       |
|   / 8-bit)         |                           | no AVR-8 stability yet           |
+--------------------+---------------------------+----------------------------------+
| Error handling     | errno, return codes (-1)  | Result<T,E>, Option<T>,          |
|                    | no enforcement            | ? operator, exhaustive matching  |
+--------------------+---------------------------+----------------------------------+
| Package management | manual / conan / vcpkg    | cargo + crates.io built-in       |
+--------------------+---------------------------+----------------------------------+
| Learning curve     | Small language, large     | Steeper (ownership, lifetimes),  |
|                    | footgun surface           | but compiler teaches you         |
+--------------------+---------------------------+----------------------------------+

Where C Shines

Operating system kernels. Linux is 30+ million lines of C. Windows kernel is C. macOS kernel is C. These are not being rewritten. When you write a Linux driver, you write C (or now, optionally Rust for new modules).

ABI stability. When Python calls a shared library, it uses the C ABI. When Java uses JNI, it uses the C ABI. When Rust calls foreign code, it uses the C ABI. C is the lingua franca of systems interfaces.

Existing codebases. SQLite: ~150,000 lines of carefully audited C. OpenSSL, zlib, libpng, curl — the infrastructure of the internet is C. You don't rewrite what works.

Exotic hardware. Writing firmware for an 8-bit PIC microcontroller? A DSP with a custom architecture? C has a compiler for it. Rust needs LLVM to support the target.

Team expertise. If your team has 20 years of C experience and deep knowledge of its pitfalls, that expertise is real and valuable.


Where Rust Shines

When correctness matters. Safety-critical systems. Financial software. Aerospace. Medical devices. The cost of a bug is not just a crash — it is lives or millions of dollars.

Concurrent code. The Send/Sync system catches data races at compile time. Chapter 23 showed you this. In C, concurrent bugs hide for years.

New projects. No legacy to maintain? No existing C codebase to integrate with? Rust gives you the same performance with a dramatically smaller bug surface.

When bugs are expensive. Google reported that ~70% of Chromium security bugs are memory safety issues. Microsoft reported the same for Windows. Each CVE costs investigation, patching, disclosure, and reputation. Rust eliminates the entire class.

Long-running services. A web server that runs for months. A database. Memory leaks that build up over days? Use-after-free that triggers once per million requests? Rust catches these before you deploy.


The Sharp Knife Metaphor

C gives you a sharp knife with no guard. Rust gives you the same sharp knife with a guard you can remove (unsafe) when needed. The blade is equally sharp.

Both produce the same machine code. Both give you the same control. The difference is in what the compiler checks before you run.

   C programmer's workflow:
   Write code -> Compile -> Run -> Test -> Find bug in prod -> Debug

   Rust programmer's workflow:
   Write code -> Compile (fight borrow checker) -> It compiles!
   -> Run -> Fewer bugs in prod

The borrow checker fight is real. It can be frustrating. But every error the borrow checker throws is a bug you did not ship.


FFI: They Interoperate, Not Compete

Calling C from Rust

// Declare the C function signature
extern "C" {
    fn strlen(s: *const u8) -> usize;
}

fn main() {
    let s = b"hello\0";
    let len = unsafe { strlen(s.as_ptr()) };
    println!("Length: {}", len);  // 5
}

The unsafe block is required because Rust cannot verify the C function's memory safety. You are telling the compiler: "I have checked this myself."

Calling Rust from C

#![allow(unused)]
fn main() {
// lib.rs
#[no_mangle]
pub extern "C" fn rust_add(a: i32, b: i32) -> i32 {
    a + b
}
}
// main.c
#include <stdio.h>

extern int rust_add(int a, int b);

int main(void) {
    printf("Rust says: %d\n", rust_add(10, 20));
    return 0;
}
$ rustc --crate-type=staticlib lib.rs -o librust_add.a
$ gcc main.c -L. -lrust_add -lpthread -ldl -o main
$ ./main
Rust says: 30

Same ABI. Same calling convention. Same registers. The CPU does not know which language produced the instructions.


Same Function, Same Assembly

Here is add in C and Rust:

int add(int a, int b) { return a + b; }
#![allow(unused)]
fn main() {
pub fn add(a: i32, b: i32) -> i32 { a + b }
}

Compile both with optimizations and look at the assembly:

; Both produce exactly this (x86-64, -O2):
add:
    lea  eax, [rdi+rsi]
    ret

Same instruction. Same registers. Same binary. The language is a compile-time concept. At runtime, there is only machine code.

Fun Fact

You can verify this yourself on godbolt.org. Type the C version in one pane and the Rust version in another. With optimizations enabled, the assembly is often instruction-for-instruction identical.


When to Choose What

  Choose C when:                      Choose Rust when:
  +-------------------------------+   +-------------------------------+
  | Extending a C codebase        |   | Starting a new project        |
  | Targeting exotic hardware     |   | Correctness is critical       |
  | Maximum ABI compatibility     |   | Heavy concurrency             |
  | OS kernel work (tradition)    |   | Bugs are very expensive       |
  | Team deeply knows C           |   | Long-running services         |
  | Interfacing with C-only libs  |   | Want unified tooling (cargo)  |
  +-------------------------------+   +-------------------------------+

  Choose BOTH when:
  +-------------------------------+
  | Wrapping C libs in safe Rust  |
  | Adding Rust to a C project    |
  | Performance-critical + safe   |
  +-------------------------------+

What do you think happens?

If you write a function in C and the same function in Rust, and both compile to the same assembly — what is the "cost" of Rust's safety? Where does the safety checking actually happen?

The answer: the cost is entirely at compile time. Zero runtime cost. The safety checks are erased before the binary is produced. This is Rust's core promise.


Task

  1. Write a function int square(int x) in C and pub fn square(x: i32) -> i32 in Rust.
  2. Compile both with -O2 / --release and compare the assembly (use objdump -d or godbolt.org).
  3. Use extern "C" to call your C square from Rust. Print the result.
  4. Use #[no_mangle] pub extern "C" to call your Rust square from C. Print the result.
  5. Bonus: Write a C function with a deliberate buffer overflow. Wrap it in Rust with a safe API that checks bounds before calling the C function. This is the pattern real-world Rust/C interop uses.