Rust

43 Posts

Type Aliases and Newtype

5 minute

C uses typedef heavily:

typedef uint32_t DeviceId;
typedef uint8_t Byte;
typedef int (*compare_fn)(const void *a, const void *b);

It reduces repetition, hides complex declarations, and gives a business name to a type. But typedef does not always create a stronger type boundary. Often it is just another name for the original type.

Rust has two common tools:

type DeviceId = u32;      // type alias, not a new type
struct DeviceId(u32);     // newtype, a distinct type

The important split is simple: use an alias to simplify names; use a newtype when you want the compiler to stop accidental mixing.

Read More

Structs and Memory Layout

6 minute

C developers often read struct as a memory layout:

#include <stdint.h>

struct Header {
    uint8_t version;
    uint32_t length;
    uint16_t flags;
};

Fields are laid out in declaration order, padding may be inserted, and the whole struct has an alignment requirement. That mental model matters for protocols, file formats, register maps, and FFI.

Rust also has struct, but the first split is this:

normal Rust struct: describes an internal data model
#[repr(C)] struct: describes a layout that must match the C ABI

The keyword is familiar, but the default layout contract is not the same. In ordinary Rust code, treat a struct as named fields. Bring memory layout into the type only when an ABI or binary boundary needs it.

Read More

Pointers, References, and Raw Pointers

6 minute

C uses T * everywhere. It can mean many different things:

void print_config(const struct Config *cfg);
void reset_config(struct Config *cfg);
int parse_packet(const uint8_t *data, size_t len, struct Packet *out);
void driver_write(void *addr, uint32_t value);

These all look like pointers, but the meanings differ:

  • read-only access to an object
  • mutation of a caller-provided object
  • pointer plus length for a buffer
  • output parameter
  • nullable pointer
  • hardware address or FFI-provided address

Rust does not put all of these into one pointer concept. Start with this table:

const T *              -> &T / Option<&T>
T *                    -> &mut T / Option<&mut T>
const T * + len        -> &[T]
T * + capacity         -> &mut [T]
out parameter          -> return value / &mut T / &mut [T]
void *                 -> generic, trait, raw pointer, or FFI context
real address / FFI ptr -> *const T / *mut T

The first layer is the split itself: ordinary library APIs should prefer references and slices, while raw pointers should stay close to FFI, hardware addresses, and low-level wrappers.

Read More

Modules and Files: from .h/.c to mod

5 minute

C projects commonly split .h and .c files like this:

packet.h   -> public structs, functions, macros
packet.c   -> function implementations and private helpers
main.c     -> includes packet.h and calls public functions

For example:

/* packet.h */
int packet_parse(const uint8_t *data, size_t len, struct Packet *out);

/* packet.c */
static int parse_header(const uint8_t *data, size_t len);

int packet_parse(const uint8_t *data, size_t len, struct Packet *out)
{
    /* ... */
}

Rust also separates files, internal implementation, and public APIs, but the model is not #include. The first mapping is:

C .h/.c         -> Rust module + pub
C include       -> Rust use / module path
C static helper -> Rust private function by default
C public header -> Rust pub API

Cargo.toml describes the package

C projects usually rely on Makefiles, CMake, or another build system to describe source files, include paths, and library outputs.

Read More

Scope, Bindings, and Visibility

6 minute

C programmers usually think of scope first as name visibility:

void handle(void)
{
    int offset = 0;

    {
        int len = 16;
        offset += len;
    }

    /* len is not visible, offset is still visible */
}

Rust block scope has the same basic rule:

fn handle() {
    let mut offset = 0;

    {
        let len = 16;
        offset += len;
    }

    // len is not visible, offset is still visible
}

But Rust scope is not only about whether a name can be used. It also affects when resources are released, whether a binding can be shadowed, whether a reference can continue to exist, and which names become part of a module’s public API.

Read More

Functions, Parameters, and Return Values

6 minute

For a C programmer, a Rust function first looks like the same idea with syntax moved around:

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

In Rust:

fn add(a: i32, b: i32) -> i32 {
    a + b
}

They do the same work, but Rust functions introduce a few early habits:

  • parameter names come before types
  • return types come after ->
  • the final expression can be the return value
  • return is often used for early exits
  • functions without useful return values return ()
  • C out parameters and return codes often become Rust return values and Result

This is not full ownership theory, but it immediately affects how you write a first library function.

Read More

Control Flow: if, while, loop, and for

6 minute

C programmers usually start with this mapping:

C if       -> Rust if
C while    -> Rust while
C for      -> Rust for
C break    -> Rust break
C continue -> Rust continue

The mapping is mostly right, but a few differences affect everyday code:

  • if and while conditions must be bool
  • if can produce a value
  • Rust has no C-style for (init; condition; step)
  • Rust for iterates over an iterable value
  • loop expresses an intentional infinite loop and can return a value with break value

These are not advanced features. They show up immediately in CLI tools and buffer processing.

Read More

Operators, Expressions, and Casts

5 minute

For C programmers, the tricky part of Rust operators is not whether +, -, *, and / look familiar. The tricky part is that many conversions C performs automatically do not happen in Rust.

C often allows code like this:

#include <stdint.h>
#include <stddef.h>

uint8_t version = 1;
size_t len = 128;
uint32_t total = version + len;

if (total) {
    /* ... */
}

Rust asks you to spell out the type relationship:

let version: u8 = 1;
let len: usize = 128;
let total: usize = version as usize + len;

if total != 0 {
    // ...
}

The first adjustment is that type changes must be visible in the code. Control-flow structure builds on top of that.

Read More

Basic Types, Variables, and Constants

3 minute

The first obstacle for a C programmer in Rust is not ownership. It is being able to write ordinary variables, constants, and basic types without fighting the language.

C code often starts like this:

#include <stdint.h>
#include <stdbool.h>

#define MAX_PACKET_SIZE 1024

int retry = 3;
uint32_t flags = 0;
bool enabled = true;
char tag = 'A';

Rust can express the same ideas, but the defaults are different:

const MAX_PACKET_SIZE: usize = 1024;

let mut retry: i32 = 3;
let flags: u32 = 0;
let enabled: bool = true;
let tag: u8 = b'A';

Struct layout, arrays, pointers, and strings all build on these basic types. Getting bindings, constants, and widths right first keeps later memory and API decisions from blurring together.

Read More

FFI: Letting Rust Call C

6 minute

Calling C from Rust can look like just writing an extern "C" declaration.

The real issue is different: a C function does not carry Rust’s borrowing, lifetime, Result, or destructor rules. Rust can call it, but the compiler cannot prove that the call satisfies the C API contract.

A better model is:

safe Rust API
-> validate and prepare arguments
-> call C inside a small unsafe block
-> convert error codes, output parameters, and raw pointers back to Rust types

The C header is the entry point

Suppose an existing C library exposes this header:

Read More