库调用(library call)在调用它的合约的上下文和存储中执行已声明合约类(contract class)的逻辑。这类似于 Solidity 的 delegatecall,但使用的是类哈希(class hashes)而不是已部署的合约地址。
在本文中,您将详细了解库调用的工作原理以及在 Cairo 合约中的两种实现方法。
库调用如何工作
考虑这个包含两个合约的代码示例:CallerContract 和 CalledContract。
CalledContract 是一个合约类,定义了可重用的逻辑,已部署的合约可以通过库调用来执行它。它包含一个 add() 函数,该函数接收两个数字并返回它们的和:
#[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 是一个已部署的合约,它存储了 CalledContract 的类哈希。当它的 calculate() 函数被调用时,它会通过库调用执行 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 如何通过库调用执行 CalledContract 中的代码:

库调用保留调用者的上下文
当用户调用 CallerContract.calculate() 时,在 CallerContract 内部:
get_caller_address()返回用户的地址get_contract_address()返回CallerContract的地址
当 CallerContract 通过库调用执行 CalledContract 类的 add() 函数时,执行仍留在 CallerContract 的上下文中。这意味着:
get_caller_address()依然返回用户的地址(从原始调用中保留下来)get_contract_address()依然返回CallerContract的地址- 存储更新发生在
CallerContract的存储中(result字段被修改)
CalledContract 中的代码就像直接写在 CallerContract 内部一样被执行。
get_contract_address()始终返回其上下文处于活跃状态的合约地址,而不一定是正在运行其代码的合约地址。
库调用与 Solidity 的 delegatecall 比较:
| 比较维度 | Cairo 库调用 | Solidity DELEGATECALL |
|---|---|---|
| 目标 (Target) | 合约类(声明的类哈希) | 已部署的库合约 |
| 机制 (Mechanism) | library_call_syscall |
DELEGATECALL 操作码 |
| 上下文 (Context) | 调用者的上下文 | 调用者的上下文 |
| 存储修改 (Storage Modified) | 调用者的存储 | 调用者的存储 |
| msg.sender 等价物 | 保留原始用户地址 | 保留原始 msg.sender |
它与常规跨合约调用的关键区别在于,谁的存储被更新,以及代码在谁的上下文中执行。
发起库调用的方式
在 Starknet 上有两种发起库调用的方式:
- 使用库调度器(library dispatcher)
- 直接使用
library_call_syscall
让我们逐一了解它们。
1. 使用库调度器
库调度器是编译器生成的结构体,它允许对合约类进行类型安全的库调用。它封装了一个 ClassHash,并实现了编译器根据你的 #[starknet::interface] 生成的 trait。
当你通过库调度器调用函数时,只需像调用常规函数一样调用它。在内部,调度器会:
- 在编译时根据函数名计算出函数选择器
- 将函数参数序列化为
felt252值 - 使用
library_call_syscall传入类哈希、函数选择器和序列化后的参数来执行调用 - 将返回的
Span<felt252>反序列化回预期的 Cairo 类型
就像合约调度器(用于跨合约调用)一样,库调度器也有两种变体:
- 常规库调度器:发起库调用,如果调用发生 panic,则回滚(revert)整个交易
- 安全库调度器:发起库调用并返回
Result<T, Array<felt252>>,允许你在不回滚的情况下处理失败。然而,某些系统级失败(例如使用了链上不存在的类哈希,或传统的 Cairo Zero 类中的错误)仍然会导致无法被捕获的立即交易回滚。
让我们看看 Calculator 合约如何使用库调度器来执行 MathUtils 类中的计算函数。我们将创建一个定义了可重用代码的 MathUtils 类。该类在链上声明,但从未作为合约实例部署,因此不会分配任何存储空间:
#[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 合约中,我们从 IMathUtils 接口引入自动生成的调度器类型:
use super::{IMathUtilsDispatcherTrait, IMathUtilsLibraryDispatcher};
然后,当我们需要执行 MathUtils 中的代码时,我们会使用该类哈希创建一个调度器实例并调用该函数。例如,在 add 函数中:
let sum = IMathUtilsLibraryDispatcher { class_hash: self.math_class.read() }
.add(a, b)
这会创建调度器实例,并立即调用 MathUtils 中的 add 函数。
当我们调用 Calculator.add(5, 3) 时,它会对 MathUtils 类进行库调用以执行 add 函数。计算发生在 Calculator 的上下文中,并且 Calculator 将结果 (8) 存储在它自己的存储中。
当我们调用 Calculator.multiply(4, 2) 时,它会执行 MathUtils 的 multiply 函数,然后将乘积 (8) 存储在 Calculator 的存储中。
get_result() 函数直接从 Calculator 的存储中读取,返回最后存储的任何值。
这展示了库调用如何在无需部署单独的合约实例的情况下实现代码重用。Calculator 执行 MathUtils 类中的逻辑,就像它是自身代码的一部分一样。
2. 直接使用 library_call_syscall
由于库调度器在底层使用了 library_call_syscall,当需要手动处理序列化时,你也可以直接调用这个系统调用(syscall)。
以下是直接使用 library_call_syscall 时 Calculator 合约的样子:
#[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 函数展示了进行直接库调用的三步过程:
-
手动序列化:我们创建一个空数组,并使用
Serde::serialize()将每个参数(a和b)序列化为felt252值。这会将我们的u256参数转换为系统调用预期的低级格式。 -
直接的库系统调用:我们使用三个参数调用
library_call_syscall:MathUtils的类哈希(从存储中检索)- 函数选择器(
"add") - 序列化后的 calldata
-
手动反序列化:系统调用返回原始的
felt252数据,我们使用Serde::<u256>::deserialize()手动将其反序列化回u256类型。
当此函数执行时,它在 Calculator 的上下文中运行 MathUtils 的代码。然后 Calculator 会将结果存储在自己的存储中。
直接使用 library_call_syscall 允许显式的序列化处理,但与使用库调度器相比,需要编写更多的代码。
只有在需要手动处理序列化,或者必须在运行时确定函数选择器时,才应该使用直接的底层系统调用。库调度器要求在编译时明确知道要调用哪个函数(例如,dispatcher.add()),这使得它不适用于函数取决于用户输入或合约状态的情况。在这些场景中,您需要直接使用 library_call_syscall。