When C developers first look at Rust types, the obvious move is to find familiar mappings: uint8_t to u8, int32_t to i32, size_t to usize, struct to struct.
That is a good starting point, but similar names are not enough.
In C, basic types, struct layout, alignment, and ABI affect file formats, network packets, register maps, dynamic library interfaces, and cross-language calls. Rust also cares about these issues, but its defaults have a different goal: Rust types first serve internal safety and optimization. If a type has to match C or a binary layout, that boundary must be made explicit.
This article answers one question: how should a C developer read Rust basic types and struct layout?
Fixed-Width Integers Mostly Map Directly
C commonly uses stdint.h to make integer widths explicit:
#include <stdint.h>
uint8_t flags;
int32_t temperature;
uint64_t timestamp;
Rust fixed-width integers are direct:
let flags: u8 = 0;
let temperature: i32 = -120;
let timestamp: u64 = 0;
The usual mapping is:
uint8_t -> u8
uint16_t -> u16
uint32_t -> u32
uint64_t -> u64
int8_t -> i8
int16_t -> i16
int32_t -> i32
int64_t -> i64
There is no need to make this mysterious. For protocol fields, file formats, and hardware register widths, Rust should also use fixed-width integer types.
The care is needed with another group of types: types that look like integers but do not mean “a fixed number of bits.”
usize Is More Like size_t
In C, size_t is used for lengths, sizes, and array indexes. Its width depends on the platform: usually 32 bits on 32-bit targets and 64 bits on 64-bit targets.
Rust’s usize is similar:
let len: usize = data.len();
usize is right for sizes of in-memory objects, slice lengths, and collection indexes. It is not a good default for protocol fields, file fields, or persistent cross-platform data.
If a file header has a 4-byte length field, write:
let payload_len: u32 = 1024;
Not:
let payload_len: usize = 1024;
The reason is the same as in C: the platform-dependent width of usize would leak into your data format. In-memory lengths can be usize; externally stored lengths should use explicit widths.
Rust char Is Not C char
C char is usually a one-byte integer type. It is often used for bytes, ASCII characters, string elements, and small integers.
Rust char does not mean that.
Rust char is a Unicode scalar value, and it is 4 bytes:
let c: char = '中';
If you mean “one byte”, use u8:
let byte: u8 = b'A';
If you mean binary data, you will often use:
let data: Vec<u8> = vec![0x01, 0x02, 0x03];
If you mean text, that is a String and &str topic, not a direct replacement for char *.
The first model is:
C char: often one byte
Rust char: one Unicode scalar value
Byte data: use u8 / [u8]
struct Has the Same Name, Not the Same Default Layout
C developers have an important intuition about struct: fields appear in declaration order, and the compiler inserts padding according to ABI rules.
For example:
#include <stdint.h>
struct Header {
uint8_t version;
uint32_t length;
uint16_t flags;
};
This struct is not simply 1 + 4 + 2 = 7 bytes. It may contain padding to satisfy uint32_t alignment and the struct’s overall alignment.
Rust also has struct:
struct Header {
version: u8,
length: u32,
flags: u16,
}
But there is a key difference: Rust’s default struct layout is repr(Rust), and it does not promise C field order, padding, or ABI. The compiler may choose a layout suitable for Rust’s own optimization rules.
That makes this type fine for Rust-internal use, but it should not be used directly as a C ABI object or as an on-disk or on-wire binary layout.
Use repr(C) at C Boundaries
If the struct crosses a C boundary, make that explicit:
#[repr(C)]
struct Header {
version: u8,
length: u32,
flags: u16,
}
#[repr(C)] means the type uses a C-compatible layout. Field order, alignment, and padding follow C-compatible rules.
This matters for:
- C calling a Rust library
- Rust calling a C library
- structs passed through FFI
- matching a struct from a C header
But do not overread repr(C). It solves layout. It does not mean arbitrary bytes can safely be treated as that struct.
Network packets usually have byte-order rules. Padding bytes may be undefined. Input data may be too short. When parsing binary protocols, it is usually safer to read fields from &[u8] than to cast a byte block into a struct.
Check Layout with size_of and align_of
C uses sizeof to inspect type size. Rust uses:
use std::mem::{align_of, size_of};
#[repr(C)]
struct Header {
version: u8,
length: u32,
flags: u16,
}
fn main() {
println!("size = {}", size_of::<Header>());
println!("align = {}", align_of::<Header>());
}
This helps check your layout intuition, especially when migrating C structs, writing FFI, or debugging binary data.
But size_of only reports the size for the current type on the current target. It is not a cross-platform format design tool. External data formats should define field width, byte order, and alignment rules themselves.
A Type Alias Is Not a New Type
C often uses typedef to name a type:
typedef uint32_t DeviceId;
Rust also has type aliases:
type DeviceId = u32;
But this is only an alias. DeviceId and u32 are still the same type for type checking.
If you want device IDs and ordinary integers to be harder to mix up, wrap the value in a struct:
struct DeviceId(u32);
That creates a distinct type. This becomes important in library API design: Rust can use types to distinguish “just a number” from “a device ID” and prevent accidental argument swaps.
For now, keep the first distinction: type is an alias; struct DeviceId(u32) is a new type.
What to Keep from This Article
C developers can start with this table:
uint8_t / int32_t -> u8 / i32
size_t -> usize
C char byte -> Rust u8
C text pointer -> later String / &str topic
C struct ABI -> Rust #[repr(C)] struct
sizeof -> std::mem::size_of
typedef alias -> type alias
new domain type -> tuple struct / newtype
Rust does care about memory layout. It just does not treat every struct as a C ABI object by default.
Inside Rust, default layout serves Rust’s own safety and optimization needs. At C boundaries, binary formats, protocol fields, and ABI boundaries, developers should explicitly choose fixed-width types, repr(C), byte order, and parsing strategy.
The next step is buffers, slices, and Vec<u8>. That connects directly to one of the most common C patterns: pointer plus length.