๐ŸŽ“ A1Learner
๐Ÿ”

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?