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:

  1. InitializeCounter: Başlangıç değeriyle yeni bir hesap oluşturur ve başlatır.
  2. 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.

Terminal
cargo init counter_program --lib

Proje dizinine gidin. Varsayılan src/lib.rs ve Cargo.toml dosyalarını görmelisiniz

Terminal
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.

Terminal
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.

Cargo.toml
[lib]
crate-type = ["cdylib", "lib"]

Cargo.toml dosyanız aşağıdaki gibi görünmelidir:

Cargo.toml
[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ı:

  1. solana_program paketinden gerekli bağımlılıkları içe aktarır
  2. entrypoint! makrosunu kullanarak program giriş noktasını tanımlar
  3. Talimatları uygun işleyici fonksiyonlara yönlendirecek process_instruction fonksiyonunu uygular
lib.rs
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 logic
Ok(())
}

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çin AccountInfo
  • 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:

  1. borsh crate'ini Cargo.toml dosyanıza bağımlılık olarak ekleyin:
Terminal
cargo add borsh
  1. 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.

lib.rs
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 logic
Ok(())
}
#[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 0
IncrementCounter, // 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:

  1. İlk baytı ayırarak talimat varyantını alır
  2. Varyanta göre eşleştirme yapar ve kalan baytlardan ek parametreleri ayrıştırır
  3. İ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 byte
let (&variant, rest) = input
.split_first()
.ok_or(ProgramError::InvalidInstructionData)?;
// Match instruction type and parse the remaining bytes based on the variant
match variant {
0 => {
// For InitializeCounter, parse a u64 from the remaining bytes
let 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.

lib.rs
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 logic
Ok(())
}
#[derive(BorshSerialize, BorshDeserialize, Debug)]
pub enum CounterInstruction {
InitializeCounter { initial_value: u64 }, // variant 0
IncrementCounter, // variant 1
}
impl CounterInstruction {
pub fn unpack(input: &[u8]) -> Result<Self, ProgramError> {
// Get the instruction variant from the first byte
let (&variant, rest) = input
.split_first()
.ok_or(ProgramError::InvalidInstructionData)?;
// Match instruction type and parse the remaining bytes based on the variant
match variant {
0 => {
// For InitializeCounter, parse a u64 from the remaining bytes
let 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:

lib.rs
entrypoint!(process_instruction);
pub fn process_instruction(
program_id: &Pubkey,
accounts: &[AccountInfo],
instruction_data: &[u8],
) -> ProgramResult {
// Unpack instruction data
let instruction = CounterInstruction::unpack(instruction_data)?;
// Match instruction type
match 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:

  1. Sayaç verilerini depolamak için yeni bir hesap oluşturur ve alan tahsis eder
  2. Hesap verilerini, talimata iletilen initial_value ile başlatır

lib.rs
// Initialize a new counter account
fn 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 account
let account_space = 8; // Size in bytes to store a u64
// Calculate minimum balance for rent exemption
let rent = Rent::get()?;
let required_lamports = rent.minimum_balance(account_space);
// Create the counter account
invoke(
&system_instruction::create_account(
payer_account.key, // Account paying for the new account
counter_account.key, // Account to be created
required_lamports, // Amount of lamports to transfer to the new account
account_space as u64, // Size in bytes to allocate for the data field
program_id, // Set program owner to our program
),
&[
payer_account.clone(),
counter_account.clone(),
system_program.clone(),
],
)?;
// Create a new CounterAccount struct with the initial value
let counter_data = CounterAccount {
count: initial_value,
};
// Get a mutable reference to the counter account's data
let mut account_data = &mut counter_account.data.borrow_mut()[..];
// Serialize the CounterAccount struct into the account's data
counter_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.

lib.rs
// Update an existing counter's value
fn 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 ownership
if counter_account.owner != program_id {
return Err(ProgramError::IncorrectProgramId);
}
// Mutable borrow the account data
let mut data = counter_account.data.borrow_mut();
// Deserialize the account data into our CounterAccount struct
let mut counter_data: CounterAccount = CounterAccount::try_from_slice(&data)?;
// Increment the counter value
counter_data.count = counter_data
.count
.checked_add(1)
.ok_or(ProgramError::InvalidAccountData)?;
// Serialize the updated counter data back into the account
counter_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.

Terminal
cargo add solana-program-test@1.18.26 --dev
cargo add solana-sdk@1.18.26 --dev
cargo 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.

Terminal
cargo test-sbf -- --nocapture

lib.rs
#[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 account
let counter_keypair = Keypair::new();
let initial_value: u64 = 42;
// Step 1: Initialize the counter
println!("Testing counter initialization...");
// Create initialization instruction
let mut init_instruction_data = vec![0]; // 0 = initialize instruction
init_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 instruction
let 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 data
let 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 counter
println!("Testing counter increment...");
// Create increment instruction
let increment_instruction = Instruction::new_with_bytes(
program_id,
&[1], // 1 = increment instruction
vec![AccountMeta::new(counter_keypair.pubkey(), true)],
);
// Send transaction with increment instruction
let 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 data
let 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ı:

Terminal
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 success
test test::test_counter_program ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.08s

Is this page helpful?

İçindekiler

Sayfayı Düzenle