एक Library call घोषित contract class के लॉजिक को उस contract के कॉन्टेक्स्ट (context) और स्टोरेज (storage) में निष्पादित (execute) करता है जो इसे इनवोक (invoke) करता है। यह Solidity के delegatecall के समान है लेकिन यह डिप्लॉयड contract एड्रेस के बजाय class hash का उपयोग करता है।
इस आर्टिकल में, आप विस्तार से जानेंगे कि Library calls कैसे काम करते हैं और Cairo contracts में इसके दो इम्प्लीमेंटेशन अप्रोच (implementation approaches) क्या हैं।
Library calls कैसे काम करते हैं
दो contracts वाले इस कोड उदाहरण पर विचार करें: CallerContract और CalledContract।
CalledContract एक contract class है जो रीयूजेबल (reusable) लॉजिक को परिभाषित करता है जिसे डिप्लॉयड contracts Library calls के माध्यम से निष्पादित कर सकते हैं। इसमें एक add() फंक्शन है जो दो नंबर लेता है और उनका योग (sum) लौटाता है:
#[starknet::interface]
pub trait ICalledContract<TContractState> {
fn add(self: @TContractState, a: u32, b: u32) -> u32;
}
#[starknet::contract]
mod CalledContract {
#[storage]
struct Storage {
// no storage needed
}
#[abi(embed_v0)]
impl CalledContractImpl of super::ICalledContract<ContractState> {
fn add(self: @ContractState, a: u32, b: u32) -> u32 {
a + b
}
}
}
CallerContract एक डिप्लॉयड contract है जो CalledContract का class hash स्टोर करता है। जब इसके calculate() फंक्शन को कॉल किया जाता है, तो यह एक Library call के माध्यम से CalledContract की क्लास से add() फंक्शन को निष्पादित करता है और फिर परिणाम को अपने स्वयं के स्टोरेज में स्टोर करता है:
#[starknet::interface]
pub trait ICallerContract<TContractState> {
fn calculate(ref self: TContractState, a: u32, b: u32) -> u32;
}
#[starknet::contract]
mod CallerContract {
use starknet::{ClassHash, get_caller_address, get_contract_address};
use starknet::storage::{StoragePointerWriteAccess};
#[storage]
struct Storage {
result: u32, // CallerContract's storage
called_class: ClassHash,
}
#[constructor]
fn constructor(ref self: ContractState, called_class_hash: ClassHash) {
self.called_class.write(called_class_hash);
}
#[abi(embed_v0)]
impl CallerContractImpl of super::ICallerContract<ContractState> {
fn calculate(ref self: ContractState, a: u32, b: u32) -> u32 {
let caller = get_caller_address();
let this = get_contract_address();
// Execute add() from CalledContract via library call
// Library call details will be shown later
let sum = // ... library call to add(a, b) ...
// Store result in CallerContract's storage
self.result.write(sum);
sum
}
}
}
यहाँ एक डायग्राम दिया गया है जो दर्शाता है कि कैसे CallerContract एक Library call के माध्यम से CalledContract से कोड निष्पादित करता है:

Library calls कॉलर के कॉन्टेक्स्ट को संरक्षित (preserve) करते हैं
जब कोई यूजर CallerContract.calculate() को कॉल करता है, तो CallerContract के अंदर:
get_caller_address()यूजर का एड्रेस लौटाता हैget_contract_address()CallerContractका एड्रेस लौटाता है
जब CallerContract एक Library call के माध्यम से CalledContract की क्लास से add() फंक्शन को निष्पादित करता है, तो निष्पादन (execution) CallerContract के कॉन्टेक्स्ट में ही रहता है। इसका मतलब है:
get_caller_address()अभी भी यूजर का एड्रेस लौटाता है (मूल कॉल से संरक्षित)get_contract_address()अभी भीCallerContractका एड्रेस लौटाता है- स्टोरेज अपडेट्स
CallerContractके स्टोरेज में होते हैं (resultफील्ड मॉडिफाई हो जाती है)
CalledContract का कोड इस तरह निष्पादित होता है जैसे कि वह सीधे CallerContract के अंदर लिखा गया हो।
get_contract_address()हमेशा उस contract का एड्रेस लौटाता है जिसका कॉन्टेक्स्ट एक्टिव है, न कि आवश्यक रूप से उसका जिसका कोड चल रहा है।
Solidity के delegatecall के साथ Library call की तुलना:
| Aspect | Cairo library call | Solidity DELEGATECALL |
|---|---|---|
| Target | Contract class (घोषित class hash) | डिप्लॉयड लाइब्रेरी contract |
| Mechanism | library_call_syscall |
DELEGATECALL opcode |
| Context | कॉलर का कॉन्टेक्स्ट | कॉलर का कॉन्टेक्स्ट |
| Storage Modified | कॉलर का स्टोरेज | कॉलर का स्टोरेज |
| msg.sender equivalent | ओरिजिनल यूजर का एड्रेस संरक्षित | ओरिजिनल msg.sender संरक्षित |
सामान्य cross-contract calls से मुख्य अंतर यह है कि किसका स्टोरेज अपडेट होता है और किसके कॉन्टेक्स्ट में कोड निष्पादित होता है।
Library calls करने के तरीके
Starknet पर Library calls करने के दो तरीके हैं:
- Library dispatcher का उपयोग करके
- सीधे
library_call_syscallका उपयोग करके
आइए इनमें से प्रत्येक को समझते हैं।
1. Library Dispatcher का उपयोग करना
Library dispatcher एक कंपाइलर-जेनरेटेड स्ट्रक्ट (struct) है जो contract classes के लिए टाइप-सेफ Library calls को सक्षम बनाता है। यह एक ClassHash को रैप (wrap) करता है और उस ट्रेट (trait) को इम्प्लीमेंट करता है जिसे कंपाइलर आपके #[starknet::interface] से जेनरेट करता है।
जब आप library dispatcher के माध्यम से किसी फंक्शन को कॉल करते हैं, तो आप इसे एक सामान्य फंक्शन की तरह इनवोक करते हैं। आंतरिक रूप से (Internally), dispatcher:
- कंपाइल टाइम (compile time) पर फंक्शन के नाम से फंक्शन सिलेक्टर (function selector) की गणना करता है
- फंक्शन आर्ग्यूमेंट्स को
felt252वैल्यूज़ में सीरियलाइज़ (serialize) करता है - क्लास हैश, फंक्शन सिलेक्टर, और सीरियलाइज़्ड आर्ग्यूमेंट्स के साथ कॉल को निष्पादित करने के लिए
library_call_syscallका उपयोग करता है - वापस आए
Span<felt252>को अपेक्षित Cairo टाइप्स में डिसीरियलाइज़ (deserialize) करता है
Contract dispatchers (जो cross-contract calls में उपयोग होते हैं) की तरह, library dispatchers दो वेरिएंट्स में आते हैं:
- Regular library dispatcher: Library calls करता है और यदि कॉल पैनिक (panic) हो जाती है, तो संपूर्ण ट्रांजेक्शन को रिवर्ट (revert) कर देता है
- Safe library dispatcher: Library calls करता है और
Result<T, Array<felt252>>लौटाता है, जिससे आप बिना रिवर्ट किए फेलियर्स (failures) को हैंडल कर सकते हैं। हालांकि, कुछ सिस्टम-लेवल फेलियर्स जैसे कि ऑन-चेन मौजूद न होने वाले क्लास हैश का उपयोग करना और लिगेसी Cairo Zero क्लासेस में एरर्स अभी भी तत्काल ट्रांजेक्शन रिवर्ट का कारण बनते हैं जिन्हें कैच (catch) नहीं किया जा सकता।
आइए देखें कि कैसे एक Calculator contract MathUtils क्लास से कैलकुलेशन फंक्शन्स को निष्पादित करने के लिए library dispatcher का उपयोग करता है। हम एक MathUtils क्लास बनाएंगे जो रीयूजेबल कोड को परिभाषित करती है। इस क्लास को ऑन-चेन घोषित (declare) किया जाता है लेकिन कभी भी contract इंस्टेंस के रूप में डिप्लॉय नहीं किया जाता है, इसलिए कोई स्टोरेज आवंटित नहीं किया जाता है:
#[starknet::interface]
trait IMathUtils<TContractState> {
fn add(self: @TContractState, a: u256, b: u256) -> u256;
fn multiply(self: @TContractState, a: u256, b: u256) -> u256;
}
// Math utilities class (declared but never deployed)
#[starknet::contract]
mod MathUtils {
#[storage]
struct Storage {
// no storage needed
}
#[abi(embed_v0)]
impl MathUtilsImpl of super::IMathUtils<ContractState> {
fn add(self: @ContractState, a: u256, b: u256) -> u256 {
a + b
}
fn multiply(self: @ContractState, a: u256, b: u256) -> u256 {
a * b
}
}
}
// Calculator contract (deployed instance that uses MathUtils)
#[starknet::interface]
trait ICalculator<TContractState> {
fn add(ref self: TContractState, a: u256, b: u256) -> u256;
fn multiply(ref self: TContractState, a: u256, b: u256) -> u256;
fn get_result(self: @TContractState) -> u256;
}
#[starknet::contract]
mod Calculator {
use starknet::storage::{StoragePointerReadAccess, StoragePointerWriteAccess};
use starknet::ClassHash;
use super::{IMathUtilsDispatcherTrait, IMathUtilsLibraryDispatcher};
#[storage]
struct Storage {
math_class: ClassHash,
result: u256, // Calculator's storage
}
#[constructor]
fn constructor(ref self: ContractState, math_class: ClassHash) {
self.math_class.write(math_class);
}
#[abi(embed_v0)]
impl CalculatorImpl of super::ICalculator<ContractState> {
fn add(ref self: ContractState, a: u256, b: u256) -> u256 {
// Executes MathUtils add() in Calculator's context
let sum = IMathUtilsLibraryDispatcher { class_hash: self.math_class.read() }
.add(a, b);
// Calculator stores the result
self.result.write(sum);
sum
}
fn multiply(ref self: ContractState, a: u256, b: u256) -> u256 {
// Executes MathUtils multiply() in Calculator's context
let product = IMathUtilsLibraryDispatcher { class_hash: self.math_class.read() }
.multiply(a, b);
// Calculator stores the result
self.result.write(product);
product
}
fn get_result(self: @ContractState) -> u256 {
self.result.read()
}
}
}
Calculator contract में, हम IMathUtils इंटरफेस से ऑटो-जेनरेटेड dispatcher टाइप्स को इम्पोर्ट करते हैं:
use super::{IMathUtilsDispatcherTrait, IMathUtilsLibraryDispatcher};
फिर, जब हम MathUtils से कोड निष्पादित करना चाहते हैं, तो हम क्लास हैश के साथ एक dispatcher इंस्टेंस बनाते हैं और फंक्शन को कॉल करते हैं। उदाहरण के लिए, add फंक्शन में:
let sum = IMathUtilsLibraryDispatcher { class_hash: self.math_class.read() }
.add(a, b)
यह dispatcher इंस्टेंस बनाता है और तुरंत MathUtils से add फंक्शन को कॉल करता है।
जब हम Calculator.add(5, 3) को कॉल करते हैं, तो यह MathUtils क्लास को एक Library call करता है जो add फंक्शन को निष्पादित करता है। कैलकुलेशन Calculator के कॉन्टेक्स्ट में होता है, और Calculator परिणाम (8) को अपने स्वयं के स्टोरेज में स्टोर करता है।
जब हम Calculator.multiply(4, 2) को कॉल करते हैं, तो यह MathUtils से multiply फंक्शन को निष्पादित करता है, फिर गुणनफल (product) (8) को Calculator के स्टोरेज में स्टोर करता है।
get_result() फंक्शन सीधे Calculator के स्टोरेज से पढ़ता है, जो भी अंतिम वैल्यू स्टोर की गई थी उसे लौटाता है।
यह दर्शाता है कि कैसे Library calls अलग-अलग contract इंस्टेंस को डिप्लॉय किए बिना कोड रीयूज (code reuse) को सक्षम करते हैं। Calculator MathUtils क्लास के लॉजिक को इस तरह निष्पादित करता है जैसे कि यह उसके अपने कोड का हिस्सा हो।
2. सीधे library_call_syscall का उपयोग करना
चूँकि library dispatcher आंतरिक रूप से (under the hood) library_call_syscall का उपयोग करता है, इसलिए जब आपको मैन्युअल सीरियलाइज़ेशन हैंडलिंग की आवश्यकता हो तो आप सीधे इस सिसकॉल (syscall) को कॉल कर सकते हैं।
जब आप सीधे library_call_syscall का उपयोग करते हैं तो Calculator contract कुछ इस तरह दिखता है:
#[starknet::interface]
pub trait ICalculator<TContractState> {
fn add_direct(ref self: TContractState, a: u256, b: u256) -> u256;
fn get_result(self: @TContractState) -> u256;
}
#[starknet::contract]
mod Calculator {
use starknet::storage::{StoragePointerReadAccess, StoragePointerWriteAccess};
use starknet::syscalls::library_call_syscall;
use starknet::{ClassHash, SyscallResultTrait};
#[storage]
struct Storage {
math_class: ClassHash,
result: u256,
}
#[constructor]
fn constructor(ref self: ContractState, math_class: ClassHash) {
self.math_class.write(math_class);
}
#[abi(embed_v0)]
impl CalculatorImpl of super::ICalculator<ContractState> {
fn add_direct(ref self: ContractState, a: u256, b: u256) -> u256 {
// Manually serialize function arguments
let mut calldata: Array<felt252> = array![];
Serde::serialize(@a, ref calldata);
Serde::serialize(@b, ref calldata);
// Make the direct library syscall
let mut res = library_call_syscall(
self.math_class.read(),
selector!("add"),
calldata.span(),
).unwrap_syscall();
// Manually deserialize the response
let sum = Serde::<u256>::deserialize(ref res).unwrap();
// Store the result
self.result.write(sum);
sum
}
fn get_result(self: @ContractState) -> u256 {
self.result.read()
}
}
}
add_direct फंक्शन डायरेक्ट Library calls करने की तीन-चरणीय (three-step) प्रक्रिया को दर्शाता है:
-
मैन्युअल सीरियलाइज़ेशन (Manual Serialization): हम एक खाली ऐरे (array) बनाते हैं और
Serde::serialize()का उपयोग करके प्रत्येक पैरामीटर (aऔरb) कोfelt252वैल्यूज़ में सीरियलाइज़ करते हैं। यह हमारेu256पैरामीटर्स को उस लो-लेवल (low-level) फॉर्मेट में बदल देता है जिसकी syscall अपेक्षा करता है। -
डायरेक्ट Library Syscall (Direct Library Syscall): हम तीन पैरामीटर्स के साथ
library_call_syscallको कॉल करते हैं:MathUtilsका क्लास हैश (स्टोरेज से प्राप्त किया गया)- फंक्शन सिलेक्टर (
"add") - सीरियलाइज़्ड कॉलाडेटा (calldata)
-
मैन्युअल डिसीरियलाइज़ेशन (Manual Deserialization): syscall रॉ (raw)
felt252डेटा लौटाता है, जिसे हम मैन्युअल रूप सेSerde::<u256>::deserialize()का उपयोग करके वापसu256में डिसीरियलाइज़ करते हैं।
जब यह फंक्शन निष्पादित होता है, तो यह Calculator के कॉन्टेक्स्ट में MathUtils का कोड चलाता है। Calculator फिर परिणाम को अपने स्वयं के स्टोरेज में स्टोर करता है।
सीधे library_call_syscall का उपयोग करने से स्पष्ट (explicit) सीरियलाइज़ेशन हैंडलिंग की अनुमति मिलती है लेकिन library dispatcher का उपयोग करने की तुलना में अधिक कोड की आवश्यकता होती है।
डायरेक्ट लो-लेवल syscalls का उपयोग केवल तभी किया जाना चाहिए जब आपको मैन्युअल रूप से सीरियलाइज़ेशन को हैंडल करने की आवश्यकता हो या जब रनटाइम (runtime) पर फंक्शन सिलेक्टर निर्धारित किया जाना चाहिए। Library dispatcher को कंपाइल-टाइम पर यह जानने की आवश्यकता होती है कि कौन सा फंक्शन कॉल करना है (जैसे, dispatcher.add()), जो इसे उन मामलों के लिए अनुपयुक्त बनाता है जहां फंक्शन यूजर इनपुट या contract स्टेट पर निर्भर करता है। ऐसे परिदृश्यों में, आप सीधे library_call_syscall का उपयोग करते हैं।