Advanced Anchor
Sometimes, some abstraction done with Anchor makes it impossible to build out the logic that our program needs. For this reason, in this section we're going to talk about how to use some advanced concept to work with out program.
Feature Flags
Software engineers routinely need distinct environments for local development, testing, and production. Feature flags provide an elegant solution by enabling conditional compilation and environment-specific configurations without maintaining separate codebases.
Cargo Features
Cargo features offer a powerful mechanism for conditional compilation and optional dependencies. You define named features in your Cargo.toml
's [features]
table, then enable or disable them as needed:
- Enable features via command line: --features feature_name
- Enable features for dependencies directly in Cargo.toml
This gives you fine-grained control over what gets included in your final binary.
Feature Flags in Anchor
Anchor programs commonly use feature flags to create different behaviors, constraints, or configurations based on the target environment. Use the cfg
attribute to conditionally compile code:
#[cfg(feature = "testing")]
fn function_for_testing() {
// Compiled only when "testing" feature is enabled
}
#[cfg(not(feature = "testing"))]
fn function_for_production() {
// Compiled only when "testing" feature is disabled
}
Feature flags excel at handling environment differences. Since not all tokens deploy across Mainnet and Devnet, you often need different addresses for different networks:
#[cfg(feature = "localnet")]
pub const TOKEN_ADDRESS: &str = "Local_Token_Address_Here";
#[cfg(not(feature = "localnet"))]
pub const TOKEN_ADDRESS: &str = "Mainnet_Token_Address_Here";
This approach eliminates deployment configuration errors and streamlines your development workflow by switching environments at compile time rather than runtime.
This is how you would set it up in your Cargo.toml
file:
[features]
default = ["localnet"]
localnet = []
After that, you can specify the flag you want to build your program with like this:
# Uses default (localnet)
anchor build
# Build for mainnet
anchor build --no-default-features
Working with Raw Accounts
Anchor streamlines account handling by automatically deserializing accounts through the account context:
#[derive(Accounts)]
pub struct Instruction<'info> {
pub account: Account<'info, MyAccount>,
}
#[account]
pub struct MyAccount {
pub data: u8,
}
However, this automatic deserialization becomes problematic when you need conditional account processing, such as deserializing and modifying an account only when specific criteria are met.
For conditional scenarios, use UncheckedAccount
to defer validation and deserialization until runtime. This prevents hard errors when accounts might not exist or when you need to validate them programmatically:
#[derive(Accounts)]
pub struct Instruction<'info> {
/// CHECK: Validated conditionally in instruction logic
pub account: UncheckedAccount<'info>,
}
#[account]
pub struct MyAccount {
pub data: u8,
}
pub fn instruction(ctx: Context<Instruction>, should_process: bool) -> Result<()> {
if should_process {
// Deserialize the account data
let mut account = MyAccount::try_deserialize(&mut &ctx.accounts.account.to_account_info().data.borrow_mut()[..])
.map_err(|_| error!(InstructionError::AccountNotFound))?;
// Modify the account data
account.data += 1;
// Serialize back the data to the account
account.try_serialize(&mut &mut ctx.accounts.account.to_account_info().data.borrow_mut()[..])?;
}
Ok(())
}
Zero Copy Accounts
Solana's runtime enforces strict memory limits: 4KB for stack memory and 32KB for heap memory. Additionally, the stack grows by 10KB for each loaded account. These constraints make traditional deserialization impossible for large accounts, requiring zero-copy techniques for efficient memory management.
When accounts exceed these limits, you'll encounter stack overflow errors like: Stack offset of -30728 exceeded max offset of -4096 by 26632 bytes
For medium-sized accounts, you can use Box
to move data from stack to heap (as mentioned in the introduction), but larger accounts require implementing zero_copy
.
Zero-copy bypasses automatic deserialization entirely by using raw memory access. To define an account type that uses zero-copy, annotate the struct with #[account(zero_copy)]
:
#[account(zero_copy)]
pub struct Data {
// 10240 bytes - 8 bytes account discriminator
pub data: [u8; 10_232],
}
The #[account(zero_copy)]
attribute automatically implements several traits required for zero-copy deserialization:
#[derive(Copy, Clone)]
,#[derive(bytemuck::Zeroable)]
,#[derive(bytemuck::Pod)]
,#[repr(C)]
To deserialize a zero-copy account in your instruction context, use AccountLoader<'info, T>
, where T
is your zero-copy account type:
#[derive(Accounts)]
pub struct Instruction<'info> {
pub data_account: AccountLoader<'info, Data>,
}
Initializing a Zero Copy Account
There are two different approaches for initialization, depending on your account size:
For accounts under 10,240 bytes, use the init
constraint directly:
#[derive(Accounts)]
pub struct Initialize<'info> {
#[account(
init,
// 10240 bytes is max space to allocate with init constraint
space = 8 + 10_232,
payer = payer,
)]
pub data_account: AccountLoader<'info, Data>,
#[account(mut)]
pub payer: Signer<'info>,
pub system_program: Program<'info, System>,
}
For accounts requiring more than 10,240 bytes, you must first create the account separately by calling the System Program multiple times, adding 10,240 bytes per transaction. This allows you to create accounts up to Solana's maximum size of 10MB (10,485,760 bytes), bypassing the CPI limitation.
After creating the account externally, use the zero
constraint instead of init
. The zero
constraint verifies the account hasn't been initialized by checking that its discriminator is unset:
#[account(zero_copy)]
pub struct Data {
// 10,485,780 bytes - 8 bytes account discriminator
pub data: [u8; 10_485_752],
}
#[derive(Accounts)]
pub struct Initialize<'info> {
#[account(zero)]
pub data_account: AccountLoader<'info, Data>,
}
For both initialization methods, call load_init()
to get a mutable reference to the account data and set the account discriminator:
pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
let account = &mut ctx.accounts.data_account.load_init()?;
// Set your account data here
// account.data = something;
Ok(())
}
Loading a Zero Copy Account
Once initialized, use load()
to read the account data:
#[derive(Accounts)]
pub struct ReadOnly<'info> {
pub data_account: AccountLoader<'info, Data>,
}
pub fn read_only(ctx: Context<ReadOnly>) -> Result<()> {
let account = &ctx.accounts.data_account.load()?;
// Read your data here
// let value = account.data;
Ok(())
}
Working with Raw CPIs
Anchor abstracts cross-program invocation (CPI) complexity, but understanding the underlying mechanics is crucial for advanced Solana development.
Every instruction consists of three core components: a program_id
, an accounts
array, and instruction_data
bytes that the Solana Runtime processes via the sol_invoke
syscall.
At the system level, Solana executes CPIs through this syscall:
/// Solana BPF syscall for invoking a signed instruction.
fn sol_invoke_signed_c(
instruction_addr: *const u8,
account_infos_addr: *const u8,
account_infos_len: u64,
signers_seeds_addr: *const u8,
signers_seeds_len: u64,
) -> u64;
The Runtime receives pointers to your instruction data and account information, then executes the target program with these inputs.
Here's how you'd invoke an Anchor program using raw Solana primitives:
pub fn manual_cpi(ctx: Context<MyCpiContext>) -> Result<()> {
// Construct instruction discriminator (8-byte SHA256 hash prefix)
let discriminator = sha256("global:instruction_name")[0..8]
// Build complete instruction data
let mut instruction_data = discriminator.to_vec();
instruction_data.extend_from_slice(&[additional_instruction_data]); // Your instruction parameters
// Define account metadata for the target program
let accounts = vec![
AccountMeta::new(ctx.accounts.account_1.key(), true), // Signer + writable
AccountMeta::new_readonly(ctx.accounts.account_2.key(), false), // Read-only
AccountMeta::new(ctx.accounts.account_3.key(), false), // Writable
];
// Collect account infos for the syscall
let account_infos = vec![
ctx.accounts.account_1.to_account_info(),
ctx.accounts.account_2.to_account_info(),
ctx.accounts.account_3.to_account_info(),
];
// Create the instruction
let instruction = solana_program::instruction::Instruction {
program_id: target_program::ID,
accounts,
data: instruction_data,
};
// Execute the CPI
solana_program::program::invoke(&instruction, &account_infos)?;
Ok(())
}
// For PDA-signed CPIs, use invoke_signed instead:
pub fn pda_signed_cpi(ctx: Context<PdaCpiContext>) -> Result<()> {
// ... instruction construction same as above ...
let signer_seeds = &[
b"seed",
&[bump],
];
solana_program::program::invoke_signed(
&instruction,
&account_infos,
&[signer_seeds],
)?;
Ok(())
}
CPI to another Anchor Program
Anchor's declare_program!()
macro enables type-safe cross-program invocations without adding the target program as a dependency. The macro generates Rust modules from a program's IDL, providing CPI helpers and account types for seamless program interactions.
Place the target program's IDL file in an /idls
directory anywhere in your project structure:
project/
├── idls/
│ └── target_program.json
├── programs/
│ └── your_program/
└── Cargo.toml
Then use the macro to generate the necessary modules:
use anchor_lang::prelude::*;
declare_id!("YourProgramID");
// Generate modules from IDL
declare_program!(target_program);
// Import the generated types
use target_program::{
accounts::Counter, // Account types
cpi::{self, accounts::*}, // CPI functions and account structs
program::TargetProgram, // Program type for validation
};
#[program]
pub mod your_program {
use super::*;
pub fn call_other_program(ctx: Context<CallOtherProgram>) -> Result<()> {
// Create CPI context
let cpi_ctx = CpiContext::new(
ctx.accounts.target_program.to_account_info(),
Initialize {
payer: ctx.accounts.payer.to_account_info(),
counter: ctx.accounts.counter.to_account_info(),
system_program: ctx.accounts.system_program.to_account_info(),
},
);
// Execute the CPI using generated helper
target_program::cpi::initialize(cpi_ctx)?;
Ok(())
}
}
#[derive(Accounts)]
pub struct CallOtherProgram<'info> {
#[account(mut)]
pub payer: Signer<'info>,
#[account(mut)]
pub counter: Account<'info, Counter>, // Uses generated account type
pub target_program: Program<'info, TargetProgram>,
pub system_program: Program<'info, System>,
}