setjmp y longjmp proporcionan un mecanismo de salto no local, lo que permite que la ejecución del programa salte a través de múltiples niveles de la pila de llamadas, ignorando los retornos convencionales de las funciones. setjmp captura el contexto de ejecución actual —incluyendo el contador de instrucciones, el puntero de pila y los registros de propósito general— y lo almacena en una estructura jmp_buf. Cuando se invoca longjmp, el procesador restaura dicho estado, provocando que la ejecución “regrese” al punto donde se llamó originalmente a setjmp, pero con un valor de retorno distinto de cero.
Este mecanismo es extremadamente potente para implementar una semántica de excepciones en C, siendo útil en escenarios donde un error profundo en una estructura de datos altamente anidada (como un analizador sintáctico recursivo) requiere abortar toda la operación sin pasar por cada nivel de retorno intermedio. Sin embargo, su uso debe limitarse estrictamente a la gestión de errores críticos y nunca para el flujo de control normal de la aplicación. Si se utiliza incorrectamente, se puede corromper la integridad del estado de la máquina abstracta de C: no hay un proceso de “desenrollado de la pila” (stack unwinding), lo que significa que los recursos locales (memoria, descriptores de archivos, mutexes) quedan huérfanos, y las variables locales pueden presentar valores inconsistentes si el compilador ha optimizado su almacenamiento en registros en lugar de en la memoria principal.
#include <stdio.h>
#include <setjmp.h>
#include <stdlib.h>
/* El buffer que almacena el contexto de ejecución */
jmp_buf env;
/*
* Esta variable DEBE ser volatile.
* Si no lo es, el compilador podría mantener su valor en un registro
* que se restaurará al estado original de setjmp, perdiendo el cambio.
*/
volatile int error_status = 0;
/* Esta variable NO es volatile. Su valor tras un longjmp es impredecible. */
int local_counter = 0;
void deep_level(int depth) {
if (depth > 3) {
printf("[deep] Error crítico en nivel %d. Lanzando longjmp...\n", depth);
error_status = 1; /* Modificación segura: se escribe en memoria */
local_counter = 99; /* Modificación peligrosa: probablemente en un registro */
/* El segundo argumento es el valor que recibirá el setjmp */
longjmp(env, 1);
}
printf("[deep] Nivel %d\n", depth);
deep_level(depth + 1);
}
void middle_level(int depth) {
printf("[middle] Entrando nivel %d\n", depth);
deep_level(depth);
/* Este código nunca se ejecutará si ocurre el longjmp */
printf("[middle] Salida nivel %d\n", depth);
}
int main(void) {
int rc;
/*
* setjmp retorna 0 la primera vez que es llamado.
* Retorna un valor distinto de 0 cuando se invoca longjmp desde algún nivel.
*/
rc = setjmp(env);
if (rc == 0) {
printf("[main] Estableciendo punto de restauración.\n");
middle_level(1);
printf("[main] Si llegas aquí, la ejecución fue normal.\n");
} else {
/* rc contiene el valor pasado en longjmp (en este caso, 1) */
printf("[main] Recuperado de longjmp. Código de retorno: %d\n", rc);
printf("[main] error_status (volatile): %d\n", error_status);
printf("[main] local_counter (no volatile): %d\n", local_counter);
}
return 0;
}
Análisis del mecanismo
El comportamiento observado en el ejemplo depende críticamente de la interacción entre el compilador y el modelo de memoria.
jmp_buf env: Actúa como el “snapshot” del estado del CPU. Al llamar asetjmp(env), guardamos el estado actual de la pila.- El flujo de salto: Cuando
deep_leveldetecta el error,longjmp(env, 1)no simplemente retorna amiddle_level, sino que “teletransporta” el puntero de instrucción directamente de vuelta a la línearc = setjmp(env)enmain. Esto salta por encima de los marcos de pila (stack frames) demiddle_levely las llamadas pendientes dedeep_level. - El problema de la optimización y
volatile: Fíjate enlocal_counter. En una compilación optimizada (como-O2o-O3), el compilador suele asignar variables locales a registros de la CPU para mejorar la velocidad. Cuandolongjmprestaura el contexto, restaura también el contenido de esos registros al estado que tenían en el momento desetjmp. Comolocal_counterse modificó después desetjmp, su nuevo valor (99) se pierde si el registro fue sobrescrito por el estado restaurado, volviendo a0. En cambio,error_status, al estar marcada comovolatile, obliga al compilador a realizar la escritura y lectura directamente en la memoria RAM, asegurando que el cambio sobreviva al salto. - Retorno de
setjmp: La primera llamada siempre devuelve0. Silongjmpse invoca con un valorval,setjmpretornaval. Sivales0,setjmpretornará1por convención para evitar la ambigüedad.
El error frecuente
Un error clásico y extremadamente sutil es intentar usar longjmp para saltar fuera de un manejador de señales (signal handler). Esto produce un comportamiento indefinido porque longjmp no realiza la limpieza necesaria del estado interno del manejador de señales.
/* ERROR: Código peligroso que causa Comportamiento Indefinido */
void signal_handler(int sig) {
// longjmp desde un manejador de señales es inseguro
longjmp(env, 1);
}
Si necesitas saltar desde un manejador de señales para recuperar la ejecución, debes usar la extensión [POSIX] sigsetjmp y siglongjmp, las cuales están diseñadas para restaurar también la máscara de señales, evitando dejar al proceso en un estado de bloqueo o con señales bloqueadas permanentemente. Además, recuerda que longjmp no ejecuta código de limpieza; cualquier malloc realizado en las funciones saltadas resultará en una fuga de memoria (memory leak) inmediata.
N° 90