Rust-ohjelman rakenne
Rustilla kirjoitetuilla Solana-ohjelmilla on minimaaliset rakennevaatimukset,
mikä mahdollistaa joustavuuden koodin organisoinnissa. Ainoa vaatimus on, että
ohjelmalla on oltava entrypoint
, joka määrittää mistä ohjelman suoritus alkaa.
Ohjelman rakenne
Vaikka tiedostorakenteelle ei ole tiukkoja sääntöjä, Solana-ohjelmat noudattavat tyypillisesti yleistä kaavaa:
entrypoint.rs
: Määrittää sisääntulokodan, joka ohjaa saapuvat käskyt.state.rs
: Määrittää ohjelmalle ominaisen tilan (tilin tiedot).instructions.rs
: Määrittää käskyt, joita ohjelma voi suorittaa.processor.rs
: Määrittää käskyjen käsittelijät (funktiot), jotka toteuttavat liiketoimintalogiikan kullekin käskylle.error.rs
: Määrittää mukautetut virheet, joita ohjelma voi palauttaa.
Löydät esimerkkejä Solana Program Library -kirjastosta.
Esimerkkiohjelma
Havainnollistaaksemme, kuinka rakennetaan natiivi Rust-ohjelma useilla käskyillä, käymme läpi yksinkertaisen laskuriohjelma, joka toteuttaa kaksi käskyä:
InitializeCounter
: Luo ja alustaa uuden tilin alkuarvolla.IncrementCounter
: Kasvattaa olemassa olevaan tiliin tallennettua arvoa.
Yksinkertaisuuden vuoksi ohjelma toteutetaan yhdessä lib.rs
-tiedostossa,
vaikka käytännössä saatat haluta jakaa suuremmat ohjelmat useisiin tiedostoihin.
Luo uusi ohjelma
Ensin luo uusi Rust-projekti käyttäen standardia cargo init
-komentoa
--lib
-lipulla.
cargo init counter_program --lib
Siirry projektin hakemistoon. Sinun pitäisi nähdä oletusarvoiset src/lib.rs
ja
Cargo.toml
-tiedostot
cd counter_program
Seuraavaksi lisää solana-program
riippuvuus. Tämä on vähimmäisriippuvuus, joka
vaaditaan Solana-ohjelman rakentamiseen.
cargo add solana-program@1.18.26
Seuraavaksi lisää seuraava katkelma tiedostoon Cargo.toml
. Jos et sisällytä
tätä konfiguraatiota, target/deploy
hakemistoa ei luoda, kun rakennat
ohjelman.
[lib]crate-type = ["cdylib", "lib"]
Cargo.toml
tiedostosi pitäisi näyttää seuraavalta:
[package]name = "counter_program"version = "0.1.0"edition = "2021"[lib]crate-type = ["cdylib", "lib"][dependencies]solana-program = "1.18.26"
Ohjelman sisäänkäyntipiste
Solana-ohjelman sisäänkäyntipiste on funktio, jota kutsutaan, kun ohjelma käynnistetään. Sisäänkäyntipisteellä on seuraava raaka määritelmä, ja kehittäjät voivat vapaasti luoda oman toteutuksensa sisäänkäyntipistefunktiosta.
Yksinkertaisuuden vuoksi käytä
entrypoint!
makroa solana_program
paketista määrittääksesi sisäänkäyntipisteen ohjelmaasi.
#[no_mangle]pub unsafe extern "C" fn entrypoint(input: *mut u8) -> u64;
Korvaa oletuskoodi tiedostossa lib.rs
seuraavalla koodilla. Tämä katkelma:
- Tuo tarvittavat riippuvuudet paketista
solana_program
- Määrittää ohjelman sisäänkäyntipisteen käyttäen
entrypoint!
makroa - Toteuttaa
process_instruction
funktion, joka ohjaa käskyt asianmukaisiin käsittelijäfunktioihin
use solana_program::{account_info::{next_account_info, AccountInfo},entrypoint,entrypoint::ProgramResult,msg,program::invoke,program_error::ProgramError,pubkey::Pubkey,system_instruction,sysvar::{rent::Rent, Sysvar},};entrypoint!(process_instruction);pub fn process_instruction(program_id: &Pubkey,accounts: &[AccountInfo],instruction_data: &[u8],) -> ProgramResult {// Your program logicOk(())}
entrypoint!
makro vaatii funktion, jolla on seuraava
tyyppiallekirjoitus
argumenttina:
pub type ProcessInstruction =fn(program_id: &Pubkey, accounts: &[AccountInfo], instruction_data: &[u8]) -> ProgramResult;
Kun Solana-ohjelma käynnistetään, sisäänkäyntipiste
purkaa
syötetiedot
(jotka on annettu tavuina) kolmeksi arvoksi ja välittää ne
process_instruction
funktiolle:
program_id
: Käynnistettävän ohjelman (nykyisen ohjelman) julkinen avainaccounts
:AccountInfo
tileille, joita käynnistettävä käsky vaatiiinstruction_data
: Ohjelmalle välitettävät lisätiedot, jotka määrittävät suoritettavan käskyn ja sen vaatimat argumentit
Nämä kolme parametria vastaavat suoraan tietoja, jotka asiakkaiden on annettava rakentaessaan käskyä ohjelman käynnistämiseksi.
Määrittele ohjelman tila
Kun rakennat Solana-ohjelmaa, aloitat tyypillisesti määrittelemällä ohjelmasi tilan - tiedot, jotka tallennetaan ohjelmasi luomiin ja omistamiin tileihin.
Ohjelman tila määritellään Rust-struktuurien avulla, jotka kuvaavat ohjelmasi tilien tietorakennetta. Voit määritellä useita struktuureja edustamaan erilaisia tilityyppejä ohjelmaasi varten.
Kun työskentelet tilien kanssa, tarvitset tavan muuntaa ohjelmasi tietotyyppejä tavuiksi ja takaisin tilin data-kentässä:
- Serialisointi: Tietotyyppien muuntaminen tavuiksi tallennettavaksi tilin data-kenttään
- Deserialisointi: Tiliin tallennettujen tavujen muuntaminen takaisin tietotyypeiksi
Vaikka voit käyttää mitä tahansa serialisointiformaattia Solana-ohjelmien kehityksessä, Borsh on yleisesti käytetty. Käyttääksesi Borshia Solana-ohjelmassasi:
- Lisää
borsh
crate riippuvuudeksiCargo.toml
:
cargo add borsh
- Tuo Borsh-traitit ja käytä derive-makroa toteuttaaksesi traitit struktuureillesi:
use borsh::{BorshSerialize, BorshDeserialize};// Define struct representing our counter account's data#[derive(BorshSerialize, BorshDeserialize, Debug)]pub struct CounterAccount {count: u64,}
Lisää CounterAccount
struktuuri lib.rs
määritelläksesi ohjelman tilan. Tätä
struktuuria käytetään sekä alustus- että kasvatusohjeissa.
use solana_program::{account_info::{next_account_info, AccountInfo},entrypoint,entrypoint::ProgramResult,msg,program::invoke,program_error::ProgramError,pubkey::Pubkey,system_instruction,sysvar::{rent::Rent, Sysvar},};use borsh::{BorshSerialize, BorshDeserialize};entrypoint!(process_instruction);pub fn process_instruction(program_id: &Pubkey,accounts: &[AccountInfo],instruction_data: &[u8],) -> ProgramResult {// Your program logicOk(())}#[derive(BorshSerialize, BorshDeserialize, Debug)]pub struct CounterAccount {count: u64,}
Määrittele ohjeet
Ohjeet viittaavat eri toimintoihin, joita Solana-ohjelmasi voi suorittaa. Ajattele niitä ohjelmasi julkisina API:eina - ne määrittelevät, mitä toimintoja käyttäjät voivat tehdä ollessaan vuorovaikutuksessa ohjelmasi kanssa.
Ohjeet määritellään tyypillisesti Rust-enumina, jossa:
- Jokainen enum-variantti edustaa eri ohjetta
- Variantin hyötykuorma edustaa ohjeen parametreja
Huomaa, että Rust enum -variantit numeroidaan implisiittisesti alkaen numerosta 0.
Alla on esimerkki enum-määrittelystä, joka määrittelee kaksi ohjetta:
#[derive(BorshSerialize, BorshDeserialize, Debug)]pub enum CounterInstruction {InitializeCounter { initial_value: u64 }, // variant 0IncrementCounter, // variant 1}
Kun asiakas kutsuu ohjelmaasi, heidän täytyy tarjota instruction data (tavujen puskurina), jossa:
- Ensimmäinen tavu määrittää, mikä ohjevariantti suoritetaan (0, 1, jne.)
- Loput tavut sisältävät serialisoidut ohjeparametrit (jos tarpeen)
Instruction datan (tavujen) muuntamiseksi enum-variantiksi on yleistä toteuttaa apumetodi. Tämä metodi:
- Erottaa ensimmäisen tavun saadakseen ohjevariantin
- Tekee täsmäytyksen variantin perusteella ja jäsentää mahdolliset lisäparametrit jäljellä olevista tavuista
- Palauttaa vastaavan enum-variantin
Esimerkiksi unpack
-metodi CounterInstruction
-enumille:
impl CounterInstruction {pub fn unpack(input: &[u8]) -> Result<Self, ProgramError> {// Get the instruction variant from the first bytelet (&variant, rest) = input.split_first().ok_or(ProgramError::InvalidInstructionData)?;// Match instruction type and parse the remaining bytes based on the variantmatch variant {0 => {// For InitializeCounter, parse a u64 from the remaining byteslet initial_value = u64::from_le_bytes(rest.try_into().map_err(|_| ProgramError::InvalidInstructionData)?);Ok(Self::InitializeCounter { initial_value })}1 => Ok(Self::IncrementCounter), // No additional data needed_ => Err(ProgramError::InvalidInstructionData),}}}
Lisää seuraava koodi tiedostoon lib.rs
määrittääksesi ohjeet
laskuriohjelmalle.
use borsh::{BorshDeserialize, BorshSerialize};use solana_program::{account_info::AccountInfo, entrypoint, entrypoint::ProgramResult, msg,program_error::ProgramError, pubkey::Pubkey,};entrypoint!(process_instruction);pub fn process_instruction(program_id: &Pubkey,accounts: &[AccountInfo],instruction_data: &[u8],) -> ProgramResult {// Your program logicOk(())}#[derive(BorshSerialize, BorshDeserialize, Debug)]pub enum CounterInstruction {InitializeCounter { initial_value: u64 }, // variant 0IncrementCounter, // variant 1}impl CounterInstruction {pub fn unpack(input: &[u8]) -> Result<Self, ProgramError> {// Get the instruction variant from the first bytelet (&variant, rest) = input.split_first().ok_or(ProgramError::InvalidInstructionData)?;// Match instruction type and parse the remaining bytes based on the variantmatch variant {0 => {// For InitializeCounter, parse a u64 from the remaining byteslet initial_value = u64::from_le_bytes(rest.try_into().map_err(|_| ProgramError::InvalidInstructionData)?,);Ok(Self::InitializeCounter { initial_value })}1 => Ok(Self::IncrementCounter), // No additional data needed_ => Err(ProgramError::InvalidInstructionData),}}}
Ohjekäsittelijät
Ohjekäsittelijät viittaavat funktioihin, jotka sisältävät liiketoimintalogiikan
kullekin ohjeelle. On yleistä nimetä käsittelijäfunktiot muodossa
process_<instruction_name>
, mutta voit vapaasti valita minkä tahansa
nimeämiskäytännön.
Lisää seuraava koodi tiedostoon lib.rs
. Tämä koodi käyttää
CounterInstruction
-enumia ja unpack
-metodia, jotka määriteltiin
edellisessä vaiheessa, ohjatakseen saapuvat ohjeet asianmukaisiin
käsittelijäfunktioihin:
entrypoint!(process_instruction);pub fn process_instruction(program_id: &Pubkey,accounts: &[AccountInfo],instruction_data: &[u8],) -> ProgramResult {// Unpack instruction datalet instruction = CounterInstruction::unpack(instruction_data)?;// Match instruction typematch instruction {CounterInstruction::InitializeCounter { initial_value } => {process_initialize_counter(program_id, accounts, initial_value)?}CounterInstruction::IncrementCounter => process_increment_counter(program_id, accounts)?,};}fn process_initialize_counter(program_id: &Pubkey,accounts: &[AccountInfo],initial_value: u64,) -> ProgramResult {// Implementation details...Ok(())}fn process_increment_counter(program_id: &Pubkey, accounts: &[AccountInfo]) -> ProgramResult {// Implementation details...Ok(())}
Seuraavaksi lisää toteutus process_initialize_counter
-funktiolle. Tämä
ohjekäsittelijä:
- Luo ja varaa tilan uudelle tilille laskuridatan tallentamista varten
- Alustaa tilin datan ohjeelle välitetyllä
initial_value
-arvolla
// Initialize a new counter accountfn process_initialize_counter(program_id: &Pubkey,accounts: &[AccountInfo],initial_value: u64,) -> ProgramResult {let accounts_iter = &mut accounts.iter();let counter_account = next_account_info(accounts_iter)?;let payer_account = next_account_info(accounts_iter)?;let system_program = next_account_info(accounts_iter)?;// Size of our counter accountlet account_space = 8; // Size in bytes to store a u64// Calculate minimum balance for rent exemptionlet rent = Rent::get()?;let required_lamports = rent.minimum_balance(account_space);// Create the counter accountinvoke(&system_instruction::create_account(payer_account.key, // Account paying for the new accountcounter_account.key, // Account to be createdrequired_lamports, // Amount of lamports to transfer to the new accountaccount_space as u64, // Size in bytes to allocate for the data fieldprogram_id, // Set program owner to our program),&[payer_account.clone(),counter_account.clone(),system_program.clone(),],)?;// Create a new CounterAccount struct with the initial valuelet counter_data = CounterAccount {count: initial_value,};// Get a mutable reference to the counter account's datalet mut account_data = &mut counter_account.data.borrow_mut()[..];// Serialize the CounterAccount struct into the account's datacounter_data.serialize(&mut account_data)?;msg!("Counter initialized with value: {}", initial_value);Ok(())}
Seuraavaksi lisää toteutus process_increment_counter
-funktiolle. Tämä ohje
kasvattaa olemassa olevan laskuritilin arvoa.
// Update an existing counter's valuefn process_increment_counter(program_id: &Pubkey, accounts: &[AccountInfo]) -> ProgramResult {let accounts_iter = &mut accounts.iter();let counter_account = next_account_info(accounts_iter)?;// Verify account ownershipif counter_account.owner != program_id {return Err(ProgramError::IncorrectProgramId);}// Mutable borrow the account datalet mut data = counter_account.data.borrow_mut();// Deserialize the account data into our CounterAccount structlet mut counter_data: CounterAccount = CounterAccount::try_from_slice(&data)?;// Increment the counter valuecounter_data.count = counter_data.count.checked_add(1).ok_or(ProgramError::InvalidAccountData)?;// Serialize the updated counter data back into the accountcounter_data.serialize(&mut &mut data[..])?;msg!("Counter incremented to: {}", counter_data.count);Ok(())}
Ohjeiden testaus
Testataksesi ohjelman ohjeita, lisää seuraavat riippuvuudet tiedostoon
Cargo.toml
.
cargo add solana-program-test@1.18.26 --devcargo add solana-sdk@1.18.26 --devcargo add tokio --dev
Lisää sitten seuraava testimoduuli tiedostoon lib.rs
ja suorita
cargo test-sbf
testien suorittamiseksi. Halutessasi voit käyttää lippua
--nocapture
nähdäksesi tulosteet.
cargo test-sbf -- --nocapture
#[cfg(test)]mod test {use super::*;use solana_program_test::*;use solana_sdk::{instruction::{AccountMeta, Instruction},signature::{Keypair, Signer},system_program,transaction::Transaction,};#[tokio::test]async fn test_counter_program() {let program_id = Pubkey::new_unique();let (mut banks_client, payer, recent_blockhash) = ProgramTest::new("counter_program",program_id,processor!(process_instruction),).start().await;// Create a new keypair to use as the address for our counter accountlet counter_keypair = Keypair::new();let initial_value: u64 = 42;// Step 1: Initialize the counterprintln!("Testing counter initialization...");// Create initialization instructionlet mut init_instruction_data = vec![0]; // 0 = initialize instructioninit_instruction_data.extend_from_slice(&initial_value.to_le_bytes());let initialize_instruction = Instruction::new_with_bytes(program_id,&init_instruction_data,vec![AccountMeta::new(counter_keypair.pubkey(), true),AccountMeta::new(payer.pubkey(), true),AccountMeta::new_readonly(system_program::id(), false),],);// Send transaction with initialize instructionlet mut transaction =Transaction::new_with_payer(&[initialize_instruction], Some(&payer.pubkey()));transaction.sign(&[&payer, &counter_keypair], recent_blockhash);banks_client.process_transaction(transaction).await.unwrap();// Check account datalet account = banks_client.get_account(counter_keypair.pubkey()).await.expect("Failed to get counter account");if let Some(account_data) = account {let counter: CounterAccount = CounterAccount::try_from_slice(&account_data.data).expect("Failed to deserialize counter data");assert_eq!(counter.count, 42);println!("✅ Counter initialized successfully with value: {}",counter.count);}// Step 2: Increment the counterprintln!("Testing counter increment...");// Create increment instructionlet increment_instruction = Instruction::new_with_bytes(program_id,&[1], // 1 = increment instructionvec![AccountMeta::new(counter_keypair.pubkey(), true)],);// Send transaction with increment instructionlet mut transaction =Transaction::new_with_payer(&[increment_instruction], Some(&payer.pubkey()));transaction.sign(&[&payer, &counter_keypair], recent_blockhash);banks_client.process_transaction(transaction).await.unwrap();// Check account datalet account = banks_client.get_account(counter_keypair.pubkey()).await.expect("Failed to get counter account");if let Some(account_data) = account {let counter: CounterAccount = CounterAccount::try_from_slice(&account_data.data).expect("Failed to deserialize counter data");assert_eq!(counter.count, 43);println!("✅ Counter incremented successfully to: {}", counter.count);}}}
Esimerkki tulostuksesta:
running 1 test[2024-10-29T20:51:13.783708000Z INFO solana_program_test] "counter_program" SBF program from /counter_program/target/deploy/counter_program.so, modified 2 seconds, 169 ms, 153 µs and 461 ns ago[2024-10-29T20:51:13.855204000Z DEBUG solana_runtime::message_processor::stable_log] Program 1111111QLbz7JHiBTspS962RLKV8GndWFwiEaqKM invoke [1][2024-10-29T20:51:13.856052000Z DEBUG solana_runtime::message_processor::stable_log] Program 11111111111111111111111111111111 invoke [2][2024-10-29T20:51:13.856135000Z DEBUG solana_runtime::message_processor::stable_log] Program 11111111111111111111111111111111 success[2024-10-29T20:51:13.856242000Z DEBUG solana_runtime::message_processor::stable_log] Program log: Counter initialized with value: 42[2024-10-29T20:51:13.856285000Z DEBUG solana_runtime::message_processor::stable_log] Program 1111111QLbz7JHiBTspS962RLKV8GndWFwiEaqKM consumed 3791 of 200000 compute units[2024-10-29T20:51:13.856307000Z DEBUG solana_runtime::message_processor::stable_log] Program 1111111QLbz7JHiBTspS962RLKV8GndWFwiEaqKM success[2024-10-29T20:51:13.860038000Z DEBUG solana_runtime::message_processor::stable_log] Program 1111111QLbz7JHiBTspS962RLKV8GndWFwiEaqKM invoke [1][2024-10-29T20:51:13.860333000Z DEBUG solana_runtime::message_processor::stable_log] Program log: Counter incremented to: 43[2024-10-29T20:51:13.860355000Z DEBUG solana_runtime::message_processor::stable_log] Program 1111111QLbz7JHiBTspS962RLKV8GndWFwiEaqKM consumed 756 of 200000 compute units[2024-10-29T20:51:13.860375000Z DEBUG solana_runtime::message_processor::stable_log] Program 1111111QLbz7JHiBTspS962RLKV8GndWFwiEaqKM successtest test::test_counter_program ... oktest result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.08s
Is this page helpful?