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:
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 valuesigner_seeds!(field): expands asigner_seeds!field into an_OFFoffset to the struct, anN_SEEDScount, and per-seed_ADDR_OFFand_LEN_OFFconstants (requires#[frame(Type)], see below)pubkey!(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 fieldunaligned_offset!(field): likeoffset!in frame-relative mode but without the alignment assertion, suffixed with_UOFF(requires#[frame(Type)])unaligned_pubkey_offsets!(field): likepubkey_offsets!in frame-relative mode but without the alignment assertion, suffixed with_UOFF(requires#[frame(Type)])sol_instruction!(field): emits an aligned_OFFfor theSolInstructionstruct base and unaligned_UOFFoffsets for each field (program_id,accounts,account_len,data,data_len) (requires#[frame(Type)])cpi_accounts!(field): emits anN_ACCOUNTScount,_SOL_ACCT_INFO_OFFand_SOL_ACCT_META_OFFvector start offsets, and per-account unaligned offsets for eachSolAccountInfoandSolAccountMetafield (requires#[frame(Type)], field type must be defined withcpi_accounts!)relative_offset!(Struct, from_field, to_field): computes the difference between two field offsets within the same struct, emitted as ani32immediate with_REL_OFF_IMMsuffix. In#[frame(Type)]context the struct is inferred and only the two field paths are required
Frame-relative offsets
When annotated with #[frame(Type)], the group enters frame-relative mode. In this mode, offset!(field) computes a negative offset from the frame pointer (offset_of minus size_of) and asserts 8-byte alignment (BPF_ALIGN_OF_U128). The group's doc comment defaults to the frame struct's doc comment if not explicitly provided. The signer_seeds!(field) and pubkey_offsets!(field) forms are only available in frame-relative mode.
In frame-relative mode, generated constant names include a _FM_ infix after the prefix (e.g. RM_FM_PDA_OFF instead of RM_PDA_OFF) to distinguish frame-relative constants from other offset constants.
TIP
For frame structs, #[frame("mod")] with field attributes can generate the constant group directly, making a separate constant_group! invocation unnecessary. The constant_group! macro remains available for non-frame groups (e.g. input buffer offsets, standalone immediates).
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")]
Same as 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.
#[instruction_data("target")]
Attribute macro for instruction data structs. Automatically generates an LEN associated constant (u64) from size_of::<Self>(), and a hidden module with a _LEN 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 length is accessible in Rust as RegisterMarketData::LEN.
#[instruction_accounts("target")]
Attribute macro for instruction accounts enums. Generates a LEN associated constant (u64) from the number of enum variants, plus a per-variant _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 RegisterMarketAccounts::LEN.
#[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 (4096 bytes). 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 called as #[frame("module_name")] with #[inject("target")] and #[prefix("PREFIX")] on the struct, it also generates a 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 (Address becomes SIZE_OF_ADDRESS). The value is std::mem::size_of::<Type>() cast to i32.
Interface
The interface crate uses the macros to declare all program constants. The 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 wipes all existing .equ directives and injects the generated ones above the first label. 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:
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 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.