(This is now about "stack unwinding", as in throw in C++ or longjmp
in C, rather than GC.)
Functions calls are to push as return is to pop. It might not be
*called* a stack, but it *IS* a stack ...
Indeed, function call and return -- and exception-handling or nested
setjmp() operations -- function as a kind of stack. There are some
significant differences between C and C++ here though, including
the fact that in C, the onus of "using longjmp only in a stack-like
manner" is placed entirely on the C programmer. A C++ programmer
using "throw" cannot accidentally pass an invalid goto-label-buffer
(C's "jmp_buf").
And as long as we are talking about reality, we should note that most
C implementations use a literal common stack for both return/link
addresses and autos.
Most, but not all. Significantly, many modern compilers optimize
"leaf" procedures to avoid separate stack frames, and some (probably
not as many) will use both "fake" and "real" frame pointers (as on
MIPS CPUs, where a frame pointer is generally used only if the size
of the stack frame varies, e.g., due to C99 VLAs).
... To run an exception the system, one way or another needs to implement
"pop (return) until catch found" at runtime without actually executing
the returns, which means that a uniform "pseudo-return" has to exist
outside of implicit execution.
One should, however, note that "uniform" can apply only after a
sort of union operation. The stack-unwind code might, for instance,
read something like this (here target_frame is assumed to be given
directly via longjmp; for exceptions it has to be calculated):
target_frame = jmpbuf_info[0]; /* or whatever index */
while (curframe != target_frame) {
switch (calculate_frame_type(curframe)) {
case FRAME_IN_LEAF_FUNCTION: /* can only happen once */
prevframe = jmpbuf_info[1]; /* or whatever */
break;
case FRAME_WITH_REAL_FRAME_POINTER:
prevframe = curframe->prev;
break;
case FRAME_WITH_VIRTUAL_FRAME_POINTER:
prevframe = curframe + compute_offset(curframe);
break;
default:
__panic("longjmp");
/* NOTREACHED */
}
}
Using longjmp is insufficient because you can set catch #1, then call
into something, then set catch #2, then return enough times to make
catch #2 no longer in the call stack scope, thus re-enabling catch
#1.
Indeed. However, longjmp() is *also* insufficient simply because
it just does not work properly: it may trash any non-"volatile"-qualified
variables local to the target of the longjmp() [*]. But if the compiler
is clever enough (i.e., so that setjmp/longjmp do not destroy local
variable values; and note that longjmp() is provided by the
compiler-writer, who can decide whether his compiler is sufficiently
clever), the same kinds of innards used in longjmp() *can* be used
to implement exceptions, as long as "catch" records something about
the call stack and "throw" can use that to decide whether catch-#2
is "active", for instance.
In the case of several real GCC implementations, throw really does
work a lot like the above, with the target frame being computed
somewhat dynamically and calculate_type_frame() and compute_offset()
being rather complicated operations that match the "instruction
pointer" (PC or IP register, typically) of the supplied frame[%]
against large "exception info" tables, all so that ordinary function
call and return need not manipulate the current catch stack. (In
other words, the compiler throws code-space at the problem, to
avoid a time penalty.)
[* If I had been writing the C standard, I think I would have
forced compiler-writers to handle setjmp/longjmp non-local "goto"
operations "correctly". In other words, I would have put the
burden on compiler-writers instead of C programmers, so that C
programmers would not have to mark variables "volatile" to get
predictable behavior when using longjmp.]
[% Even worse, the return-PC is often not in that frame, but rather
one frame away and/or "adjusted" somehow. Some CPUs save the PC
of the "call" instruction, some save the PC of the "next" instruction.
Some even have funny special-case frames in which multiple PCs are
saved. If longjmp and/or throw are to work across "exception"
frames, including Unix-like signal handlers, these also must be
handled.]