Salidas no locales
setjmp y longjmp: el mecanismo real, sus peligros documentados (volatile, recursos huérfanos, hilos), por qué C decidió no tener excepciones y cuál es el manejo de errores idiomático con [[nodiscard]] de C23.
Todo el control de flujo que has visto hasta aquí es local: salta dentro de una función. setjmp y longjmp rompen esa frontera y permiten abandonar decenas de marcos de pila de golpe, como una excepción sin red. Entender por qué esa herramienta existe, por qué casi nunca debes usarla y por qué C nunca adoptó excepciones te dice más sobre la filosofía del lenguaje que cualquier otra decisión de diseño.
- Usar
setjmpylongjmprespetando las restricciones del estándar. - Enumerar los peligros: variables no
volatile, recursos huérfanos y marcos muertos. - Argumentar por qué C no tiene excepciones y qué coste habrían tenido.
- Aplicar el manejo de errores idiomático de C con
[[nodiscard]]de C23.
El mecanismo
La cabecera setjmp.h define el tipo jmp_buf y dos operaciones asimétricas. setjmp guarda el contexto de ejecución —registros, puntero de pila, dirección de retorno— y devuelve 0. longjmp restaura ese contexto y hace que setjmp vuelva a retornar, esta vez con el valor que le pasaste.
#include <setjmp.h>
static jmp_buf punto_de_rescate;
static void nivel_profundo(int mal)
{
if (mal)
longjmp(punto_de_rescate, 42); /* abandona todos los marcos */
/* nunca se llega aquí si saltamos */
}
int ejecutar(void)
{
int codigo = setjmp(punto_de_rescate);
if (codigo != 0) {
/* segunda "vuelta": llegamos aquí desde longjmp */
return -codigo;
}
nivel_intermedio(); /* que a su vez llama a nivel_profundo */
return 0;
}
Cuatro reglas duras que el estándar impone y que casi nadie recuerda:
setjmpes una macro, y su invocación solo puede aparecer en contextos muy concretos: como expresión de control completa de unif,switch,whileofor, comparada con una constante entera, negada con!, o como sentencia de expresión completa. Guardar su resultado como en el ejemplo de arriba es la forma admitida; incrustarla en una expresión mayor es comportamiento indefinido.longjmpcon valor0hace quesetjmpdevuelva1. Nunca puede devolver cero la segunda vez.- Si la función que llamó a
setjmpya retornó, eljmp_bufapunta a un marco muerto y saltar ahí es comportamiento indefinido. - Saltar desde un hilo distinto al que ejecutó
setjmptambién es comportamiento indefinido.
Los peligros
El primero es el más sutil y el que aparece en los exámenes: las variables automáticas locales a la función que llamó a setjmp, si no son volatile y cambiaron entre la llamada y el salto, tienen valor indeterminado tras volver.
int contar(void)
{
volatile int intentos = 0; /* sin volatile: valor indeterminado tras el salto */
int total = 0; /* peligro: puede revertir a un valor antiguo */
if (setjmp(env) != 0)
intentos++; /* fiable solo por ser volatile */
total += trabajo();
return intentos * 10 + total;
}
La causa es mecánica: setjmp guardó los registros en ese instante y longjmp los restaura. Una variable que el compilador mantenía en un registro retrocede a su valor antiguo; una que vivía en memoria conserva el nuevo. Cuál de las dos cosas ocurre depende del nivel de optimización, así que el mismo binario compilado con -O0 y con -O2 puede dar resultados distintos. volatile fuerza el almacenamiento en memoria y elimina la ambigüedad.
El segundo peligro es estructural: longjmp no desenrolla nada. No ejecuta limpieza, no cierra ficheros, no libera memoria, no suelta locks.
flowchart TD A[ejecutar llama a setjmp] --> B[nivel intermedio: abre fichero] B --> C[nivel profundo: coge un lock] C --> D[longjmp al punto de rescate] D -->|salta sobre todo| A C -.->|nunca se ejecuta| E[soltar lock] B -.->|nunca se ejecuta| F[cerrar fichero] style D fill:#f38ba8,color:#11111b style E fill:#585b70,color:#cdd6f4 style F fill:#585b70,color:#cdd6f4
Por eso el uso legítimo de longjmp se restringe a bibliotecas que controlan todo el estado que hay entre el punto de rescate y el punto de fallo, con un arena o pool que se libera de golpe. Es exactamente lo que hacen libjpeg y libpng para abortar la decodificación de una imagen corrupta, y lo que hace el intérprete de Lua para propagar errores desde código C.
Por qué C no tiene excepciones
No fue un olvido. Las excepciones con desenrollado necesitan tres cosas que C, deliberadamente, no tiene:
- Destructores. Sin ellos, desenrollar la pila no puede liberar nada automáticamente. Unas excepciones que dejan fugas en cada lanzamiento son peores que devolver un código.
- Metadatos de desenrollado. Las excepciones de coste cero de C++ no son gratis: exigen tablas de desenrollado y una rutina de personalidad que engordan el binario incluso si nunca lanzas. En un microcontrolador con 32 KiB de flash o en un kernel, eso es inaceptable.
- Rutas de control ocultas. Cualquier llamada podría lanzar, así que toda función pasa a tener salidas invisibles en el código. C prefiere que las rutas de error se vean en el texto, aunque cueste más escribirlas.
Hay una cuarta razón, política: C es la lingua franca de las ABI. Todo lenguaje habla con el mundo a través de una interfaz C. Un mecanismo de excepciones en C obligaría a cada lenguaje huésped a implementarlo o a lidiar con él en cada frontera.
El manejo de errores idiomático
Lo que C ofrece a cambio es un conjunto de convenciones sencillas. La regla común: el valor de retorno indica el resultado, el dato sale por parámetro.
Código de retorno entero
0 para éxito y negativo para error, con el código en la magnitud. Es la convención del kernel y la más fácil de propagar hacia arriba.
Dato por parámetro de salida
El resultado se escribe en un puntero que pasa el llamante. Deja el retorno libre para el estado y evita valores centinela ambiguos.
Centinela más errno
NULL o -1 con el detalle en errno. Es lo que hace la biblioteca estándar, y obliga a poner errno a cero antes de llamar.
Enum de errores propio
Un enum cerrado por módulo, cazado por -Wswitch. Documenta el dominio de fallos mejor que un entero suelto.
C23 aporta la pieza que faltaba para que esto no dependa de la disciplina del llamante: [[nodiscard]]. Ignorar el resultado de una función marcada así produce un aviso del compilador.
[[nodiscard("hay que comprobar el fallo de asignación")]]
int reservar_buffer(size_t n, char **out);
void uso(void)
{
char *b;
reservar_buffer(4096, &b); /* aviso: resultado descartado */
if (reservar_buffer(4096, &b) != 0)
return; /* correcto */
free(b);
}
Combinado con la cascada de goto de la lección anterior, tienes el sistema completo: [[nodiscard]] garantiza que nadie ignora un error, el código de retorno lo propaga y las etiquetas de limpieza deshacen el estado en orden inverso. Todo visible en el texto, sin marcos que desaparecen a tus espaldas.
Es tentador leer el manejo de errores de C como pobreza: sin excepciones, sin Result, sin monadas, solo enteros y punteros. Pero la decisión invierte la carga de forma consciente: en un lenguaje con excepciones, la ruta de error es implícita y hay que buscarla; en C es una sentencia más, ocupa líneas, se ve en el diff y se cuenta en el presupuesto de complejidad. Ese coste sintáctico es la característica, no el defecto —te obliga a decidir, en cada llamada, qué pasa cuando falla—. Y tiene una consecuencia que se olvida: en C todas las rutas de salida de una función son enumerables leyendo su cuerpo, lo cual hace posible verificarlo formalmente, auditarlo línea a línea y ejecutarlo en contextos donde no existe pila secundaria ni tabla de desenrollado: un manejador de interrupción, un cargador de arranque, un dispositivo con kilobytes de memoria. setjmp y longjmp existen como escotilla de emergencia para los pocos casos que la necesitan, y su lista de peligros —variables indeterminadas, recursos huérfanos, marcos muertos— no es un fallo de la implementación: es la factura honesta de saltarse el modelo. C te deja pagarla, pero te enseña el recibo.
- Escribe el ejemplo de
contarsinvolatile, compílalo con-O0y con-O2y compara los resultados. - Abre un fichero antes de un
longjmpy confirma convalgrindque se queda huérfano. - Reemplaza ese
longjmppor códigos de retorno más cascada degotoy comprueba que la fuga desaparece. - Marca tres funciones de tu proyecto con
[[nodiscard]]y cuenta cuántos avisos aparecen. - Investiga cómo libjpeg monta su punto de rescate y qué estado controla para poder abortar sin fugas.