Rust FFI Repr in Depth
A repr is an attribute that names the layout a type must use. Repr choices decide whether C and Rust agree about the bytes at the border.
A fieldless enum, C-sized
No fields, no surprises. The whole enum is one C int here.
#[repr(C)]
enum Color { Red = 1, Green = 2, Blue = 3 }Pin the tag size yourself
repr(u8) squeezes the tag into one byte. A C enum would rarely promise that.
#[repr(u8)]
enum Small { A, B }
println!("small = {}", size_of::<Small>());A newtype with no costume
repr(transparent) makes the wrapper vanish. The wrapper is its u64, byte for byte.
#[repr(transparent)]
struct Wrapper(u64);
println!("wrapper = {}", size_of::<Wrapper>());An enum carrying data
The tag needs room. The u32 needs room. Together they measure 8 bytes here.
#[repr(C)]
enum Tagged {
Empty,
Number(u32),
}
println!("tagged = {}", size_of::<Tagged>());Cross with a number C knows
The C function takes its own Color enum. The Rust twin has the same shape, so the value walks straight in.
let code = unsafe { color_code(Color::Blue) };
println!("code for blue = {code}");Example
#![allow(dead_code)]
use std::ffi::c_int;
use std::mem::size_of;
#[repr(C)]
#[derive(Clone, Copy)]
enum Color {
Red = 1,
Green = 2,
Blue = 3,
}
#[repr(u8)]
enum Small {
A,
B,
}
#[repr(transparent)]
struct Wrapper(u64);
#[repr(C)]
enum Tagged {
Empty,
Number(u32),
}
unsafe extern "C" {
fn color_code(c: Color) -> c_int;
}
fn main() {
println!("repr(C) enum = {} bytes", size_of::<Color>());
println!("repr(u8) enum = {} byte", size_of::<Small>());
println!("transparent wrapper = {} bytes", size_of::<Wrapper>());
println!("u64 inside it = {} bytes", size_of::<u64>());
println!("repr(C) tagged = {} bytes", size_of::<Tagged>());
let code = unsafe { color_code(Color::Blue) };
println!("code for blue = {code}");
}Key points
- A fieldless #[repr(C)] enum matches a C enum.
- On this machine both measure 4 bytes, the size of a C int
- Give variants explicit values when C knows them by number.
- Red = 1 crosses the border as 1
- Use #[repr(u8)] or #[repr(i32)] on a fieldless enum when the tag must be an exact size.
- A C enum picks its own size.
- Rust lets you pin it
- #[repr(transparent)] on a one-field struct promises the exact layout of the field inside.
- A newtype over a pointer can cross anywhere the pointer can
- An enum with data under repr(C) has a defined layout.
- It holds a tag plus room for the largest variant.
- But C has no name for that combined type
- A repr choice pins one layout, so both sides read the same bytes.
- A repr(C) enum with data looks like a C union with a tag in front.
- Looking similar is not being the same.
In this example, four types go on the scale. Then one value crosses the border. The fieldless repr(C) enum measures 4 bytes. A real C program measures its own enum as 4 bytes too. The repr(u8) enum pins its tag to a single byte. The transparent wrapper measures exactly the u64 inside it. The tagged enum needs 8 bytes: room for the tag, plus room for its u32. At the end, Color::Blue walks into a real C function. The function multiplies its own enum value by ten. It answers 30. The numbers agree because each repr was chosen on purpose.
Note
Design the shared shape in C terms first: a struct plus a separate tag
Try it Yourself
Edit the code, then press Run.
Running is simulated ยท Rust Playground
Exercise
You must share an enum with C. The C twin is a plain enum of named values. Which repr fits?