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
- Write a function
int square(int x)in C andpub fn square(x: i32) -> i32in Rust.- Compile both with
-O2/--releaseand compare the assembly (useobjdump -dor godbolt.org).- Use
extern "C"to call your Csquarefrom Rust. Print the result.- Use
#[no_mangle] pub extern "C"to call your Rustsquarefrom C. Print the result.- 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.