56. Non-local jumps: setjmp/longjmp and why they're not exceptions
56.1 The mechanism
setjmp saves the current stack context into a jmp_buf; longjmp
restores it, making execution jump back to exactly that setjmp call —
as if it were returning again, but with the value you pass to
longjmp instead of 0. It's the only way in C to unwind multiple
stack frames at once without every intermediate function explicitly
checking and returning an error code.
#include <setjmp.h>
#include <stdio.h>
static jmp_buf recovery_point;
void deeply_nested(int depth, int fail_at) {
if (depth == fail_at) {
longjmp(recovery_point, 1); // jump straight back to setjmp, skipping every return
}
printf("entering depth %d\n", depth);
deeply_nested(depth + 1, fail_at);
printf("leaving depth %d\n", depth); // never runs for frames at or past fail_at
}
int main(void) {
if (setjmp(recovery_point) == 0) {
deeply_nested(0, 3);
printf("this line never runs\n");
} else {
printf("recovered: error occurred at depth 3\n");
}
return 0;
}
// entering depth 0 / entering depth 1 / entering depth 2 / recovered: error occurred at depth 3
// — notice NO "leaving depth" lines print at all; longjmp skipped every one of those returns.
56.2 The one rule that isn't optional: mark shared locals volatile
#include <setjmp.h>
#include <stdio.h>
static jmp_buf recovery_point;
void risky(volatile int *counter) {
(*counter)++;
longjmp(recovery_point, 1);
}
int main(void) {
volatile int counter = 0; // MUST be volatile: written before longjmp, read after it —
// without volatile, the compiler may keep counter in a
// register that the stack-rewind doesn't restore, so the
// increment could appear to vanish after the jump
if (setjmp(recovery_point) == 0) {
risky(&counter);
} else {
printf("counter after longjmp: %d\n", counter); // reliably 1, because of volatile
}
return 0;
}
This is the other legitimate use of volatile alongside Part IX
§49.1's hardware registers: any local variable that's modified between
setjmp and a longjmp that returns to it, and then read afterward,
needs volatile — otherwise the C standard explicitly leaves its value
after the jump indeterminate.
56.3 What it doesn't do: no cleanup, no destructors, no stack unwinding of resources
void process(void) {
FILE *f = fopen("data.txt", "r");
parse(f); // if parse() longjmps out on a syntax error...
fclose(f); // ...this line never runs — the FILE* leaks, exactly like an
} // error-path leak (§44.1), just triggered by a jump instead of a return
C has no equivalent of C++ destructors or defer — a longjmp that
skips over a scope skips everything in it, including any pending
free/fclose/pthread_mutex_unlock. Anything a longjmp might jump
over needs its own goto cleanup-style handling (§4.2) reachable
before the jump, or must be tracked and released at the recovery
point itself. This is precisely why real interpreters that use
setjmp/longjmp for error recovery (many do, including reference
implementations of scripting languages) pair it with an explicit
resource-tracking stack that the recovery handler walks and releases —
longjmp itself will not do it for you.
56.4 sigsetjmp/siglongjmp: the signal-safe variant
Plain setjmp/longjmp don't reliably save/restore the process's
signal mask across the jump. If you're recovering from inside a signal
handler (Section 55.5's pattern), use sigsetjmp(buf, 1) /
siglongjmp instead — the 1 tells it to also save the current signal
mask, so siglongjmp correctly restores which signals were
blocked/unblocked at the time, instead of potentially leaving a signal
masked forever after the jump.