This appendix maps C standard library functions and patterns to their Rust
equivalents. If you know the C function, find the Rust way to do the same
thing.
| C | Rust | Notes |
printf("x=%d\n", x) | println!("x={x}") | Format macros |
fprintf(stderr, ...) | eprintln!(...) | Stderr output |
sprintf(buf, ...) | format!(...) | Returns String |
snprintf(buf, n, ...) | write!(buf, ...) | Into any Write impl |
fopen(path, "r") | File::open(path) | Returns Result<File> |
fopen(path, "w") | File::create(path) | Truncates existing |
fclose(f) | (automatic via Drop) | RAII closes the file |
fread(buf, 1, n, f) | f.read(&mut buf) | Read trait |
fwrite(buf, 1, n, f) | f.write_all(&buf) | Write trait |
fgets(buf, n, f) | reader.read_line(&mut s) | BufRead trait |
fseek(f, off, SEEK_SET) | f.seek(SeekFrom::Start(off)) | Seek trait |
fflush(f) | f.flush() | Write trait |
perror("msg") | eprintln!("msg: {e}") | Where e is the error |
#![allow(unused)]
fn main() {
// Reading a file in Rust (C equivalent: fopen + fread + fclose)
use std::fs;
let contents = fs::read_to_string("file.txt").expect("read failed");
// Line-by-line reading (C equivalent: fgets loop)
use std::io::{self, BufRead};
let file = fs::File::open("file.txt").expect("open failed");
for line in io::BufReader::new(file).lines() {
let line = line.expect("read line failed");
println!("{line}");
}
}
| C | Rust | Notes |
malloc(n) | Box::new(value) | Single heap object |
malloc(n * sizeof(T)) | Vec::with_capacity(n) | Dynamic array |
calloc(n, size) | vec![0; n] | Zeroed allocation |
realloc(p, new_size) | v.reserve(additional) | Vec grows automatically |
free(p) | (automatic via Drop) | RAII frees memory |
memcpy(dst, src, n) | dst.copy_from_slice(src) | Slices |
memmove(dst, src, n) | slice.copy_within(range, dest) | Overlapping |
memset(p, 0, n) | buf.fill(0) | Fill slice |
memcmp(a, b, n) | a == b or a.cmp(&b) | Slice comparison |
#![allow(unused)]
fn main() {
// Heap allocation (C: malloc + use + free)
let boxed = Box::new(42); // heap-allocated i32
println!("{boxed}"); // auto-deref
// freed automatically when boxed goes out of scope
// Dynamic array (C: malloc + realloc pattern)
let mut v: Vec<i32> = Vec::new();
v.push(1);
v.push(2);
v.push(3);
// no manual realloc or free needed
}
| C | Rust | Notes |
strlen(s) | s.len() | O(1) in Rust (stored length) |
strcpy(dst, src) | let dst = src.to_string() | New allocation |
strcat(dst, src) | dst.push_str(src) | Append to String |
strcmp(a, b) | a == b | Direct comparison |
strchr(s, c) | s.find(c) | Returns Option<usize> |
strstr(hay, needle) | hay.find(needle) | Returns Option<usize> |
strtok(s, delim) | s.split(delim) | Returns iterator |
strtol(s, NULL, 10) | s.parse::<i64>() | Returns Result |
atoi(s) | s.parse::<i32>().unwrap() | Panics on failure |
#![allow(unused)]
fn main() {
// String operations (C equivalents in comments)
let s = String::from("hello"); // like strdup("hello")
let len = s.len(); // like strlen(s)
let upper = s.to_uppercase(); // no C equivalent in string.h
// Splitting (C: strtok loop)
let csv = "a,b,c,d";
let parts: Vec<&str> = csv.split(',').collect();
// parts = ["a", "b", "c", "d"]
// Parsing numbers (C: strtol)
let n: i64 = "42".parse().expect("not a number");
}
C: const char * (pointer to existing string) <--> &str
char *buf (owned, mutable buffer) <--> String
Rust rule: use &str for parameters, String for owned data.
| C | Rust | Notes |
sqrt(x) | x.sqrt() or f64::sqrt(x) | Method on f64 |
pow(x, y) | x.powi(n) / x.powf(y) | Integer/float exponent |
fabs(x) | x.abs() | Method |
ceil(x) | x.ceil() | Method |
floor(x) | x.floor() | Method |
round(x) | x.round() | Method |
log(x) | x.ln() | Natural log |
log10(x) | x.log10() | Base-10 log |
sin(x) | x.sin() | Radians |
INFINITY | f64::INFINITY | Associated constant |
NAN | f64::NAN | Associated constant |
isnan(x) | x.is_nan() | Method |
No -lm flag needed. Math is built into the primitive types.
| C | Rust | Notes |
fork() | (no direct equivalent) | Use Command::new() |
exec*() | Command::new(prog).exec() | Via std::os::unix |
system(cmd) | Command::new("sh").arg("-c").arg(cmd).status() | |
exit(n) | std::process::exit(n) | |
getpid() | std::process::id() | Returns u32 |
getenv("PATH") | std::env::var("PATH") | Returns Result<String> |
pipe() | Command::new(...).stdin(Stdio::piped()) | |
waitpid() | child.wait() | Returns ExitStatus |
#![allow(unused)]
fn main() {
// Running a child process (C: fork + exec + wait)
use std::process::Command;
let output = Command::new("ls")
.arg("-la")
.output()
.expect("failed to execute");
println!("stdout: {}", String::from_utf8_lossy(&output.stdout));
println!("exit: {}", output.status);
}
| C | Rust | Notes |
socket(AF_INET, SOCK_STREAM, 0) | TcpListener::bind(addr) | Combined |
connect() | TcpStream::connect(addr) | |
bind() + listen() | TcpListener::bind(addr) | |
accept() | listener.accept() | Returns (TcpStream, SocketAddr) |
send(fd, buf, n, 0) | stream.write_all(&buf) | Write trait |
recv(fd, buf, n, 0) | stream.read(&mut buf) | Read trait |
close(fd) | (automatic via Drop) | |
inet_ntop() | addr.to_string() | Display trait |
htons(port) | (automatic) | Rust handles byte order |
#![allow(unused)]
fn main() {
// TCP echo server (C equivalent: socket + bind + listen + accept loop)
use std::net::TcpListener;
use std::io::{Read, Write};
let listener = TcpListener::bind("127.0.0.1:8080").unwrap();
for stream in listener.incoming() {
let mut stream = stream.unwrap();
let mut buf = [0u8; 1024];
let n = stream.read(&mut buf).unwrap();
stream.write_all(&buf[..n]).unwrap();
}
}
| C pattern | Rust equivalent |
Return -1 and set errno | Return Err(io::Error) |
Check if (ret < 0) | match or ? operator |
perror("msg") | eprintln!("msg: {e}") |
strerror(errno) | e.to_string() |
#![allow(unused)]
fn main() {
// The ? operator replaces C error-checking boilerplate
use std::io;
use std::fs;
fn read_config() -> io::Result<String> {
let contents = fs::read_to_string("/etc/myapp.conf")?; // returns Err on failure
Ok(contents)
}
}
#![allow(unused)]
fn main() {
use std::io::{Read, Write};
// Anything that implements Read can be read from:
// File, TcpStream, &[u8], Stdin, ...
fn process_input(mut reader: impl Read) -> io::Result<String> {
let mut buf = String::new();
reader.read_to_string(&mut buf)?;
Ok(buf)
}
}
Replaces C for-loops over arrays and linked lists.
#![allow(unused)]
fn main() {
// C: for (int i = 0; i < n; i++) sum += arr[i];
let sum: i32 = arr.iter().sum();
// C: filter + transform loop
let even_squares: Vec<i32> = (0..10)
.filter(|x| x % 2 == 0)
.map(|x| x * x)
.collect();
}
#![allow(unused)]
fn main() {
use std::fmt;
// Display: for user-facing output (like a custom printf format)
impl fmt::Display for MyType {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "MyType({})", self.value)
}
}
// Debug: for developer output (derive it)
#[derive(Debug)]
struct Point { x: f64, y: f64 }
let p = Point { x: 1.0, y: 2.0 };
println!("{p:?}"); // Point { x: 1.0, y: 2.0 }
}
#![allow(unused)]
fn main() {
// From<T>: conversion from one type to another
// Replaces C's explicit casting and conversion functions
let s: String = String::from("hello");
let n: i64 = i64::from(42i32);
// Into<T>: the reverse direction (auto-derived from From)
fn takes_string(s: String) { /* ... */ }
takes_string("hello".into()); // &str -> String via Into
}
#![allow(unused)]
fn main() {
// Copy: bitwise copy (like C assignment for simple types)
// Applies to: integers, floats, bool, char, references
let a: i32 = 5;
let b = a; // copy, both valid
// Clone: explicit deep copy (like manual malloc + memcpy)
let s1 = String::from("hello");
let s2 = s1.clone(); // explicit deep copy
// Both s1 and s2 are valid
}
| C (pthreads) | Rust | Notes |
pthread_create() | thread::spawn(closure) | Closure captures data |
pthread_join() | handle.join() | Returns Result |
pthread_mutex_t | Mutex<T> | Data inside the mutex |
pthread_rwlock_t | RwLock<T> | |
pthread_cond_t | Condvar | |
sem_t | (use Mutex + Condvar) | No direct equivalent |
atomic_int | AtomicI32 | std::sync::atomic |
| (shared data) | Arc<T> | Thread-safe ref counting |
#![allow(unused)]
fn main() {
use std::sync::{Arc, Mutex};
use std::thread;
let counter = Arc::new(Mutex::new(0));
let handles: Vec<_> = (0..4).map(|_| {
let c = Arc::clone(&counter);
thread::spawn(move || {
let mut num = c.lock().unwrap();
*num += 1;
})
}).collect();
for h in handles { h.join().unwrap(); }
println!("Result: {}", *counter.lock().unwrap());
}
| C | Rust | Notes |
time(NULL) | SystemTime::now() | Wall clock |
clock_gettime(CLOCK_MONOTONIC) | Instant::now() | For benchmarks |
sleep(n) | thread::sleep(Duration::from_secs(n)) | |
difftime(t1, t0) | t1.duration_since(t0) | Returns Duration |
strftime(...) | (use chrono crate) | No built-in formatting |
#![allow(unused)]
fn main() {
use std::time::Instant;
let start = Instant::now();
// ... work ...
let elapsed = start.elapsed();
println!("Took {elapsed:?}");
}
| C pattern | Rust type | Notes |
T array[N] | [T; N] | Fixed-size array |
T *arr + len | Vec<T> | Dynamic array |
| Hash table (hand-rolled) | HashMap<K, V> | |
| Binary tree (hand-rolled) | BTreeMap<K, V> | Sorted |
| Linked list (hand-rolled) | LinkedList<T> | (rarely used) |
| Bit set (manual) | HashSet<T> or bitflags crate | |
| Ring buffer (manual) | VecDeque<T> | Double-ended queue |
C type --> Rust type
int i32
unsigned int u32
long i64 (on 64-bit Linux)
size_t usize
ssize_t isize
char u8 (byte) or char (Unicode)
char * &str (borrowed) or String (owned)
void * *const u8 / *mut u8 or &[u8]
NULL None (in Option<T>)
FILE * File (in std::fs)
bool (C99) bool
_Bool bool