Type Mapping
Mapping between Rust types, zero-copy Pod representations, and IDL/TypeScript types.
Quasar reads account data directly from the runtime's memory buffer without deserialization. Native Rust integer types are mapped to alignment-1 Pod types that store values as little-endian byte arrays.
Rust to Pod type mapping
When you define an #[account] struct, the derive macro generates a #[repr(C)] companion struct where each field is mapped to its zero-copy equivalent:
| Rust Type | Zero-Copy Type | Size (bytes) | Alignment |
|---|---|---|---|
u8 | u8 (no wrapper) | 1 | 1 |
i8 | i8 (no wrapper) | 1 | 1 |
u16 | PodU16 | 2 | 1 |
i16 | PodI16 | 2 | 1 |
u32 | PodU32 | 4 | 1 |
i32 | PodI32 | 4 | 1 |
u64 | PodU64 | 8 | 1 |
i64 | PodI64 | 8 | 1 |
u128 | PodU128 | 16 | 1 |
i128 | PodI128 | 16 | 1 |
bool | PodBool | 1 | 1 |
Address | Address (no wrapper) | 32 | 1 |
All Pod types have alignment 1. The Solana runtime provides account data in a flat byte buffer with no alignment guarantees, so all types in #[repr(C)] account structs must have alignment 1.
Given this account definition:
#[account(discriminator = 1)]
pub struct Vault {
pub authority: Address,
pub balance: u64,
pub is_active: bool,
}The derive macro generates this zero-copy companion:
#[repr(C)]
#[derive(Copy, Clone)]
pub struct VaultZc {
pub authority: Address, // 32 bytes
pub balance: PodU64, // 8 bytes
pub is_active: PodBool, // 1 byte
}Alignment is verified at compile time:
const _: () = assert!(core::mem::align_of::<VaultZc>() == 1);Pod Types
Pod types are defined in quasar_pod and re-exported via quasar_lang::prelude. Each is #[repr(transparent)] over [u8; N], storing values in little-endian byte order.
Reading values
Use .get() to extract the native Rust value:
let balance: u64 = vault.balance.get();
let is_active: bool = vault.is_active.get();
let slot: u64 = clock.slot.get();Writing values
Assign native values directly. The #[account] macro generates setters that handle the conversion:
// Direct field access via DerefMut
vault.balance = PodU64::from(1000u64);
vault.is_active = PodBool::from(true);Arithmetic
Pod types implement +, -, *, /, % with both Pod and native types:
let a = PodU64::from(100u64);
let b = a + 50u64; // PodU64 + u64 -> PodU64
let c = a + PodU64::from(50); // PodU64 + PodU64 -> PodU64
let d = a * 2u64; // PodU64 * u64 -> PodU64Overflow behavior:
- Debug builds: Panics on overflow (via
checked_*operations) - Release builds: Uses wrapping semantics for CU efficiency
For explicit overflow detection in all builds:
| Method | Description |
|---|---|
checked_add(rhs) | Returns None on overflow |
checked_sub(rhs) | Returns None on underflow |
checked_mul(rhs) | Returns None on overflow |
checked_div(rhs) | Returns None if rhs is zero |
saturating_add(rhs) | Clamps at numeric bounds |
saturating_sub(rhs) | Clamps at zero (unsigned) or bounds (signed) |
saturating_mul(rhs) | Clamps at numeric bounds |
let balance = vault.balance;
let new_balance = balance.checked_add(deposit_amount)
.ok_or(ProgramError::ArithmeticOverflow)?;Constants
| Constant | Description |
|---|---|
ZERO | The zero value |
MAX | The largest representable value |
MIN | The smallest representable value |
if vault.balance == PodU64::ZERO {
return Err(MyError::EmptyVault.into());
}Comparisons
Pod types implement PartialEq, Eq, PartialOrd, and Ord. Compare directly with native types:
if vault.balance > 100u64 {
// ...
}
if vault.balance == PodU64::ZERO {
// ...
}PodBool
Stores a boolean as a single byte. Any non-zero byte is true (canonical form is 0x01).
let active: bool = vault.is_active.get(); // read
vault.is_active = PodBool::from(true); // write
let toggled = !vault.is_active; // bitwise NOTBitwise operations
Pod integer types support:
| Operator | Description |
|---|---|
& | Bitwise AND |
| | Bitwise OR |
^ | Bitwise XOR |
! | Bitwise NOT |
<< | Left shift |
>> | Right shift |
Discriminator
Every account and instruction type has a discriminator prefix -- a byte sequence at the start of account data that identifies the type.
Account discriminators
Set via #[account]:
#[account(discriminator = 1)]
pub struct Vault { ... }
#[account(discriminator = [1, 2])]
pub struct Escrow { ... }Rules:
- At least one byte is required
- At least one byte must be non-zero (all-zero discriminators are rejected at compile time, since they match uninitialized data)
- Single values use a one-byte discriminator; arrays support multi-byte discriminators
Instruction discriminators
Set via #[instruction]:
#[instruction(discriminator = 0)]
pub fn initialize(ctx: Ctx<Initialize>) -> Result<(), ProgramError> { ... }
#[instruction(discriminator = 1)]
pub fn deposit(ctx: Ctx<Deposit>) -> Result<(), ProgramError> { ... }Instruction discriminators can be zero. The quasar new instruction command auto-increments from the highest existing discriminator.
Calculating account space
Total account space:
space = discriminator_size + fixed_fields_size + dynamic_prefix_bytes + dynamic_data_bytesFixed-size accounts
For accounts with only fixed-width fields, Space provides a compile-time constant:
#[account(discriminator = 1)]
pub struct Vault {
pub authority: Address, // 32 bytes
pub balance: u64, // 8 bytes
pub is_active: bool, // 1 byte
}
// Vault::SPACE = 1 (disc) + 32 + 8 + 1 = 42 bytesThe formula is SPACE = discriminator_bytes + sizeof(ZcCompanion), where sizeof(ZcCompanion) is the sum of all field sizes after Pod mapping:
| Field | Pod Type | Size |
|---|---|---|
authority: Address | Address | 32 |
balance: u64 | PodU64 | 8 |
is_active: bool | PodBool | 1 |
| Total fields | 41 | |
| + discriminator (1 byte) | 42 |
Dynamic accounts
For accounts with String or Vec fields, space is variable. The macro provides MIN_SPACE and MAX_SPACE:
#[account(discriminator = 5)]
pub struct Profile<'a> {
pub owner: Address, // 32 bytes (fixed)
pub name: String<u16, 64>, // 2 prefix + 0..64 data
pub tags: Vec<Address, u8, 10>, // 1 prefix + 0..320 data
}
// Profile::MIN_SPACE = 1 (disc) + 32 (fixed) + 2 + 1 = 36 bytes
// Profile::MAX_SPACE = 36 + 64 + 320 = 420 bytesDynamic field layout:
| Field Type | Prefix | Data Bytes |
|---|---|---|
String | u32 (4 bytes) | 0..1024 |
String<u16, N> | u16 (2 bytes) | 0..N |
String<u8, N> | u8 (1 byte) | 0..N |
Vec<T> | u32 (4 bytes) | 0..8 * sizeof(T) |
Vec<T, u16, N> | u16 (2 bytes) | 0..N * sizeof(T) |
Vec<T, u8, N> | u8 (1 byte) | 0..N * sizeof(T) |
&str (tail) | none | remaining bytes |
&[u8] (tail) | none | remaining bytes |
String and Vec are marker types that configure the wire format:
// String<P, MAX> where P = prefix type, MAX = max byte length
pub name: String, // String<u32, 1024> (defaults)
pub tag: String<u8, 32>, // u8 prefix, max 32 bytes
pub bio: String<u16, 4096>, // u16 prefix, max 4096 bytes
// Vec<T, P, MAX> where T = element, P = prefix type, MAX = max count
pub members: Vec<Address>, // Vec<Address, u32, 8> (defaults)
pub scores: Vec<PodU64, u8, 4>, // u8 prefix, max 4 elementsTail fields
A tail field (&str or &[u8]) consumes all remaining bytes with no length prefix. Must be the last dynamic field. Maximum size is 1024 bytes.
#[account(discriminator = 5)]
pub struct Note<'a> {
pub author: Address,
pub content: &'a str, // tail field -- no prefix, uses remaining data
}Space override
Use space = <expr> on init to override computed space:
#[account(init, payer = payer, space = 8 + 32 + 4 + name.len())]
pub profile: &'info mut Account<Profile<'info>>,IDL type mapping
The IDL generator maps Rust types to IDL primitives, which the TypeScript client generator maps to TypeScript types and codecs.
Rust to IDL to TypeScript
| Rust Type | IDL Type | TypeScript Type | Codec (@solana/codecs) |
|---|---|---|---|
u8 | u8 | number | getU8Codec() |
u16 | u16 | number | getU16Codec() |
u32 | u32 | number | getU32Codec() |
u64 | u64 | bigint | getU64Codec() |
u128 | u128 | bigint | getU128Codec() |
i8 | i8 | number | getI8Codec() |
i16 | i16 | number | getI16Codec() |
i32 | i32 | number | getI32Codec() |
i64 | i64 | bigint | getI64Codec() |
i128 | i128 | bigint | getI128Codec() |
bool | bool | boolean | getBooleanCodec() |
Address / Pubkey | publicKey | Address | getAddressCodec() (kit) |
String | string | string | -- |
String<N> | { string: { maxLength: N } } | string | getDynStringCodec() |
Vec<T, N> | { vec: { items: T, maxLength: N } } | Array<T> | getDynVecCodec(T) |
&str (tail) | { tail: { element: "string" } } | string | getUtf8Codec() |
&[u8] (tail) | { tail: { element: "bytes" } } | Uint8Array | getBytesCodec() |
| Custom struct | { defined: "Name" } | Name | NameCodec |
