En la primera parte, cubrimos el formato de serialización de entrada de los programas de Solana y cómo las entradas del programa se estructuran en la memoria. En esta parte, veremos cómo los programas dirigen las entradas entrantes a los manejadores correspondientes y el código de soporte que establece el entrypoint para hacerlo posible.
El procesador de instrucciones
La función entrypoint (generada por la macro entrypoint!) deserializa el array de bytes sin procesar en program_id, accounts e instruction_data, y luego los pasa al procesador de instrucciones. El procesador de instrucciones lee la instruction_data para determinar qué manejador debe ejecutarse, de manera similar a cómo los contratos de Solidity enrutan las llamadas a funciones basándose en el selector de función.
El procesador de instrucciones debe ser definido por tu propio programa. El entrypoint no proporciona esta función, solo define la firma de tipo con la que tu procesador de instrucciones debe coincidir. Este tipo se define como:
pub type ProcessInstruction = fn(program_id: &Pubkey, accounts: &[AccountInfo], instruction_data: &[u8]) -> ProgramResult;
Tu programa debe crear una función que coincida con esta firma (toma esas tres entradas del programa y devuelve un ProgramResult), y luego pasarla a la macro entrypoint! (veremos cómo funciona esto a continuación). La forma en que creas esta función depende de si estás usando Anchor o escribiendo un programa nativo en Rust.
En los programas nativos en Rust, defines una función con esta firma exacta y la pasas a la macro entrypoint! que mencionamos anteriormente. Esta función maneja la lógica de enrutamiento de instrucciones examinando los datos de la instrucción para determinar a qué función llamar. Nuestra serie de programas nativos de Solana en Rust cubre cómo implementar esto en detalle.
En los programas de Anchor, la macro #[program] genera automáticamente el procesador de instrucciones por ti. Cada función pública en tu módulo #[program] se convierte en un manejador de instrucciones, y Anchor genera la lógica de enrutamiento basándose en un discriminador derivado del nombre de la función. Esta es una de las formas en que Anchor abstrae el código repetitivo y hace que el desarrollo de programas sea más conveniente.
Cuando el procesador de instrucciones devuelve Ok(()), el entrypoint devuelve SUCCESS (0). Si devuelve un error, el entrypoint convierte ese error en un código de error y lo devuelve.
Así es como encaja todo en la macro 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!();
};
}
La macro genera código que deserializa la entrada (usando la función deserialize que discutimos anteriormente) para extraer las tres entradas del programa, las pasa a tu función process_instruction y maneja el resultado.
Nota esas otras dos llamadas a macros al final: custom_heap_default!() y custom_panic_default!(). Estas configuran la infraestructura crítica que tu programa necesita para ejecutarse, así que veamos qué hacen.
Asignador de Heap personalizado
La macro custom_heap_default!() configura el asignador de heap predeterminado para los programas de Solana. Los programas de Solana se ejecutan en un entorno no_std, lo que significa que no usan la biblioteca estándar de Rust. Esto se debe a que se ejecutan dentro de la VM de Solana, la cual no tiene sistema operativo. El asignador de la biblioteca estándar de Rust depende de llamadas al sistema del SO como malloc y free, que la VM de Solana no proporciona. En consecuencia, los programas de Solana necesitan un asignador personalizado que trabaje directamente con la memoria de la VM. Sin un asignador global, cualquier código que asigne memoria en el heap (como Vec::new(), o String::from()) fallará al compilar, ya que Rust requiere un asignador global para las operaciones del heap.
Este asignador de heap personalizado tiene un tamaño fijo de 32KB. Funciona manteniendo un puntero que avanza con cada asignación. Nunca libera memoria durante una única ejecución del programa, lo que hace que las asignaciones sean muy rápidas. Una vez que asignas algo, esa memoria permanece asignada hasta que el programa termina de ejecutarse. Después de que se completa la ejecución del programa, todo el heap se libera y se reinicia para la siguiente ejecución. Si intentas asignar más de 32KB en total durante una sola ejecución, la asignación falla y devuelve un puntero nulo.
Aquí está la definición de la macro 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,
)
};
};
}
Este asignador de heap personalizado solo se habilita si no has definido una característica custom-heap en tu Cargo.toml. Esto te permite proporcionar tu propio asignador si este no satisface tus necesidades. En la práctica, esto rara vez es necesario. El asignador proporcionado funciona bien para la mayoría de los programas. Solo considerarías reemplazarlo si necesitas liberar y reutilizar memoria durante una sola ejecución del programa (ya que este asignador personalizado predeterminado de Solana nunca libera memoria durante la ejecución), si estás construyendo un programa que necesita más de 32KB de espacio en el heap, o si deseas un asignador más sofisticado para aplicaciones críticas en cuanto a rendimiento.
Manejador de Panic personalizado
La macro custom_panic_default!() configura un manejador de panic personalizado. De manera similar al asignador de heap, el manejador de panic estándar de Rust depende de características del sistema operativo (como stderr y la finalización de procesos) que no existen en la VM de Solana. Por lo tanto, los programas de Solana necesitan un manejador de panic personalizado que use llamadas al sistema específicas de Solana (sol_log_ y sol_panic_) para reportar errores. Sin definir un manejador de panic, los programas de Solana no compilarán porque Rust requiere que se especifique uno en entornos no_std.
Este manejador de panic personalizado registra el mensaje de panic y la ubicación (archivo, línea, columna) utilizando llamadas al sistema de Solana, y luego el programa termina. Así es como se ve:
#[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,
)
}
}
}
};
}
Al igual que el asignador de heap, puedes anular esto agregando una característica custom-panic a tu Cargo.toml y definiendo tu propio manejador de panic. La razón más común para hacer esto es reducir el tamaño del programa. Este manejador de panic personalizado incluye código de registro y formateo de cadenas que añade alrededor de 25KB a tu programa compilado. Si estás alcanzando el límite de tamaño del programa (10MB para programas BPF), puedes definir un manejador de panic no-op que no haga nada, lo cual ahorra ese espacio. La contrapartida es que los panics se vuelven silenciosos. No verás mensajes de error, lo que dificulta la depuración. La mayoría de los desarrolladores solo hacen esto como último recurso cuando necesitan reducir al máximo el tamaño del programa.
Este artículo es parte de una serie de tutoriales sobre desarrollo en Solana