Rust for C Developers

43 Posts

match and Pattern Matching in Depth

6 minute

C programmers often first read match as a stricter switch.

That comparison helps at the beginning, but it is incomplete. C switch mainly branches on integer values. If the state also carries data, you still manually read fields from a struct or union.

Rust match performs pattern matching:

check what shape a value has, and bind the data inside that shape.

Earlier, when discussing enum, we used Rust enums as a safer replacement for C tagged unions. This article continues from there: how match handles richer state, references, conditions, and local unpacking.

Read More

Drop in Depth

6 minute

Earlier we used Drop as the first replacement for C cleanup paths: when a value leaves scope, its resource is released automatically.

That already replaces many goto cleanup patterns. But when writing library code that wraps file handles, connections, locks, temporary directories, or FFI handles, “scope exit releases the value” is not enough.

C programmers still need answers to practical questions:

  • What is the exact cleanup order?
  • How do you release early?
  • What if cleanup can fail?
  • Which struct field is dropped first?
  • Can Drop move a field out?
  • How should an FFI handle be wrapped?

These are the parts of Drop that matter in real engineering code.

Read More

Mutable Borrowing

7 minute

In C, getting a T * usually means “this code may modify the value.” The hard part is that C does not stop you from keeping many access paths alive:

uint8_t *a = buf;
uint8_t *b = buf;

a[0] = 1;
printf("%u\n", b[0]);

This code is not automatically wrong. But in real projects, several pointers reading and writing the same data make state changes hard to track. Who changed it last? Who still holds an old view? Can a function modify it before the next read?

Read More

Move, Copy, and clone

5 minute

C developers often read assignment and parameter passing as copying a value:

uint32_t a = 10;
uint32_t b = a;

Struct values can also be copied:

struct Point p1 = {1, 2};
struct Point p2 = p1;

But once a struct contains a pointer, the meaning changes:

struct Buffer {
    uint8_t *data;
    size_t len;
};

struct Buffer b2 = b1; // only copies pointer and length

That is not a deep copy. b1.data and b2.data point to the same allocation. Who frees it? Can both mutate it? Can one still use it after the other frees it? C handles this by convention.

Read More

Testing

4 minute

C project tests often start as another main:

#include <assert.h>

int main(void)
{
    assert(checksum((uint8_t[]){1, 2, 3}, 3) == 6);
    return 0;
}

Or as small programs, shell scripts, and Makefile targets:

test:
	./test_checksum
	./test_parser

Rust puts the entry point into Cargo:

cargo test

First model: Rust tests are not a separate project system. They are part of the crate.

Minimal Unit Test

A normal function:

fn checksum(data: &[u8]) -> u32 {
    data.iter().map(|b| *b as u32).sum()
}

A nearby test:

Read More

CLI Output and Exit Status

4 minute

Command-line output is not just printing text. For shells, CI, and other programs, the interface is often:

stdout   -> normal data
stderr   -> diagnostics and errors
exit code -> success or failure

C code commonly writes:

printf("%s\n", result);
fprintf(stderr, "tool: %s\n", error);
return 1;

Rust has the same boundaries, with a few ways to express them.

stdout Is for Normal Results

Simple output uses println!:

println!("{count}\t{path}");

It writes to stdout, which is what users and pipelines consume:

Read More

Library API Design, First Layer

5 minute

In C library APIs, one pointer can mean many different things:

int parse_packet(const uint8_t *data, size_t len, struct Packet *out);
void normalize(uint8_t *data, size_t len);
void consume_buffer(uint8_t *data, size_t len);
uint8_t *make_packet(size_t *out_len);

These signatures all involve pointers, but the API meaning differs:

  • read-only input
  • in-place mutation
  • output result
  • taking over a caller-owned resource
  • creating a new resource for the caller

The first layer of Rust API design is making those intentions visible in the type.

Read-Only Input: Borrow

C read-only buffer:

uint32_t checksum(const uint8_t *data, size_t len);

Rust uses a slice:

fn checksum(data: &[u8]) -> u32 {
    data.iter().map(|b| *b as u32).sum()
}

&[u8] means the function temporarily reads bytes. It does not own the memory and cannot mutate it.

Read More

Dynamic Allocation and Containers

5 minute

C dynamic allocation usually starts with:

void *p = malloc(size);
p = realloc(p, new_size);
free(p);

Ordinary Rust code rarely calls the allocator directly. It usually uses owning types:

Box<T>     -> one T on the heap
Vec<T>     -> growable contiguous array on the heap
String     -> growable UTF-8 text on the heap

These types allocate and release memory internally, but callers usually do not write free. When the value leaves scope, Drop releases the owned allocation.

Box Is One Heap Object

C allocation for one struct:

Read More

Files, Directories, and Processes

5 minute

C tools and libraries quickly reach for interfaces like these:

FILE *fp = fopen(path, "rb");
fread(buf, 1, sizeof(buf), fp);
fclose(fp);

DIR *dir = opendir(path);
system("gzip file.log");
getenv("HOME");

Rust’s standard library splits the same area into a few modules:

std::fs       -> convenient file and directory operations
std::io       -> Read / Write / BufRead traits
std::env      -> arguments, environment variables, current directory
std::process  -> child processes, exit status, captured output

This article focuses on the first replacement layer. Ownership and automatic cleanup are covered by the Drop article.

Read More

Bit Flags and Bit-Fields

5 minute

C commonly uses bit flags:

#define FLAG_READ   (1u << 0)
#define FLAG_WRITE  (1u << 1)
#define FLAG_ERROR  (1u << 2)

uint32_t flags = FLAG_READ | FLAG_WRITE;

if (flags & FLAG_ERROR) {
    handle_error();
}

Rust can do the same bit operations, but one habit must change:

C:    if (flags & FLAG_ERROR)
Rust: if flags & FLAG_ERROR != 0

Rust if conditions must be bool. Integers are not implicitly truthy or falsy.

Bit Flags Can Be const

C often defines masks with macros:

Read More