Appearance
Build Scaffolding
Assembly constants (instruction discriminants, error codes, offsets, etc.) are defined in Rust in the interface crate and injected into assembly files at build time. This keeps the assembly source in sync with Rust type layouts and avoids hardcoded magic numbers.
Overview
Dropset build scaffolding has multiple layers:
macroscrate: proc macros that turn Rust enums and constant definitions intodropset_build::Constantmetadata.interfacecrate: declares the program's constants using those macros. Injection sites are specified via#[inject("file")], where the target names an assembly file (e.g.#[inject("entrypoint")]targetsprogram/src/dropset/entrypoint.s).buildcrate: reads the constant metadata and writes.equdirectives into assembly injection sites.
The workspace-root build.rs invokes the injection:
Core types are as follows:
Macros
The macros crate provides several proc macros. Attribute macros (#[frame], #[svm_data], #[instruction_data], etc.) are used when the macro annotates a natural Rust item (struct, enum). Function-like macros (constant_group!, size_of_group!) are used when the body has custom syntax that doesn't map to a standard Rust item.
constant_group!
Defines a group of named assembly constants with an injection target. The #[inject("file")] attribute specifies which assembly file receives the constants. An optional #[prefix("...")] attribute prepends a prefix to all generated constant names. An optional /// doc comment on the group itself adds a header comment and separator lines around the group in the output assembly file. Each constant is assigned a value using one of the following syntax forms (parsed within the proc macro, not standalone macros):
offset!(expr): ani16memory offset, the generated name is suffixed with_OFFimmediate!(expr): ani32immediate valuepubkey!(expr): splits a 32-byte pubkey into four 8-byte chunks, emitting full 64-bit_CHUNK_{0..3}i64constants (forlddw) plus_CHUNK_{0..3}_LOand_CHUNK_{0..3}_HIi32immediates (twelve constants total)pubkey_offsets!(expr): emits a base_OFFoffset plus four_CHUNK_{0..3}_OFFoffsets for each 8-byte chunk of a 32-byte pubkey field. The generated constant name should mirror the struct field name: useADDRESSwhen the field is namedaddress(e.g.RuntimeAccount.address), andPUBKEYor the field name otherwise, since "address" also means a runtime pointer in this codebaserelative_offset!(Struct, from_field, to_field): computes the difference between two field offsets within the same struct, emitted as ani32immediate with_REL_OFF_IMMsuffix
Frame-relative constant kinds (e.g. signer_seeds!, cpi_accounts!, sol_instruction!, unaligned_offset!) are only available via #[frame] field attributes, not directly in constant_group!.
Each group generates:
- A Rust module with public constants (with compile-time range checks)
dropset_build::Constantmetadata entries for build-time injection, with names derived from the constant name (plus prefix if specified).equdirectives injected into the target assembly file, with doc comments carried over as assembly comments. Groups with a doc comment are wrapped in a header and separator lines
#[discriminant_enum("target")]
Re-emits the enum with #[repr(u8)] and explicit discriminant values, numbered from 0. A From<Enum> for u8 impl is generated so variants can be used without explicit casts (e.g. Discriminant::RegisterMarket.into()). A hidden module with DISC_-prefixed assembly constants and a GROUP is generated for build-time injection.
#[error_enum("target")]
Similar to discriminant_enum but with #[repr(u32)], prefixed with E_, starting at 1 (0 is reserved for success). A From<Enum> for u32 impl is generated. Additionally, error-handler label blocks are generated for each variant: a lowercase e_snake_name: label that sets r0 to the corresponding E_ constant and exits. When error labels are present, the build system fully regenerates the target assembly file.
#[instruction_data("target")]
Attribute macro for instruction data structs. Automatically generates a SIZE associated constant (u64) from size_of::<Self>(), and a hidden module with an INSN_DATA_SIZE suffixed assembly constant and GROUP for build-time injection. The target string names the assembly file (e.g. "market/register" targets program/src/dropset/market/register.s).
The size is accessible in Rust as Data::SIZE.
#[instruction_accounts("target")]
Attribute macro for instruction accounts enums. Generates a COUNT associated constant (u64) from the number of enum variants, plus a per-variant INSN_ACCTS_*_POS position constant (i32) for each variant. A hidden module with assembly constants and GROUP is emitted for build-time injection. Assembly comments are auto-generated from the variant names.
The count is accessible in Rust as Accounts::COUNT.
#[frame]
Attribute macro for stack frame structs. Applies #[repr(C, align(8))] (aligned to BPF_ALIGN_OF_U128) and asserts at compile time that the struct fits within one SBPF stack frame. Also registers field-to-type mappings and the struct's doc comment in proc-macro shared state so that constant_group! can auto-discover frame fields and derive its header comment.
When combined with #[inject("target")] and #[prefix("PREFIX")] on the struct, it also generates a frame constant group module directly from field-level attributes, eliminating the need for a separate constant_group! invocation. Supported field attributes:
#[offset]: aligned frame-relative offset (_OFFsuffix). Name is auto-inferred from the field name viaSCREAMING_SNAKE_CASE, or overridden with#[offset(CUSTOM_NAME)]#[unaligned_offset]: frame-relative offset without alignment (_UOFF)#[pubkey_offsets]: base offset + four chunk offsets#[signer_seeds]: auto-expands seed offsets fromsigner_seeds!shared state#[cpi_accounts]: auto-expands CPI account offsets fromcpi_accounts!shared state#[sol_instruction]: base offset + per-fieldSolInstructionoffsets
Sub-field access uses comma-separated form: #[unaligned_offset(NAME, subfield, "doc")].
Struct-level #[relative_offset(NAME, from, to, "doc")] attributes compute the difference between two field offsets.
#[svm_data]
Attribute macro for packed onchain data structs. Applies #[repr(C, packed)] to the struct so its layout matches the SVM memory map exactly. Use this for any struct that maps directly to an onchain memory region (account data, input buffer segments, tree nodes).
signer_seeds!
Function-like macro that defines a #[repr(C)] struct where every field is typed as SolSignerSeed. Field names are registered in proc-macro shared state so that signer_seeds!(field) inside a constant_group!, or an #[signer_seeds] field attribute on a #[frame] struct, can auto-discover all seed fields by looking up the parent field's type.
cpi_accounts!
Function-like macro that defines a #[repr(C)] struct with SolAccountInfo fields first (contiguous), then SolAccountMeta fields (contiguous), for each named account. Field names are registered in proc-macro shared state so that cpi_accounts!(field) inside a constant_group!, or a #[cpi_accounts] field attribute on a #[frame] struct, can auto-discover all account fields by looking up the parent field's type.
size_of_group!
Injects SIZE_OF_<TYPE> immediates for each listed type. Names and doc comments are auto-derived from the type name (Pubkey becomes SIZE_OF_PUBKEY). The value is std::mem::size_of::<Type>() cast to i32. Note that Pubkey is a local alias for pinocchio::Address (see Pubkeys for the naming convention).
Interface
The interface crate uses the macros to declare program constants, data types, and instruction definitions. These inject into the assembly files described in the program layout.
txt
interface/src/
├── lib.rs # Module declarations and re-exports
├── groups.rs # Injection groups registry
├── entrypoint.rs # Discriminants, entrypoint, input buffer
├── error.rs # ErrorCode enum
├── common/
│ ├── account.rs # Runtime account layout and CPI constants
│ ├── cpi_bindings.rs # Auto-generated Sol* C structs
│ ├── memory.rs # Data primitives and type sizes
│ ├── pubkey.rs # Pubkey chunk offsets, well-known program IDs
│ └── token.rs # SPL Token constants
├── market/
│ ├── mod.rs # MarketHeader and market-level constants
│ └── register.rs # REGISTER-MARKET instruction types, frame, constants
├── order/mod.rs # Order data structure
├── seat/mod.rs # Seat data structure
└── stack/mod.rs # StackNode data structureThe INJECTION_GROUPS slice collects every constant group for the build script.
Build crate
The build crate has two responsibilities: assembly constant injection and CPI bindings generation.
Assembly injection
The inject() function writes .equ directives into assembly files. For each target file, it replaces everything above the first label with the generated directives. Doc comments from the Rust source become assembly comments. Groups that carry a doc comment are rendered with a header comment and separator lines; groups without a doc comment are separated by a blank line.
For example, entrypoint.s receives only .equ directives:
When a group contains error labels (from #[error_enum]), the entire target file is regenerated with both .equ directives and error-handler label blocks. For example, error.s receives both:
CPI bindings
The generate_bindings() function fetches Solana CPI C headers from the Agave repository on GitHub, runs bindgen to produce Rust FFI structs, and replaces SolPubkey references with Pubkey (a local alias for pinocchio::Address). The output is written to interface/src/cpi_bindings.rs and formatted with rustfmt.
Bindings generation only runs when the AGAVE_REV environment variable is set. Locally, cargo check and make asm skip it entirely. On CI, the bindings workflow sets AGAVE_REV (along with AGAVE_RAW_BASE and AGAVE_RAW_PATH) and verifies the committed file is up to date.
To update the bindings for a new Agave version, change the AGAVE_REV value in .github/workflows/bindings.yml, regenerate locally with the three environment variables set, and commit the updated cpi_bindings.rs.