पहले भाग में, हमने Solana प्रोग्राम इनपुट सीरियलाइज़ेशन फॉर्मेट और मेमोरी में प्रोग्राम इनपुट्स को कैसे लेआउट किया जाता है, इसके बारे में कवर किया था। इस भाग में, हम यह कवर करेंगे कि प्रोग्राम कैसे आने वाले प्रोग्राम इनपुट्स को उपयुक्त हैंडलर्स तक रूट करते हैं और इसे संभव बनाने के लिए entrypoint किस सपोर्टिंग कोड को सेट अप करता है।
The Instruction Processor
Entrypoint फ़ंक्शन (entrypoint! मैक्रो द्वारा जनरेट किया गया) कच्चे (raw) बाइट ऐरे को program_id, accounts, और instruction_data में डीसीरियलाइज़ करता है, फिर उन्हें instruction processor को पास कर देता है। Instruction processor instruction_data को पढ़ता है ताकि यह निर्धारित किया जा सके कि कौन सा हैंडलर एक्सीक्यूट होना चाहिए—ठीक वैसे ही जैसे Solidity कॉन्ट्रैक्ट्स फ़ंक्शन सिलेक्टर के आधार पर फ़ंक्शन कॉल्स को रूट करते हैं।
Instruction processor को आपके प्रोग्राम द्वारा ही डिफाइन किया जाना चाहिए। Entrypoint यह फ़ंक्शन प्रदान नहीं करता है, यह केवल उस टाइप सिग्नेचर को डिफाइन करता है जिससे आपके instruction processor को मैच करना चाहिए। इस टाइप को इस प्रकार डिफाइन किया गया है:
pub type ProcessInstruction = fn(program_id: &Pubkey, accounts: &[AccountInfo], instruction_data: &[u8]) -> ProgramResult;
आपके प्रोग्राम को इस सिग्नेचर से मैच करने वाला एक फ़ंक्शन बनाना होगा (जो उन तीन प्रोग्राम इनपुट्स को लेता है और एक ProgramResult रिटर्न करता है), और फिर इसे entrypoint! मैक्रो को पास करना होगा (हम नीचे देखेंगे कि यह कैसे काम करता है)। आप यह फ़ंक्शन कैसे बनाते हैं, यह इस बात पर निर्भर करता है कि आप Anchor का उपयोग कर रहे हैं या एक नेटिव Rust प्रोग्राम लिख रहे हैं।
नेटिव Rust प्रोग्राम्स में, आप इस सटीक सिग्नेचर के साथ एक फ़ंक्शन डिफाइन करते हैं और इसे उस entrypoint! मैक्रो को पास करते हैं जिसका हमने पहले उल्लेख किया था। यह फ़ंक्शन instruction data की जांच करके instruction routing लॉजिक को संभालता है ताकि यह तय किया जा सके कि किस फ़ंक्शन को कॉल करना है। हमारी native Rust Solana program series में इसे विस्तार से इम्प्लीमेंट करने का तरीका कवर किया गया है।
Anchor प्रोग्राम्स में, #[program] मैक्रो आपके लिए स्वचालित रूप से instruction processor जनरेट करता है। आपके #[program] मॉड्यूल का प्रत्येक पब्लिक फ़ंक्शन एक instruction handler बन जाता है, और Anchor फ़ंक्शन के नाम से प्राप्त एक discriminator के आधार पर राउटिंग लॉजिक जनरेट करता है। यह उन तरीकों में से एक है जिनसे Anchor बॉयलरप्लेट को एब्सट्रैक्ट करता है और प्रोग्राम डेवलपमेंट को अधिक सुविधाजनक बनाता है।
जब instruction processor Ok(()) रिटर्न करता है, तो entrypoint SUCCESS (0) रिटर्न करता है। यदि यह कोई एरर रिटर्न करता है, तो entrypoint उस एरर को एक एरर कोड में बदल देता है और उसे रिटर्न कर देता है।
यहाँ बताया गया है कि entrypoint! मैक्रो में सब कुछ एक साथ कैसे फिट होता है:
#[macro_export]
macro_rules! entrypoint {
($process_instruction:ident) => {
/// # Safety
#[no_mangle]
pub unsafe extern "C" fn entrypoint(input: *mut u8) -> u64 {
let (program_id, accounts, instruction_data) = unsafe { $crate::deserialize(input) };
match $process_instruction(program_id, &accounts, instruction_data) {
Ok(()) => $crate::SUCCESS,
Err(error) => error.into(),
}
}
$crate::custom_heap_default!();
$crate::custom_panic_default!();
};
}
मैक्रो ऐसा कोड जनरेट करता है जो तीन प्रोग्राम इनपुट्स को निकालने के लिए इनपुट को डीसीरियलाइज़ करता है (उस deserialize फ़ंक्शन का उपयोग करके जिसकी हमने पहले चर्चा की थी), उन्हें आपके process_instruction फ़ंक्शन में पास करता है, और परिणाम को संभालता है।
अंत में उन दो अन्य मैक्रो कॉल्स पर ध्यान दें: custom_heap_default!() और custom_panic_default!()। ये उस महत्वपूर्ण इन्फ्रास्ट्रक्चर को सेटअप करते हैं जिसकी आपके प्रोग्राम को रन होने के लिए आवश्यकता होती है, तो आइए देखते हैं कि वे क्या करते हैं।
Custom Heap Allocator
custom_heap_default!() मैक्रो Solana प्रोग्राम्स के लिए डिफ़ॉल्ट हीप एलोकेटर (heap allocator) सेट अप करता है। Solana प्रोग्राम्स no_std वातावरण में रन होते हैं, जिसका अर्थ है कि वे Rust की स्टैण्डर्ड लाइब्रेरी का उपयोग नहीं करते हैं। ऐसा इसलिए है क्योंकि वे Solana VM के अंदर एक्सीक्यूट होते हैं, जिसमें कोई ऑपरेटिंग सिस्टम नहीं होता है। Rust का स्टैण्डर्ड लाइब्रेरी एलोकेटर malloc और free जैसे OS सिसकॉल्स (syscalls) पर निर्भर करता है, जिन्हें Solana VM प्रदान नहीं करता है। परिणामस्वरूप, Solana प्रोग्राम्स को एक कस्टम एलोकेटर की आवश्यकता होती है जो सीधे VM की मेमोरी के साथ काम करता है। ग्लोबल एलोकेटर के बिना, कोई भी कोड जो हीप मेमोरी एलोकेट करता है (जैसे कि Vec::new(), या String::from()) कंपाइल होने में विफल हो जाएगा, क्योंकि Rust को हीप ऑपरेशन्स के लिए एक ग्लोबल एलोकेटर की आवश्यकता होती है।
इस कस्टम हीप एलोकेटर का साइज़ 32KB तय होता है। यह एक पॉइंटर को मेंटेन करके काम करता है जो प्रत्येक एलोकेशन के साथ आगे बढ़ता है। यह कभी भी एक सिंगल प्रोग्राम एक्सीक्यूशन के दौरान मेमोरी को फ्री नहीं करता है, जिससे एलोकेशन बहुत तेज़ हो जाता है। एक बार जब आप कुछ एलोकेट कर देते हैं, तो वह मेमोरी तब तक एलोकेटेड रहती है जब तक कि प्रोग्राम का एक्सीक्यूशन समाप्त नहीं हो जाता। प्रोग्राम का एक्सीक्यूशन पूरा होने के बाद, पूरे हीप को फ्री कर दिया जाता है और अगले एक्सीक्यूशन के लिए रीसेट कर दिया जाता है। यदि आप एक सिंगल एक्सीक्यूशन के दौरान कुल 32KB से अधिक एलोकेट करने का प्रयास करते हैं, तो एलोकेशन विफल हो जाता है और एक नल (null) पॉइंटर रिटर्न करता है।
यहाँ custom_panic_default!() मैक्रो का डेफिनेशन दिया गया है:
#[macro_export]
macro_rules! custom_heap_default {
() => {
#[cfg(all(not(feature = "custom-heap"), target_os = "solana"))]
#[global_allocator]
static A: $crate::BumpAllocator = unsafe {
$crate::BumpAllocator::with_fixed_address_range(
$crate::HEAP_START_ADDRESS as usize,
$crate::HEAP_LENGTH,
)
};
};
}
यह कस्टम हीप एलोकेटर केवल तभी इनेबल्ड होता है जब आपने अपने Cargo.toml में custom-heap फीचर को डिफाइन नहीं किया हो। यह आपको अपना खुद का एलोकेटर प्रदान करने की अनुमति देता है यदि यह आपकी आवश्यकताओं को पूरा नहीं करता है। व्यवहार में, इसकी शायद ही कभी आवश्यकता होती है। प्रदान किया गया एलोकेटर अधिकांश प्रोग्राम्स के लिए ठीक काम करता है। आप इसे बदलने पर केवल तभी विचार करेंगे जब आपको किसी सिंगल प्रोग्राम एक्सीक्यूशन के दौरान मेमोरी को फ्री करने और फिर से उपयोग करने की आवश्यकता हो (क्योंकि यह डिफ़ॉल्ट कस्टम Solana एलोकेटर एक्सीक्यूशन के दौरान कभी फ्री नहीं करता है), यदि आप ऐसा प्रोग्राम बना रहे हैं जिसे 32KB से अधिक हीप स्पेस की आवश्यकता है, या यदि आप परफॉरमेंस-क्रिटिकल एप्लिकेशन्स के लिए अधिक सोफिस्टिकेटेड एलोकेटर चाहते हैं।
Custom Panic Handler
custom_panic_default!() मैक्रो एक कस्टम पैनिक हैंडलर (panic handler) सेट अप करता है। हीप एलोकेटर के समान, Rust का स्टैण्डर्ड पैनिक हैंडलर OS फीचर्स (जैसे stderr और प्रोसेस टर्मिनेशन) पर निर्भर करता है जो Solana VM में मौजूद नहीं हैं। इसलिए, Solana प्रोग्राम्स को एक कस्टम पैनिक हैंडलर की आवश्यकता होती है जो एरर्स को रिपोर्ट करने के लिए Solana-विशिष्ट सिसकॉल्स (sol_log_ और sol_panic_) का उपयोग करता है। पैनिक हैंडलर को डिफाइन किए बिना, Solana प्रोग्राम्स कंपाइल नहीं होंगे क्योंकि Rust को no_std वातावरण में एक पैनिक हैंडलर निर्दिष्ट करने की आवश्यकता होती है।
यह कस्टम पैनिक हैंडलर Solana सिसकॉल्स का उपयोग करके पैनिक मैसेज और लोकेशन (फ़ाइल, लाइन, कॉलम) को लॉग करता है, फिर प्रोग्राम टर्मिनेट हो जाता है। यह कुछ इस तरह दिखता है:
#[macro_export]
macro_rules! custom_panic_default {
() => {
#[cfg(all(not(feature = "custom-panic"), target_os = "solana"))]
#[no_mangle]
fn custom_panic(info: &core::panic::PanicInfo<'_>) {
if let Some(mm) = info.message().as_str() {
unsafe {
$crate::__log(mm.as_ptr(), mm.len() as u64);
}
}
if let Some(loc) = info.location() {
unsafe {
$crate::__panic(
loc.file().as_ptr(),
loc.file().len() as u64,
loc.line() as u64,
loc.column() as u64,
)
}
}
}
};
}
हीप एलोकेटर की तरह, आप अपने Cargo.toml में custom-panic फीचर जोड़कर और अपना खुद का पैनिक हैंडलर डिफाइन करके इसे ओवरराइड कर सकते हैं। ऐसा करने का सबसे आम कारण प्रोग्राम का साइज़ कम करना है। इस कस्टम पैनिक हैंडलर में स्ट्रिंग फॉर्मेटिंग और लॉगिंग कोड शामिल होता है जो आपके कंपाइल किए गए प्रोग्राम में लगभग 25KB जोड़ता है। यदि आप प्रोग्राम साइज़ लिमिट (BPF प्रोग्राम्स के लिए 10MB) तक पहुँच रहे हैं, तो आप एक no-op पैनिक हैंडलर डिफाइन कर सकते हैं जो कुछ नहीं करता है, जिससे वह स्पेस बच जाता है। इसका ट्रेडऑफ यह है कि पैनिक साइलेंट हो जाते हैं। आपको एरर मैसेजेस दिखाई नहीं देंगे, जिससे डिबगिंग कठिन हो जाती है। अधिकांश डेवलपर्स केवल अंतिम उपाय के रूप में ऐसा करते हैं जब उन्हें प्रोग्राम के साइज़ को कम करने की आवश्यकता होती है।
यह लेख Solana development पर एक ट्यूटोरियल सीरीज़ का हिस्सा है