Appendix B: Rust std for C Programmers

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.

I/O: stdio.h -> std::io, std::fs

CRustNotes
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}");
}
}

Memory: stdlib.h -> Box, Vec, ownership

CRustNotes
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
}

Strings: string.h -> String, &str

CRustNotes
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 == bDirect 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");
}

The &str vs String distinction

C:      const char *  (pointer to existing string)  <-->  &str
        char *buf     (owned, mutable buffer)        <-->  String

Rust rule: use &str for parameters, String for owned data.

Math: math.h -> std::f64, num traits

CRustNotes
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
INFINITYf64::INFINITYAssociated constant
NANf64::NANAssociated constant
isnan(x)x.is_nan()Method

No -lm flag needed. Math is built into the primitive types.

Process: unistd.h, stdlib.h -> std::process

CRustNotes
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);
}

Networking: sys/socket.h -> std::net

CRustNotes
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();
}
}

Error Handling: errno -> Result<T, E>

C patternRust equivalent
Return -1 and set errnoReturn 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)
}
}

Key Traits Every C Programmer Should Know

Read and Write

#![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)
}
}

Iterator

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();
}

Display and Debug

#![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 }
}

From and Into

#![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
}

Clone and Copy

#![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
}

Concurrency: pthreads -> std::thread, std::sync

C (pthreads)RustNotes
pthread_create()thread::spawn(closure)Closure captures data
pthread_join()handle.join()Returns Result
pthread_mutex_tMutex<T>Data inside the mutex
pthread_rwlock_tRwLock<T>
pthread_cond_tCondvar
sem_t(use Mutex + Condvar)No direct equivalent
atomic_intAtomicI32std::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());
}

Time: time.h -> std::time

CRustNotes
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:?}");
}

Collections: Manual C -> std::collections

C patternRust typeNotes
T array[N][T; N]Fixed-size array
T *arr + lenVec<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

Quick Conversion Cheatsheet

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