El manejo de señales en sistemas operativos tipo Unix (como SIGINT para Ctrl+C o SIGTERM) representa un desafío crítico en Rust debido a la propiedad de async-signal-safety. Cuando una señal llega, el kernel interrumpe la ejecución del hilo en cualquier punto exacto para ejecutar el signal handler. Si ese hilo estaba en medio de una operación que requiere un bloqueo (como una asignación de memoria en el heap o la adquisición de un Mutex), y el manejador de señales intenta realizar la misma operación, se produce un deadlock inmediato o corrupción de memoria.

Por esta razón, la biblioteca estándar de Rust (std) no proporciona una interfaz directa para registrar manejadores de señales, ya que la mayoría de las funciones de std no son seguras para ser llamadas desde un contexto de señal. El patrón estándar y seguro para abordar esto es el uso de un flag atómico (AtomicBool). El manejador de señal (escrito en código de bajo nivel mediante libc) simplemente cambia el estado del flag, y el hilo principal, que es el dueño del bucle de ejecución, consulta dicho flag de forma segura en cada iteración.

Por otro lado, los panics son interrupciones de la ejecución del programa dentro del modelo de ejecución de Rust. Para un control avanzado, existen tres mecanismos: std::panic::set_hook para definir una acción global (como escribir logs en disco) justo antes de que el hilo comience el unwinding; std::panic::take_hook para interceptar y reemplazar el comportamiento por defecto; y std::panic::catch_unwind para capturar un pánico en un límite específico (como una tarea de un hilo o un borde de FFI), evitando que el pánico propague y destruya todo el proceso. Es vital recordar que nunca se debe permitir que un panic cruce una frontera de FFI (Foreign Function Interface), ya que esto resulta en comportamiento indefinido, ya que el lenguaje de destino (como C) no sabe cómo manejar el desapilado de marcos de Rust.

// Requiere la dependencia: libc = "0.2"
use std::sync::atomic::{AtomicBool, Ordering};
use std::panic;
use std::thread;
use std::time::Duration;
use libc::{self, signal, SIGINT};

// Usamos un AtomicBool global para comunicación segura entre 
// el manejador de señal (contexto de interrupción) y el hilo principal.
static RUNNING: AtomicBool = AtomicBool::new(true);

/// El manejador de señal debe ser 'extern "C"' para cumplir con la convención de llamada
/// de la ABI de C y solo debe realizar operaciones 'async-signal-safe'.
/// Cambiar un flag atómico es una operación segura para señales.
extern "C" fn handle_sigint(_: i32) {
    RUNNING.store(false, Ordering::SeqCst);
}

fn main() {
    // 1. Configuración del Panic Hook global.
    // Esto se ejecutará antes de que el hilo empiece el unwinding.
    panic::set_hook(Box::new(|panic_info| {
        eprintln!("[LOG CRÍTICO] El programa ha entrado en estado de pánico: {}", panic_info));
    }));

    // 2. Registro del manejador de señal mediante FFI.
    // 'signal' es una función de C que requiere un bloque unsafe.
    unsafe {
        if signal(SIGINT, handle_sigint) == -1 {
            // Si el registro falla, abortamos porque no podemos asegurar la salida limpia.
            panic!("Error crítico: No se pudo registrar el manejador de SIGINT");
        }
    }

    println!("Aplicación en ejecución. Presione Ctrl+C para salir limpiamente.");

    // 3. El bucle principal consulta el estado del flag atómico.
    while RUNNING.load(Ordering::SeqCst) {
        println!("Procesando datos...");
        thread::sleep(Duration::from_secs(1));

        // 4. Ejemplo de captura de pánico local con catch_unwind.
        // Se usa para evitar que un error en una unidad lógica detenga todo el programa.
        let result = panic::catch_unwind(|| {
            // Simulamos una condición que podría disparar un pánico
            // En un escenario real, esto sería código de lógica de negocio.
            // panic!("Error controlado en un componente!"); 
        });

        if result.is_err() {
            println!("Se capturó un pánico local, pero el bucle principal continúa.");
        }
    }

    // Limpieza de recursos antes de la salida real.
    println!("Señal de interrupción recibida. Realizando limpieza y saliendo...");
}

Análisis del código

  • RUNNING: Es una variable estática de tipo AtomicBool. Se utiliza Ordering::SeqCst (Sequentially Consistent) para garantizar que todos los hilos y el manejador de señal vean los cambios en el mismo orden, evitando problemas de memoria relacional.
  • handle_sigint: Es una función con la convención de llamada extern "C". Se define para que el sistema operativo pueda invocarla directamente. Su única tarea es RUNNING.store(false, ...), que es una operación atómica segura para señales, evitando el uso de println! o bloqueos que causarían un deadlock.
  • signal(SIGINT, handle_sigint): Es una llamada a la función signal de la biblioteca libc. Se encapsula en un bloque unsafe porque Rust no puede garantizar la seguridad de la llamada a una función externa de C. Registra nuestra función para que se ejecute cuando el proceso reciba la señal SIGINT (Ctrl+C).
  • panic::set_hook: Establece un cierre (closure) que toma una referencia a panic::PanicInfo. Esto permite capturar detalles del pánico (como el archivo y la línea) antes de que el programa termine, siendo útil para instrumentación y telemetría.
  • catch_unwind: Esta función envuelve una sección de código y captura el pánico si ocurre dentro de ella, devolviendo un Result<T, Box<dyn Any + Send>>. En el ejemplo, permite que si algo fallara dentro de la clausura, el hilo principal pueda decidir si continuar o no, en lugar de abortar todo el proceso.

126