Rust Program Structure
Solana programs written in Rust have minimal structural requirements, allowing
for flexibility in how code is organized. The only requirement is that a program
must have an entrypoint
, which defines where the execution of a program
begins.
Program Structure
While there are no strict rules for file structure, Solana programs typically follow a common pattern:
entrypoint.rs
: Defines the entrypoint that routes incoming instructions.state.rs
: Define program-specific state (account data).instructions.rs
: Defines the instructions that the program can execute.processor.rs
: Defines the instruction handlers (functions) that implement the business logic for each instruction.error.rs
: Defines custom errors that the program can return.
You can find examples in the Solana Program Library.
Example Program
To demonstrate how to build a native Rust program with multiple instructions, we'll walk through a simple counter program that implements two instructions:
InitializeCounter
: Creates and initializes a new account with an initial value.IncrementCounter
: Increments the value stored in an existing account.
For simplicity, the program will be implemented in a single lib.rs
file,
though in practice you may want to split larger programs into multiple files.
Create a new Program
First, create a new Rust project using the standard cargo init
command with
the --lib
flag.
Navigate to the project directory. You should see the default src/lib.rs
and
Cargo.toml
files
Next, add the solana-program
dependency. This is the minimum dependency
required to build a Solana program.
Next, add the following snippet to Cargo.toml
. If you don't include this
config, the target/deploy
directory will not be generated when you build the
program.
Your Cargo.toml
file should look like the following:
Program Entrypoint
A Solana program entrypoint is the function that gets called when a program is invoked. The entrypoint has the following raw definition and developers are free to create their own implementation of the entrypoint function.
For simplicity, use the
entrypoint!
macro from the solana_program
crate to define the entrypoint in your program.
Replace the default code in lib.rs
with the following code. This snippet:
- Imports the required dependencies from
solana_program
- Defines the program entrypoint using the
entrypoint!
macro - Implements the
process_instruction
function that will route instructions to the appropriate handler functions
The entrypoint!
macro requires a function with the the following
type signature
as an argument:
When a Solana program is invoked, the entrypoint
deserializes
the
input data
(provided as bytes) into three values and passes them to the
process_instruction
function:
program_id
: The public key of the program being invoked (current program)accounts
: TheAccountInfo
for accounts required by the instruction being invokedinstruction_data
: Additional data passed to the program which specifies the instruction to execute and its required arguments
These three parameters directly correspond to the data that clients must provide when building an instruction to invoke a program.
Define Program State
When building a Solana program, you'll typically start by defining your program's state - the data that will be stored in accounts created and owned by your program.
Program state is defined using Rust structs that represent the data layout of your program's accounts. You can define multiple structs to represent different types of accounts for your program.
When working with accounts, you need a way to convert your program's data types to and from the raw bytes stored in an account's data field:
- Serialization: Converting your data types into bytes to store in an account's data field
- Deserialization: Converting the bytes stored in an account back into your data types
While you can use any serialization format for Solana program development, Borsh is commonly used. To use Borsh in your Solana program:
- Add the
borsh
crate as a dependency to yourCargo.toml
:
- Import the Borsh traits and use the derive macro to implement the traits for your structs:
Add the CounterAccount
struct to lib.rs
to define the program state. This
struct will be used in both the initialization and increment instructions.
Define Instructions
Instructions refer to the different operations that your Solana program can perform. Think of them as public APIs for your program - they define what actions users can take when interacting with your program.
Instructions are typically defined using a Rust enum where:
- Each enum variant represents a different instruction
- The variant's payload represents the instruction's parameters
Note that Rust enum variants are implicitly numbered starting from 0.
Below is an example of an enum defining two instructions:
When a client invokes your program, they must provide instruction data (as a buffer of bytes) where:
- The first byte identifies which instruction variant to execute (0, 1, etc.)
- The remaining bytes contain the serialized instruction parameters (if required)
To convert the instruction data (bytes) into a variant of the enum, it is common to implement a helper method. This method:
- Splits the first byte to get the instruction variant
- Matches on the variant and parses any additional parameters from the remaining bytes
- Returns the corresponding enum variant
For example, the unpack
method for the CounterInstruction
enum:
Add the following code to lib.rs
to define the instructions for the counter
program.
Instruction Handlers
Instruction handlers refer to the functions that contain the business logic for
each instruction. It's common to name handler functions as
process_<instruction_name>
, but you're free to choose any naming convention.
Add the following code to lib.rs
. This code uses the CounterInstruction
enum
and unpack
method defined in the previous step to route incoming instructions
to the appropriate handler functions:
Next, add the implementation of the process_initialize_counter
function. This
instruction handler:
- Creates and allocates space for a new account to store the counter data
- Initializing the account data with
initial_value
passed to the instruction
Next, add the implementation of the process_increment_counter
function. This
instruction increments the value of an existing counter account.
Instruction Testing
To test the program instructions, add the following dependencies to
Cargo.toml
.
Then add the following test module to lib.rs
and run cargo test-sbf
to
execute the tests. Optionally, use the --nocapture
flag to see the print
statements in the output.
Example output: