|
|
|
|
|
by phoe-krk
2167 days ago
|
|
C's longjmp doesn't cleanly unwind the stack. Common Lisp's GO, just like all CL control flow operators, cleanly unwind the stack, allowing cleanup forms established by UNWIND-PROTECT to be executed. That's the key difference. The book contains an implementation of dynamic variables in C, though, which uses a GCC cleanup extension; the example code contains the contributed code examples for that showing various means of implementing dynavars in C. Promises are asynchronous while signaling and condition handling is fully synchronous; I don't know what kind of parallel one can draw here. If anything, a promise's .catch(...) method may act like a CL HANDLER-CASE; the promise becomes resolved, just a different code block is executed in case of a failure. |
|
Well, once upon a time it used to be setjmp/longjmp; currently it's based on a re-implementation of a mechanism that is almost setjmp/longjmp in assembly language.
To use setjmp/longjmp for implementing sophisticated exception handling, you have to maintain your own unwind chain on the stack and unwind that.
Before invoking longjmp, you have to walk your own frames that are chained through the stack, and do all the clean-up yourself, up to that frame that holds the jmp_buf where you want to jump.
TXR correctly maintains the chain connectivity even under delimited continuation support, which works by copying sections of the C stack to and from a heap object.
When a delimited continuation is restored (involving copying it out of a heap into a new location on the stack), the frame linkage in the restored continuation is fixed up and hooked up. The continuation can throw an exception and unwind out through the caller that invoked it.
Here is the implementation of unwind-protect operator in the interpreter:
The grotty jump saving and restoring stuff is hidden behind friendly-looking macros. The simple catch begin/end macros will create a frame with a particular type field. That type field tells the unwinder that it's supposed to stop there to do clean-up code. How that works is that the frame contains the saved jump buffer: a longjmp-like operation (that used to be longjmp once upon a time) restores control here, then the forms in the uw_unwind { } are executed and then the unwinding is resumed.In the virtual machine, there is a uwprot instruction instead:
uwprot 4 means that the cleanup code is found at instruction address 4. uwprot registers a frame which references that code. What immediately follows the uwprot instruction is the protected code. This code is terminated by an end instruction. When the code hits the end instruction, control returns to the uwprot instruction, which then transfers control to instruction address 4. The cleanup code is also terminated by an end instruction. In the non-unwinding case, that just falls through to the next end instruction for ending this whole block and returning the value of register t2, which is the result of the (foo) call produced in gcall t2 0. In the unwinding case the end nil at 6 will allow control to return to the unwinder.The function that the vm interpreter dispatches for uwprot is simplicity itself:
The VM context (frame level and instruction pointer) are saved very simply into local variables on the C stack. Well, what is saved is not the current instruction pointer but the one of the cleanup code, pulled from the instruction's operand. Then there is a simple catch frame which re-enters the vm, continuing with the next instruction. vm_execute will return when it hits that end instruction, passing control back to here. If an exception is thrown, then the uw_unwind block restores the VM context from the two variables and runs the cleanup code through to the end instruction, which also happens in the normal case.