Una llamada a biblioteca (library call) ejecuta la lógica de una clase de contrato declarada en el contexto y almacenamiento del contrato que la invoca. Esto es similar a delegatecall de Solidity, pero utiliza hashes de clase en lugar de direcciones de contratos desplegados.
En este artículo, aprenderás cómo funcionan en detalle las llamadas a biblioteca y los dos enfoques de implementación en los contratos de Cairo.
Cómo funcionan las llamadas a biblioteca
Considera este ejemplo de código con dos contratos: CallerContract y CalledContract.
CalledContract es una clase de contrato que define lógica reutilizable que los contratos desplegados pueden ejecutar mediante llamadas a biblioteca. Tiene una función add() que toma dos números y devuelve su suma:
#[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 es un contrato desplegado que almacena el class hash de CalledContract. Cuando se llama a su función calculate(), ejecuta la función add() de la clase de CalledContract a través de una llamada a biblioteca y luego almacena el resultado en su propio almacenamiento:
#[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
}
}
}
Aquí tienes un diagrama que muestra cómo CallerContract ejecuta el código de CalledContract a través de una llamada a biblioteca:

Las llamadas a biblioteca preservan el contexto del llamador
Cuando un usuario llama a CallerContract.calculate(), dentro de CallerContract:
get_caller_address()devuelve la dirección del usuarioget_contract_address()devuelve la dirección deCallerContract
Cuando CallerContract ejecuta la función add() de la clase de CalledContract a través de una llamada a biblioteca, la ejecución permanece en el contexto de CallerContract. Esto significa que:
get_caller_address()sigue devolviendo la dirección del usuario (preservada de la llamada original)get_contract_address()sigue devolviendo la dirección deCallerContract- Las actualizaciones de almacenamiento ocurren en el almacenamiento de
CallerContract(el camporesultes el que se modifica)
El código de CalledContract se ejecuta como si estuviera escrito directamente dentro de CallerContract.
get_contract_address()siempre devuelve la dirección del contrato cuyo contexto está activo, no necesariamente del que se está ejecutando el código.
Comparación de la llamada a biblioteca con delegatecall de Solidity:
| Aspecto | Llamada a biblioteca en Cairo | DELEGATECALL de Solidity |
|---|---|---|
| Objetivo | Clase de contrato (class hash declarado) | Contrato de biblioteca desplegado |
| Mecanismo | library_call_syscall |
Opcode DELEGATECALL |
| Contexto | Contexto del llamador | Contexto del llamador |
| Almacenamiento modificado | Almacenamiento del llamador | Almacenamiento del llamador |
| Equivalente a msg.sender | Dirección del usuario original preservada | msg.sender original preservado |
La diferencia clave con respecto a las llamadas regulares entre contratos es qué almacenamiento se actualiza y en qué contexto se ejecuta el código.
Formas de hacer llamadas a biblioteca
Hay dos formas de hacer llamadas a biblioteca en Starknet:
- Usando el despachador de biblioteca (library dispatcher)
- Usando el
library_call_syscalldirectamente
Repasemos cada una de ellas.
1. Usando el despachador de biblioteca
Un despachador de biblioteca es un struct generado por el compilador que permite realizar llamadas a biblioteca con seguridad de tipos (type-safe) a clases de contratos. Envuelve un ClassHash e implementa el trait que el compilador genera a partir de tu #[starknet::interface].
Cuando llamas a una función a través de un despachador de biblioteca, simplemente la invocas como una función normal. Internamente, el despachador:
- calcula el selector de función a partir del nombre de la función en tiempo de compilación
- serializa los argumentos de la función en valores
felt252 - utiliza
library_call_syscallpara ejecutar la llamada con el class hash, el selector de función y los argumentos serializados - deserializa el
Span<felt252>devuelto de nuevo a los tipos esperados de Cairo
Al igual que los despachadores de contrato (usados en llamadas entre contratos), los despachadores de biblioteca vienen en dos variantes:
- Despachador de biblioteca regular: Realiza llamadas a biblioteca y revierte la transacción completa si la llamada entra en pánico
- Despachador de biblioteca seguro: Realiza llamadas a biblioteca y devuelve
Result<T, Array<felt252>>, lo que te permite manejar fallos sin revertir. Sin embargo, ciertos fallos a nivel del sistema, como usar un class hash que no existe en cadena (on-chain) y errores en clases heredadas (legacy) de Cairo Zero, siguen provocando reversiones inmediatas de la transacción que no se pueden capturar.
Examinemos cómo un contrato Calculator usa el despachador de biblioteca para ejecutar funciones de cálculo de una clase MathUtils. Crearemos una clase MathUtils que define código reutilizable. Esta clase se declara en la cadena pero nunca se despliega como una instancia de contrato, por lo que no se le asigna almacenamiento:
#[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()
}
}
}
En el contrato Calculator, importamos los tipos de despachadores generados automáticamente desde la interfaz IMathUtils:
use super::{IMathUtilsDispatcherTrait, IMathUtilsLibraryDispatcher};
Luego, cuando queremos ejecutar código de MathUtils, creamos una instancia del despachador con el class hash y llamamos a la función. Por ejemplo, en la función add:
let sum = IMathUtilsLibraryDispatcher { class_hash: self.math_class.read() }
.add(a, b)
Esto crea la instancia del despachador y llama inmediatamente a la función add de MathUtils.
Cuando llamamos a Calculator.add(5, 3), se realiza una llamada a biblioteca a la clase MathUtils que ejecuta la función add. El cálculo ocurre en el contexto de Calculator, y Calculator almacena el resultado (8) en su propio almacenamiento.
Cuando llamamos a Calculator.multiply(4, 2), se ejecuta la función multiply de MathUtils, luego almacena el producto (8) en el almacenamiento de Calculator.
La función get_result() lee directamente del almacenamiento de Calculator, devolviendo cualquier valor que se haya almacenado por última vez.
Esto muestra cómo las llamadas a biblioteca permiten la reutilización de código sin desplegar instancias de contratos separadas. Calculator ejecuta la lógica de la clase MathUtils como si fuera parte de su propio código.
2. Usando el library_call_syscall directamente
Dado que el despachador de biblioteca usa library_call_syscall internamente, también puedes llamar a esta syscall directamente cuando necesites un manejo manual de la serialización.
Así es como se ve el contrato Calculator cuando usas library_call_syscall directamente:
#[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()
}
}
}
La función add_direct muestra el proceso de tres pasos para realizar llamadas a biblioteca directas:
-
Serialización manual: Creamos un array vacío y serializamos cada parámetro (
ayb) en valoresfelt252usandoSerde::serialize(). Esto convierte nuestros parámetrosu256al formato de bajo nivel que espera la syscall. -
Syscall directa a biblioteca: Llamamos a
library_call_syscallcon tres parámetros:- El class hash de
MathUtils(obtenido del almacenamiento) - El selector de función (
"add") - Los calldata serializados
- El class hash de
-
Deserialización manual: La syscall devuelve datos
felt252en crudo, que nosotros deserializamos manualmente de vuelta a unu256usandoSerde::<u256>::deserialize().
Cuando esta función se ejecuta, corre el código de MathUtils dentro del contexto de Calculator. Calculator luego almacena el resultado en su propio almacenamiento.
Usar library_call_syscall directamente permite el manejo explícito de la serialización, pero requiere más código que usar el despachador de biblioteca.
Las syscalls directas de bajo nivel solo deben usarse cuando necesitas manejar la serialización manualmente o cuando el selector de función debe determinarse en tiempo de ejecución. El despachador de biblioteca requiere conocimiento en tiempo de compilación de qué función llamar (por ejemplo, dispatcher.add()), lo que lo hace inadecuado para casos donde la función depende de la entrada del usuario o del estado del contrato. En tales escenarios, usas library_call_syscall directamente.