In part one, we covered the Solana program input serialization format and how program inputs are laid out in memory. In this part, we’ll cover how programs route incoming program inputs to the appropriate handlers and the supporting code the entrypoint sets up to make that possible.
The Instruction Processor
The entrypoint function (generated by the entrypoint! macro) deserializes the raw byte array into program_id, accounts, and instruction_data, then passes them to the instruction processor. The instruction processor reads the instruction_data to determine which handler should execute—similar to how Solidity contracts route function calls based on the function selector.
The instruction processor must be defined by your program itself. The entrypoint doesn’t provide this function, it only defines the type signature that your instruction processor must match. This type is defined as:
pub type ProcessInstruction = fn(program_id: &Pubkey, accounts: &[AccountInfo], instruction_data: &[u8]) -> ProgramResult;
Your program must create a function matching this signature (takes those three program inputs and returns a ProgramResult), and then pass it to the entrypoint! macro (we’ll see how this works below). How you create this function depends on whether you’re using Anchor or writing a native Rust program.
In native Rust programs, you define a function with this exact signature and pass it to the entrypoint! macro we mentioned earlier. This function handles the instruction routing logic by examining the instruction data to determine which function to call. Our native Rust Solana program series covers how to implement this in detail.
In Anchor programs, the #[program] macro automatically generates the instruction processor for you. Each public function in your #[program] module becomes an instruction handler, and Anchor generates the routing logic based on a discriminator derived from the function name. This is one of the ways Anchor abstracts away the boilerplate and makes program development more convenient.
When the instruction processor returns Ok(()), the entrypoint returns SUCCESS (0). If it returns an error, the entrypoint converts that error into an error code and returns it.
Here’s how everything fits together in the entrypoint! macro:
#[macro_export]
macro_rules! entrypoint {
($process_instruction:ident) => {
/// # Safety
#[no_mangle]
pub unsafe extern "C" fn entrypoint(input: *mut u8) -> u64 {
let (program_id, accounts, instruction_data) = unsafe { $crate::deserialize(input) };
match $process_instruction(program_id, &accounts, instruction_data) {
Ok(()) => $crate::SUCCESS,
Err(error) => error.into(),
}
}
$crate::custom_heap_default!();
$crate::custom_panic_default!();
};
}
The macro generates code that deserializes the input (using the deserialize function we discussed earlier) to extract the three program inputs, passes them to your process_instruction function, and handles the result.
Notice those two other macro calls at the end: custom_heap_default!() and custom_panic_default!(). These set up critical infrastructure your program needs to run, so let’s see what they do.
Custom Heap Allocator
The custom_heap_default!() macro sets up the default heap allocator for Solana programs. Solana programs run in a no_std environment, meaning they don’t use Rust’s standard library. This is because they execute inside the Solana VM, which has no operating system. Rust’s standard library allocator depends on OS syscalls like malloc and free, which the Solana VM doesn’t provide. Consequently, Solana programs need a custom allocator that works directly with the VM’s memory. Without a global allocator, any code that allocates heap memory (such as Vec::new(), or String::from()) will fail to compile, since Rust requires a global allocator for heap operations.
This custom heap allocator has a fixed size of 32KB. It works by maintaining a pointer that moves forward with each allocation. It never frees memory during a single program execution, which makes allocations very fast. Once you allocate something, that memory stays allocated until the program finishes executing. After the program execution completes, the entire heap is freed and reset for the next execution. If you try to allocate more than 32KB total during a single execution, the allocation fails and returns a null pointer.
Here’s the definition of the custom_panic_default!() macro:
#[macro_export]
macro_rules! custom_heap_default {
() => {
#[cfg(all(not(feature = "custom-heap"), target_os = "solana"))]
#[global_allocator]
static A: $crate::BumpAllocator = unsafe {
$crate::BumpAllocator::with_fixed_address_range(
$crate::HEAP_START_ADDRESS as usize,
$crate::HEAP_LENGTH,
)
};
};
}
This custom heap allocator is only enabled if you haven’t defined a custom-heap feature in your Cargo.toml. This allows you to provide your own allocator if this one doesn’t meet your needs. In practice, this is rarely needed. The provided allocator works fine for most programs. You’d only consider replacing it if you need to free and reuse memory during a single program execution (since this default custom Solana allocator never frees during execution), if you’re building a program that needs more than 32KB of heap space, or if you want a more sophisticated allocator for performance-critical applications.
Custom Panic Handler
The custom_panic_default!() macro sets up a custom panic handler. Similar to the heap allocator, Rust’s standard panic handler relies on OS features (like stderr and process termination) that don’t exist in the Solana VM. Therefore, Solana programs need a custom panic handler that uses Solana-specific syscalls (sol_log_ and sol_panic_) to report errors. Without defining a panic handler, Solana programs won’t compile because Rust requires one to be specified in no_std environments.
This custom panic handler logs the panic message and location (file, line, column) using Solana syscalls, then the program terminates. Here’s what it looks like:
#[macro_export]
macro_rules! custom_panic_default {
() => {
#[cfg(all(not(feature = "custom-panic"), target_os = "solana"))]
#[no_mangle]
fn custom_panic(info: &core::panic::PanicInfo<'_>) {
if let Some(mm) = info.message().as_str() {
unsafe {
$crate::__log(mm.as_ptr(), mm.len() as u64);
}
}
if let Some(loc) = info.location() {
unsafe {
$crate::__panic(
loc.file().as_ptr(),
loc.file().len() as u64,
loc.line() as u64,
loc.column() as u64,
)
}
}
}
};
}
Like the heap allocator, you can override this by adding a custom-panic feature to your Cargo.toml and defining your own panic handler. The most common reason to do this is to reduce program size. This custom panic handler includes string formatting and logging code that adds around 25KB to your compiled program. If you’re hitting the program size limit (10MB for BPF programs), you can define a no-op panic handler that does nothing, which saves that space. The tradeoff is that panics become silent. You won’t see error messages, which makes debugging harder. Most developers only do this as a last resort when they need to squeeze down program size.
This article is part of a tutorial series on Solana development