Skip to main content

Structs

R65 structs group named fields into a single composite type, designed for minimal overhead and predictable memory layout on the 65816.

For a struct with a single positional field — struct TileId(u8); — see Newtypes; that form is a scalar rather than an aggregate.

Definition​

Structs group named fields into a single composite type. All fields are packed in declaration order with no padding or alignment.

struct Player {
x: u8,
y: u8,
health: u16,
sprite_id: u8
}

Fields must be named. A struct body of positional fields is only valid with exactly one field, which makes it a newtype rather than an aggregate:

struct TileId(u8);        // OK -- this is a newtype, not a struct

struct Point(u8, u16);
// error: a newtype wraps exactly one field
// hint: give each field a name: 'struct Point { field0: u8, field1: u16 }'

Memory Layout​

Structs use a packed layout: fields are stored contiguously in declaration order with no padding bytes. The total size is the sum of all field sizes.

struct Player { x: u8, y: u8, health: u16 }
// Offset 0: x (1 byte)
// Offset 1: y (1 byte)
// Offset 2: health (2 bytes, little-endian)
// Total: 4 bytes

There are no alignment requirements. A u16 field at an odd offset is valid and incurs no penalty on the 65816 (which has no alignment constraints).

Methods and Impl Blocks​

Structs can have methods defined in impl blocks. Methods receive *self (a pointer to the struct instance) as their first parameter:

struct Player { x: u8, y: u8, health: u16 }

impl Player {
fn get_x(*self) -> u8 {
return self.x;
}

fn take_damage(*self, amount @ A: u8) {
self.health = self.health - amount as u16;
}
}

Call methods with dot notation on a struct variable or pointer:

#[zeropage]
static mut PLAYER: Player;

PLAYER.take_damage(5); // Compiler passes &PLAYER as self
let x = PLAYER.get_x();

A method's self pointer drives whether it is near or far — write *self for a near method, or far *self for one that operates on far pointers. There is no impl-level qualifier:

impl Player {
fn update(far *self) { // far self
self.health = self.health - 1;
}
}

Under the hood, methods are mangled to free functions (e.g., Player__take_damage) with self as the first stack-passed argument.

Associated Constants​

impl blocks can also define associated constants, accessed with :: syntax:

impl Player {
const MAX_HEALTH: u8 = 100;
}

if PLAYER.health > Player::MAX_HEALTH {
PLAYER.health = Player::MAX_HEALTH;
}

Associated Functions​

A method that declares no self parameter is an associated function, called on the type:

impl Player {
fn spawn_hp() -> u8 { return 100; }
}

let hp: u8 = Player::spawn_hp();

No receiver is passed and none is synthesized, so it compiles to an ordinary free-function call. See Traits -- Associated Functions.

Method Macros​

impl blocks can contain macro_rules! definitions that act as scoped method macros. Inside the macro body, self refers to the receiver the macro is called on:

impl Console {
macro_rules! print($fmt:literal, $($args:expr),*) {
format!(__console_fmt_buf, $fmt, $($args),*);
self.print(&__console_fmt_buf as far *u8);
}

macro_rules! println($fmt:literal, $($args:expr),*) {
format!(__console_fmt_buf, $fmt, $($args),*);
self.println(&__console_fmt_buf as far *u8);
}
}

Invoke method macros with receiver.name!(args):

my_console.print!("Score: {u16}", score);
my_console.println!("Level: {u8}", level);

During expansion, self in the macro body is replaced with the receiver expression (my_console in this case), then the body is expanded as a regular macro.

An impl macro whose body does not name self can be invoked on the type instead, as Type::name!(args):

impl Color {
macro_rules! rgb($r:expr, $g:expr, $b:expr) {
{ Color(((($b) & 0x1F) << 10) | ((($g) & 0x1F) << 5) | (($r) & 0x1F)) }
}
}

let red = Color::rgb!(31, 0, 0);

See Macros for more details.

Trait implementations also use impl blocks — see Traits.

Declaration​

Structs can be declared as static variables with a storage attribute:

#[ram]
static mut PLAYER: Player;

#[zeropage]
static mut ACTIVE: Player;

Or as local variables:

let p = Player { x: 10, y: 20, health: 100 };

Field Access​

Fields are accessed with dot notation:

PLAYER.x = 10;
PLAYER.y = 20;
let hp: u16 = PLAYER.health;

Auto-Dereference Through Pointers​

Pointer-to-struct supports direct field access using . notation. No explicit dereference or -> operator is needed:

#[zeropage]
static mut PTR: *Player;

PTR.x = 10; // Equivalent to (*PTR).x = 10
let hp = PTR.health; // Equivalent to (*PTR).health

This applies to both near and far pointers:

#[zeropage]
static mut FAR_PTR: far *Player;

FAR_PTR.health = 100; // Auto-dereference through far pointer

Pass-by-Pointer Only​

Structs cannot be passed by value to functions, returned by value from functions, or directly assigned from one variable to another. This is a deliberate restriction: copying multi-byte structures is expensive on the 65816 and the cost should be explicit.

// ERROR: Cannot pass struct by value
fn bad(player: Player) { }

// ERROR: Cannot return struct by value
fn bad_return() -> Player { }

// ERROR: Cannot assign struct by value with a bare '='
PLAYER1 = PLAYER2;
// type error: Cannot assign 'Player' by value
// structs and arrays are not copied by a bare '='
// Suggestion: use `dst = src.clone()` or `dst.clone_from(&src)`

Use pointers instead:

// Pass by pointer
fn process_player(player: *Player) {
player.health = player.health - 1;
}

// Initialize through pointer or field-by-field
fn init_player(dest: *Player) {
dest.x = 0;
dest.y = 0;
dest.health = 100;
}

// Copy the whole struct with Clone
PLAYER1 = PLAYER2.clone();
PLAYER1.clone_from(&PLAYER2);

// Or copy field-by-field
PLAYER1.x = PLAYER2.x;
PLAYER1.y = PLAYER2.y;
PLAYER1.health = PLAYER2.health;

.clone() requires an impl Clone for Player {} -- an empty body is enough, and the compiler generates the bitwise copy. See Operator Overloading.

Struct Literal Initialization​

Structs can be initialized with a struct literal expression:

let p = Player { x: 10, y: 20, health: 100 };

All fields must be specified. There is no default initialization or partial initialization syntax.

Nested Structs​

Structs can contain other structs as fields:

struct Vec2 { x: u8, y: u8 }

struct Entity {
pos: Vec2,
health: u8
}

The inner struct is stored inline (packed). Nested field access uses chained dot notation:

ENTITY.pos.x = 10;

TypeId Insertion​

When a struct implements any trait, the compiler automatically inserts a hidden __type_id: u8 field at offset 0, shifting all declared fields by one byte. See Traits for details.

This applies to structs only. A newtype that implements a trait gets no TypeId byte -- every byte is payload, which is why a newtype cannot be a *dyn target. A union cannot implement a trait at all, for the same reason: offset 0 is field data.

struct Player { x: u8, y: u8 }
impl Drawable for Player { /* ... */ }

// Actual layout: [__type_id(1), x(1), y(1)] = 3 bytes

Type Aliases​

The type keyword creates an alias for an existing type. Aliases are fully transparent to the type checker.

type Word = u16;
type Callback = fn(u8) -> u8;
type SpriteTable = [u8; 512];

Type aliases can be used anywhere a type is expected:

type Health = u16;

struct Player {
x: u8,
y: u8,
health: Health // Same as u16
}

fn heal(amount @ A: Health) -> Health {
return A + 10;
}

Size Reference​

TypeSize
u8, i8, bool1 byte
u16, i162 bytes
*T (near pointer)2 bytes
far *T (far pointer)3 bytes
Enum (all values ≤ 255)1 byte
Enum (any value > 255)2 bytes
StructSum of field sizes
Array [T; N]N * sizeof(T)