What Is Stack Unwinding

What is stack unwinding?

Stack unwinding is usually talked about in connection with exception handling. Here's an example:

void func( int x )
{
char* pleak = new char[1024]; // might be lost => memory leak
std::string s( "hello world" ); // will be properly destructed

if ( x ) throw std::runtime_error( "boom" );

delete [] pleak; // will only get here if x == 0. if x!=0, throw exception
}

int main()
{
try
{
func( 10 );
}
catch ( const std::exception& e )
{
return 1;
}

return 0;
}

Here memory allocated for pleak will be lost if an exception is thrown, while memory allocated to s will be properly released by std::string destructor in any case. The objects allocated on the stack are "unwound" when the scope is exited (here the scope is of the function func.) This is done by the compiler inserting calls to destructors of automatic (stack) variables.

Now this is a very powerful concept leading to the technique called RAII, that is Resource Acquisition Is Initialization, that helps us manage resources like memory, database connections, open file descriptors, etc. in C++.

Now that allows us to provide exception safety guarantees.

Error handling in Swift does not involve stack unwinding. What does it mean?

Stack unwinding is just the process of navigating up the stack looking for the handler. Wikipedia summarizes it as follows:

Some languages call for unwinding the stack as this search progresses. That is, if function f, containing a handler H for exception E, calls function g, which in turn calls function h, and an exception E occurs in h, then functions h and g may be terminated, and H in f will handle E.

Whereas a Swift error doesn't unwind the stack looking for a handler. It just returns, and expects the caller to handle the thrown error. In fact, the sentence after the one you quote goes on to say:

As such, the performance characteristics of a throw statement are comparable to those of a return statement.

So, using that first example, where f called g which calls h, in Swift, if you want f to catch the error thrown by h, then:

  • h must explicitly be marked that it throws errors;
  • g must explicitly try its call to h;
  • g must also be marked that it throws errors, too; and
  • f must explicitly try its call to g.

In short, while some other languages offer stack unwinding in the process of finding the exception handler, in Swift error handling, you must either explicitly catch the error thrown by functions you try, or be designated as a function that throws so that failed try calls will be thrown back up to the caller. There is no automatic unwinding of the stack in Swift.

All of this is unrelated to the question of whether deallocation takes place. As you've seen, yes, the throw in Swift behaves much like return, deallocating those local variables.

It's worth noting that not all exception handling that involves stack unwinding does the deallocation. Generally it does (because of course we want it to clean up when we're handling exceptions), but for example, "the GNU C++ unwinder does not call object destructors when an unhandled exception occurs. The reason for this is to improve debuggability." (From Exception Handling in LLVM.) Clearly, that's only appropriate for unhandled exceptions in debugging environments, but it illustrates the issue that unwinding the stack doesn't necessarily mean that objects are deallocated.

Is stack unwinding with exceptions guaranteed by c++ standard?

Is stack unwinding for uncaught exceptions guaranteed by the standard?

Stack unwinding is guaranteed to happen only for caught exceptions ([except.handle]/9):

If no matching handler is found, the function std::terminate() is called; whether or not the stack is unwound before this call to std::terminate() is implementation-defined.

So it's implementation-defined, otherwise.

If not, why?

In the event of an uncaught exception, the standard causes std::terminate to be called. That represents the end of the execution of the program. If you have some platform-specific way of logging information about the state of the system at that time, you may not want that state to be disturbed by stack unwinding.

And if you're not doing that... then you don't care either way.

If you truly need the stack to always be unwound, then you can put your main code (and any thread functions) in a try {} catch(...) {throw;} block.

When 'nested stack unwinding' is OK?

Throwing an exception while stack unwinding is in progress is fine. But throwing an exception from a destructor of an object that is being unwound from the stack or from any other function invoked by the exception handling mechanism (e.g. a catch parameter constructor) causes a call to std::terminate.

Throwing from a function here is supposed to mean that the thrown exception is not caught inside its body (or function try-catch block) and actually escapes the function.

In your example, no exception is leaving the destructor. So there is no problem.

It is allowed to have nested exceptions being handled or being in the process of stack unwinding. Once the nested exception is caught, handled and the destructor finishes, the original stack unwinding can continue calling the next destructor moving up the stack again. There is even API for this. For example you can use std::uncaught_exceptions() which gives you the number of currently uncaught exceptions.

The implementation of the C++ runtime just has to make sure to keep track of all of the exception objects currently alive.

(I assume that you are not really interested in the exact implementation details of the unwind implementations. If you are, then please clarify this in the question.)

is it possible to execute C code during C++ stack unwinding / exception

I'd like to register the cleanup function to be run during stack unwinding programmatically using portable C.

Not possible in portable C.

The C11 standard n1570 does not even require any call stack and permit compiler optimizations not using it. In some cases, there is no "stack unwinding". Think of tail-call optimizations (try gcc -Wall -O3 -S -fverbose-asm with a recent GCC) and read this draft report explaining some gcc optimizations (work in progress in June 2020). If you think of C++, read n3337, its C++11 standard.

However, if you decide to use (specifically) a recent enough GCC (so GCC 10 in June 2020) you could consider using specific builtins or pragmas. GCC has a chapter about C language extensions and another one on C++ extensions and also one about invoking it.

You might even be interested in writing your GCC plugin, or in using its libgccjit or in reusing its libbacktrace by Ian Taylor.

On Linux, see also dlopen(3) and dlsym(3) and consider using Clang.

You could ask some help from e.g. AdaCore or on gcc-help@gcc.gnu.org public mailing list.



Related Topics



Leave a reply



Submit