هيكل برنامج Rust

تتميز برامج Solana المكتوبة بلغة Rust بمتطلبات هيكلية بسيطة، مما يتيح المرونة في تنظيم الشيفرة. المتطلب الوحيد هو أن البرنامج يجب أن يحتوي على "entrypoint"، الذي يحدد نقطة بداية تنفيذ البرنامج.

هيكل البرنامج

على الرغم من عدم وجود قواعد صارمة لهيكل الملفات، فإن برامج Solana عادةً تتبع نمطًا شائعًا:

  • "lib.rs": يحدد نقطة الدخول التي توجه التعليمات الواردة.
  • "state.rs": تحديد حالة خاصة بالبرنامج (بيانات الحساب).
  • "instruction.rs": يحدد التعليمات التي يمكن للبرنامج تنفيذها.
  • "processor.rs": يحدد معالجات التعليمات (الوظائف) التي تنفذ المنطق التجاري لكل تعليمة.
  • "error.rs": يحدد الأخطاء المخصصة التي يمكن للبرنامج إرجاعها.

يمكنك العثور على أمثلة في مكتبة برامج Solana.

مثال على البرنامج

لتوضيح كيفية بناء برنامج Rust أصلي مع تعليمات متعددة، سنشرح برنامج عداد بسيط ينفذ تعليمتين:

  1. "initialize": ينشئ ويهيئ حسابًا جديدًا بقيمة أولية.
  2. "increment": يزيد القيمة المخزنة في حساب موجود.

للتبسيط، سيتم تنفيذ البرنامج في ملف "lib.rs" واحد، على الرغم من أنه في الممارسة العملية قد ترغب في تقسيم البرامج الأكبر إلى ملفات متعددة.

إنشاء برنامج جديد

أولاً، قم بإنشاء مشروع Rust جديد باستخدام أمر "cargo new" القياسي مع خيار "--lib".

cargo new counter --lib

انتقل إلى دليل المشروع. يجب أن ترى ملفات "Cargo.toml" و "src/lib.rs" الافتراضية

Terminal
cd counter_program

بعد ذلك، أضف تبعية solana-program. هذه هي الحد الأدنى من التبعيات المطلوبة لبناء برنامج Solana.

Terminal
cargo add solana-program@1.18.26

بعد ذلك، أضف المقتطف التالي إلى Cargo.toml. إذا لم تقم بتضمين هذه الإعدادات، فلن يتم إنشاء دليل target/deploy عند بناء البرنامج.

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

يجب أن يبدو ملف Cargo.toml الخاص بك كما يلي:

Cargo.toml
[package]
name = "counter_program"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib", "lib"]
[dependencies]
solana-program = "1.18.26"

نقطة دخول البرنامج

نقطة دخول برنامج Solana هي الدالة التي يتم استدعاؤها عند تنفيذ البرنامج. تحتوي نقطة الدخول على التعريف الأولي التالي ويمكن للمطورين إنشاء تنفيذهم الخاص لدالة نقطة الدخول.

للتبسيط، استخدم الماكرو entrypoint! من حزمة solana_program لتعريف نقطة الدخول في برنامجك.

#[no_mangle]
pub unsafe extern "C" fn entrypoint(input: *mut u8) -> u64;

استبدل الكود الافتراضي في lib.rs بالكود التالي. هذا المقتطف:

  1. يستورد التبعيات المطلوبة من solana_program
  2. يعرّف نقطة دخول البرنامج باستخدام ماكرو entrypoint!
  3. ينفذ دالة process_instruction التي ستوجه التعليمات إلى دوال المعالجة المناسبة
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! دالة ذات توقيع النوع التالي كوسيط:

pub type ProcessInstruction =
fn(program_id: &Pubkey, accounts: &[AccountInfo], instruction_data: &[u8]) -> ProgramResult;

عند استدعاء برنامج Solana، تقوم نقطة الدخول بفك تسلسل بيانات الإدخال (المقدمة كبايتات) إلى ثلاث قيم وتمررها إلى دالة process_instruction:

  • program_id: المفتاح العام للبرنامج الذي يتم استدعاؤه (البرنامج الحالي)
  • accounts: AccountInfo للحسابات المطلوبة بواسطة التعليمات التي يتم استدعاؤها
  • instruction_data: بيانات إضافية تُمرر إلى البرنامج والتي تحدد التعليمات المراد تنفيذها والوسائط المطلوبة لها

تتوافق هذه المعلمات الثلاث مباشرة مع البيانات التي يجب على العملاء توفيرها عند بناء تعليمات لاستدعاء برنامج.

تحديد حالة البرنامج

عند بناء برنامج Solana، ستبدأ عادةً بتحديد حالة البرنامج - البيانات التي سيتم تخزينها في الحسابات التي ينشئها ويمتلكها برنامجك.

يتم تحديد حالة البرنامج باستخدام هياكل Rust التي تمثل تخطيط بيانات حسابات برنامجك. يمكنك تحديد هياكل متعددة لتمثيل أنواع مختلفة من الحسابات لبرنامجك.

عند العمل مع الحسابات، تحتاج إلى طريقة لتحويل أنواع بيانات برنامجك من وإلى البايتات الخام المخزنة في حقل بيانات الحساب:

  • التسلسل (Serialization): تحويل أنواع البيانات الخاصة بك إلى بايتات لتخزينها في حقل بيانات الحساب
  • إلغاء التسلسل (Deserialization): تحويل البايتات المخزنة في الحساب مرة أخرى إلى أنواع البيانات الخاصة بك

بينما يمكنك استخدام أي تنسيق تسلسل لتطوير برنامج Solana، يتم استخدام Borsh بشكل شائع. لاستخدام Borsh في برنامج Solana الخاص بك:

  1. أضف مكتبة borsh كتبعية إلى ملف Cargo.toml الخاص بك:
Terminal
cargo add borsh
  1. استورد سمات Borsh واستخدم ماكرو derive لتنفيذ السمات لهياكلك:
use borsh::{BorshSerialize, BorshDeserialize};
// Define struct representing our counter account's data
#[derive(BorshSerialize, BorshDeserialize, Debug)]
pub struct CounterAccount {
count: u64,
}

أضف هيكل Counter إلى lib.rs لتحديد حالة البرنامج. سيتم استخدام هذا الهيكل في كل من تعليمات التهيئة والزيادة.

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,
}

تحديد التعليمات

تشير التعليمات إلى العمليات المختلفة التي يمكن لبرنامج Solana الخاص بك تنفيذها. فكر فيها كواجهات برمجة تطبيقات (APIs) عامة لبرنامجك - فهي تحدد الإجراءات التي يمكن للمستخدمين اتخاذها عند التفاعل مع برنامجك.

يتم تحديد التعليمات عادةً باستخدام تعداد Rust حيث:

  • كل متغير في التعداد يمثل تعليمة مختلفة
  • حمولة المتغير تمثل معلمات التعليمة

لاحظ أن متغيرات Rust enum يتم ترقيمها تلقائيًا بدءًا من 0.

فيما يلي مثال على enum يحدد تعليمتين:

#[derive(BorshSerialize, BorshDeserialize, Debug)]
pub enum CounterInstruction {
InitializeCounter { initial_value: u64 }, // variant 0
IncrementCounter, // variant 1
}

عندما يقوم العميل باستدعاء برنامجك، يجب عليه توفير بيانات التعليمات (كمخزن مؤقت من البايتات) حيث:

  • البايت الأول يحدد أي متغير من التعليمات سيتم تنفيذه (0، 1، إلخ)
  • البايتات المتبقية تحتوي على معلمات التعليمات المسلسلة (إذا كانت مطلوبة)

لتحويل بيانات التعليمات (البايتات) إلى متغير من الـ enum، من الشائع تنفيذ طريقة مساعدة. هذه الطريقة:

  1. تفصل البايت الأول للحصول على متغير التعليمات
  2. تطابق المتغير وتحلل أي معلمات إضافية من البايتات المتبقية
  3. تعيد متغير الـ enum المقابل

على سبيل المثال، طريقة unpack للـ enum CounterInstruction:

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),
}
}
}

أضف الكود التالي إلى lib.rs لتحديد التعليمات لبرنامج العداد.

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),
}
}
}

معالجات التعليمات

تشير معالجات التعليمات إلى الوظائف التي تحتوي على منطق الأعمال لكل تعليمة. من الشائع تسمية وظائف المعالج كـ process_<instruction_name>، ولكن يمكنك اختيار أي اتفاقية تسمية.

أضف الكود التالي إلى lib.rs. يستخدم هذا الكود enum CounterInstruction وطريقة unpack المحددة في الخطوة السابقة لتوجيه التعليمات الواردة إلى وظائف المعالج المناسبة:

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(())
}

بعد ذلك، أضف تنفيذ وظيفة process_initialize_counter. معالج التعليمات هذا:

  1. ينشئ ويخصص مساحة لحساب جديد لتخزين بيانات العداد
  2. تهيئة بيانات الحساب بـ initial_value الممررة إلى التعليمة

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(())
}

بعد ذلك، أضف تنفيذ دالة process_increment_counter. هذه التعليمة تزيد قيمة حساب العداد الموجود.

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(())
}

اختبار التعليمات

لاختبار تعليمات البرنامج، أضف التبعيات التالية إلى Cargo.toml.

Terminal
cargo add solana-program-test@1.18.26 --dev
cargo add solana-sdk@1.18.26 --dev
cargo add tokio --dev

ثم أضف وحدة الاختبار التالية إلى lib.rs وقم بتشغيل cargo test-sbf لتنفيذ الاختبارات. اختيارياً، استخدم العلامة --nocapture لرؤية عبارات الطباعة في المخرجات.

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);
}
}
}

مثال للمخرجات:

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?

جدول المحتويات

تعديل الصفحة