Rust Program Yapısı
Rust ile yazılan Solana programları, kodun nasıl düzenleneceği konusunda
esneklik sağlayan minimum yapısal gereksinimlere sahiptir. Tek gereksinim, bir
programın çalışmasının başladığı yeri tanımlayan bir entrypoint
içermesidir.
Program Yapısı
Dosya yapısı için katı kurallar olmasa da, Solana programları genellikle yaygın bir modeli takip eder:
entrypoint.rs
: Gelen talimatları yönlendiren giriş noktasını tanımlar.state.rs
: Programa özgü durumu (hesap verisi) tanımlar.instructions.rs
: Programın yürütebileceği talimatları tanımlar.processor.rs
: Her talimat için iş mantığını uygulayan talimat işleyicilerini (fonksiyonlar) tanımlar.error.rs
: Programın döndürebileceği özel hataları tanımlar.
Örnekleri Solana Program Library içinde bulabilirsiniz.
Örnek Program
Birden fazla talimat içeren yerel bir Rust programının nasıl oluşturulacağını göstermek için, iki talimatı uygulayan basit bir sayaç programını inceleyeceğiz:
InitializeCounter
: Başlangıç değeriyle yeni bir hesap oluşturur ve başlatır.IncrementCounter
: Mevcut bir hesapta depolanan değeri artırır.
Basitlik açısından, program tek bir lib.rs
dosyasında uygulanacaktır, ancak
uygulamada daha büyük programları birden fazla dosyaya bölmek isteyebilirsiniz.
Yeni Bir Program Oluşturma
İlk olarak, standart cargo init
komutunu --lib
bayrağıyla kullanarak yeni
bir Rust projesi oluşturun.
cargo init counter_program --lib
Proje dizinine gidin. Varsayılan src/lib.rs
ve Cargo.toml
dosyalarını
görmelisiniz
cd counter_program
Sonra, solana-program
bağımlılığını ekleyin. Bu, bir Solana programı
oluşturmak için gereken minimum bağımlılıktır.
cargo add solana-program@1.18.26
Ardından, aşağıdaki kod parçasını Cargo.toml
dosyasına ekleyin. Bu
yapılandırmayı dahil etmezseniz, programı derlediğinizde target/deploy
dizini
oluşturulmayacaktır.
[lib]crate-type = ["cdylib", "lib"]
Cargo.toml
dosyanız aşağıdaki gibi görünmelidir:
[package]name = "counter_program"version = "0.1.0"edition = "2021"[lib]crate-type = ["cdylib", "lib"][dependencies]solana-program = "1.18.26"
Program giriş noktası
Bir Solana programı giriş noktası, program çağrıldığında çalıştırılan fonksiyondur. Giriş noktasının aşağıdaki ham tanımı vardır ve geliştiriciler giriş noktası fonksiyonunun kendi uygulamalarını oluşturmakta özgürdür.
Basitlik için, programınızda giriş noktasını tanımlamak için solana_program
paketinden
entrypoint!
makrosunu kullanın.
#[no_mangle]pub unsafe extern "C" fn entrypoint(input: *mut u8) -> u64;
lib.rs
içindeki varsayılan kodu aşağıdaki kodla değiştirin. Bu kod parçası:
solana_program
paketinden gerekli bağımlılıkları içe aktarırentrypoint!
makrosunu kullanarak program giriş noktasını tanımlar- Talimatları uygun işleyici fonksiyonlara yönlendirecek
process_instruction
fonksiyonunu uygular
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!
makrosu, argüman olarak aşağıdaki
tip imzasına
sahip bir fonksiyon gerektirir:
pub type ProcessInstruction =fn(program_id: &Pubkey, accounts: &[AccountInfo], instruction_data: &[u8]) -> ProgramResult;
Bir Solana programı çağrıldığında, giriş noktası
girdi verilerini
(bayt olarak sağlanan)
deserialize eder
ve üç değeri
process_instruction
fonksiyonuna geçirir:
program_id
: Çağrılan programın (mevcut program) açık anahtarıaccounts
: Çağrılan talimat tarafından gereken hesaplar içinAccountInfo
instruction_data
: Yürütülecek talimatı ve gerekli argümanlarını belirten, programa aktarılan ek veriler
Bu üç parametre, müşterilerin bir programı çağırmak için bir talimat oluştururken sağlamaları gereken verilerle doğrudan ilişkilidir.
Program Durumunu Tanımlama
Bir Solana programı oluştururken, genellikle programınızın durumunu tanımlayarak başlarsınız - programınız tarafından oluşturulan ve sahip olunan hesaplarda depolanacak veriler.
Program durumu, programınızın hesaplarının veri düzenini temsil eden Rust struct'ları kullanılarak tanımlanır. Programınız için farklı hesap türlerini temsil etmek üzere birden fazla struct tanımlayabilirsiniz.
Hesaplarla çalışırken, programınızın veri türlerini bir hesabın veri alanında depolanan ham baytlara dönüştürmek ve bunlardan geri dönüştürmek için bir yönteme ihtiyacınız vardır:
- Serileştirme: Veri türlerinizi bir hesabın veri alanında depolamak için baytlara dönüştürme
- Deserileştirme: Bir hesapta depolanan baytları veri türlerinize geri dönüştürme
Solana program geliştirmede herhangi bir serileştirme formatı kullanabilirsiniz, ancak Borsh yaygın olarak kullanılır. Solana programınızda Borsh kullanmak için:
borsh
crate'iniCargo.toml
dosyanıza bağımlılık olarak ekleyin:
cargo add borsh
- Borsh trait'lerini içe aktarın ve struct'larınız için trait'leri uygulamak üzere derive makrosunu kullanın:
use borsh::{BorshSerialize, BorshDeserialize};// Define struct representing our counter account's data#[derive(BorshSerialize, BorshDeserialize, Debug)]pub struct CounterAccount {count: u64,}
Program durumunu tanımlamak için lib.rs
dosyasına CounterAccount
struct'ını
ekleyin. Bu struct, hem başlatma hem de artırma talimatlarında kullanılacaktır.
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,}
Talimatları Tanımlama
Talimatlar, Solana programınızın gerçekleştirebileceği farklı işlemleri ifade eder. Bunları programınızın genel API'leri olarak düşünün - kullanıcıların programınızla etkileşimde bulunurken gerçekleştirebilecekleri eylemleri tanımlarlar.
Talimatlar genellikle bir Rust enum kullanılarak tanımlanır:
- Her enum varyantı farklı bir talimatı temsil eder
- Varyantın yükü, talimatın parametrelerini temsil eder
Rust enum varyantlarının 0'dan başlayarak otomatik olarak numaralandırıldığını unutmayın.
Aşağıda iki talimat tanımlayan bir enum örneği verilmiştir:
#[derive(BorshSerialize, BorshDeserialize, Debug)]pub enum CounterInstruction {InitializeCounter { initial_value: u64 }, // variant 0IncrementCounter, // variant 1}
Bir istemci programınızı çağırdığında, şu özelliklere sahip talimat verilerini (bayt tamponu olarak) sağlamalıdır:
- İlk bayt, hangi talimat varyantının yürütüleceğini belirtir (0, 1, vb.)
- Kalan baytlar, serileştirilmiş talimat parametrelerini içerir (gerekirse)
Talimat verilerini (baytlar) enum'un bir varyantına dönüştürmek için genellikle bir yardımcı metot uygulanır. Bu metot:
- İlk baytı ayırarak talimat varyantını alır
- Varyanta göre eşleştirme yapar ve kalan baytlardan ek parametreleri ayrıştırır
- İlgili enum varyantını döndürür
Örneğin, CounterInstruction
enum'u için unpack
metodu:
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),}}}
Sayaç programı için talimatları tanımlamak üzere lib.rs
dosyasına aşağıdaki
kodu ekleyin.
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),}}}
Talimat İşleyicileri
Talimat işleyicileri, her talimat için iş mantığını içeren fonksiyonlardır.
İşleyici fonksiyonlarını process_<instruction_name>
olarak adlandırmak
yaygındır, ancak istediğiniz adlandırma kuralını seçmekte özgürsünüz.
Aşağıdaki kodu lib.rs
dosyasına ekleyin. Bu kod, önceki adımda tanımlanan
CounterInstruction
enum'unu ve unpack
metodunu kullanarak gelen talimatları
uygun işleyici fonksiyonlara yönlendirir:
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(())}
Şimdi process_initialize_counter
fonksiyonunun uygulamasını ekleyin. Bu
talimat işleyicisi:
- Sayaç verilerini depolamak için yeni bir hesap oluşturur ve alan tahsis eder
- Hesap verilerini, talimata iletilen
initial_value
ile başlatır
// 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(())}
Şimdi, process_increment_counter
fonksiyonunun uygulamasını ekleyin. Bu
talimat, mevcut bir sayaç hesabının değerini artırır.
// 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(())}
Talimat testi
Program talimatlarını test etmek için, aşağıdaki bağımlılıkları Cargo.toml
dosyasına ekleyin.
cargo add solana-program-test@1.18.26 --devcargo add solana-sdk@1.18.26 --devcargo add tokio --dev
Ardından aşağıdaki test modülünü lib.rs
dosyasına ekleyin ve testleri
çalıştırmak için cargo test-sbf
komutunu çalıştırın. İsteğe bağlı olarak,
çıktıda yazdırma ifadelerini görmek için --nocapture
bayrağını kullanın.
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);}}}
Örnek çıktı:
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?