[cxx-abi-dev] gcc unwind ABI change for forced unwind

Dave Butenhof David.Butenhof at hp.com
Thu May 22 14:25:38 UTC 2003


Greetings.

I've been forwarded part of this discussion and asked to comment. For 
those who don't know me, the relevant context is probably that I know 
nothing about gcc internals, a little about its external manifestation, 
a moderate amount about C++ syntax and semantics without being by any 
means a C++ expert, and I know rather a lot about threads and POSIX. I'm 
the principal architect for the POSIX threads library on Tru64 UNIX and 
OpenVMS, where cancel and thread exit are exceptions implemented using 
the system libexc library -- also used by C++, Ada, and others for their 
exceptions. That said, I have both opinions and a fair amount of 
experience behind (and often in front of) those opinions. ;-)

> >From: Cary Coutant <cary at cup.hp.com>
> Content-Transfer-Encoding: 7bit
> Message-Id: <A34F2B63-8BD7-11D7-8E8F-003065589C02 at cup.hp.com>
> X-Mailer: Apple Mail (2.552)
> Subject: [cxx-abi-dev] gcc unwind ABI change for forced unwind
> Status: RO
>
> Many of you are probably aware of (and several of you participated in)
> a discussion thread on the gcc-patches mailing list about a new unwind
> API that Richard Henderson had to add to support forced unwinds
> resulting from (among possibly other things) thread cancellation. I
> thought it would be appropriate to bring this issue to this mailing list.
>
> Courtesy of Jim Wilson (who posted a note to the libunwind mailing
> list, which brought it to my attention), here are some pointers to the
> discussion threads leading up to this.
>
> http://gcc.gnu.org/ml/gcc-patches/2003-04/msg00008.html
> http://gcc.gnu.org/ml/gcc-patches/2003-04/msg02246.html
> http://gcc.gnu.org/ml/gcc-patches/2003-05/msg00473.html
> http://gcc.gnu.org/ml/gcc-patches/2003-05/msg00160.html
>
> As I understand the central issue, we would like to run C++ cleanups on
> a thread cancellation, in addition to the cleanups registered through
> the POSIX C bindings to the pthreads library. Cleanups resulting from
> local automatic objects that need destruction are easy, but the problem
> is what to do about catch(...) blocks. Richard's approach was to end
> such blocks with a call to the new API, "_Unwind_Resume_or_Rethrow()",
> if the block did not already end with a rethrow.
>
> I think Jason Merrill hit the nail on the head when he said (on 4/30):
>
> > The problem is that catch(...) is overloaded in C++.  It's used both for
> > code that wants to write a cleanup inline and rethrow and for code that
> > wants to trap all exceptions.
>
> There was some discussion about whether catch(...) blocks should run at
> all when doing a forced unwind, and whether forced unwinds should be
> allowed to penetrate a function declared throw(). I think I saw a
> consensus on the latter issue that thread cancellation and
> longjmp_unwind are not really exceptions, and must be allowed to
> proceed. On the former issue, however, there didn't seem to be a clear
> resolution.
>
Personally, I dislike the attempt to separate "cleanup" from 
"finalization" (handle/catch). I don't like the idea of an exception 
that can't be finalized, because it reduces the application's ability to 
control behavior.

Our cancel exception is in every way a normal exception (though there 
doesn't happen to be a C++ *name* for this exception). By CONVENTION, we 
declare that it should normally be handled via "finally" clauses rather 
than "catch" clauses, because it expresses some component's desire to 
terminate the thread and that should usually be honored. Similarly, a 
longjmp_unwind type operation shouldn't usually be finalized until it 
propagates to the target frame.

But there are always, er, "exceptions".

For example, back in the early days of DCE and DCE threads, we see the 
RPC component servers running what amounts to a remote extension of the 
client's call stack. The client makes an RPC call, and a remote server 
application fires up the server side of that call. It does this inside a 
managed server thread. If the server were to raise an exception, such as 
cancel, it should propagate and clean up the subset of the call stack 
that is logically a part of the client's call... but the managed server 
logic must be able to finalize the exception and marshall it back to the 
client so it can be made aware of what happened. And there's no need to 
unwind/cleanup any further, because the managed server thread can live 
to serve again.

A catch(...) is a catch. A catch(...) that happens to end in a throw 
isn't fundamentally different. It does represent a sort of ambiguity in 
the language model, though. Really, "cleanup" in C++ is a destructor, 
whereas catch() is for finalization. But as most people who initially 
took up C++ had learned their exception model on another language, such 
as Ada or Modula-2, the idiom 'catch (...) {throw;}' looked a lot like 
'finally{}' and provides a familiar hook for frame-based (rather than 
object-based) cleanup.

If someone's going to take this idiom more seriously, to the point of 
defining radically divergent behavior, you'd be far better off adding a 
true 'finally' keyword that makes it obvious. However, I'd prefer to see 
emphasis on the "pure C++" model that cleanup is done in local object 
destructors, while catch() is really for finalization. (That is, there 
may be reasons for 'catch(...) { if ( ... ) throw; else ... ; }' but 
'catch(...) { ... ; throw; }' should be, at least, strongly discouraged.)

> Ideally, one would take the position that good C++ code would
> encapsulate any cleanups it needs into local automatic objects, so that
> the compiler-generated cleanups would invoke the destructor. Real code,
> however, doesn't seem to work that way -- we see catch(...) blocks
> written with the intent to do cleanups. Given this real code, we should
> try to run those cleanups. But what happens when we hit a catch(...) of
> the other flavor -- the kind that just want to catch all exceptions?
> Ideally, we wouldn't want to run them at all on a forced unwind, since
> they're exception handlers, not cleanups. Without Richard's approach,
> if we execute such a block on a forced unwind, and that block doesn't
> end with a rethrow, the forced unwind doesn't resume (until, in the
> case of thread cancellation, the thread next reaches a cancellation
> point, and the process gets repeated). With Richard's new routine, a
> forced unwind gets the opportunity to rethrow, while a normal exception
> gets to resume execution.
>
There's nothing wrong with finalizing (catching) a cancel, thread exit, 
or even a longjmp_unwind... if that's what the application intended, and 
if the designers knew what they were doing. And if not... that's not a 
language issue, it's an application issue.

I realize that, pragmatically, there are complications when adding 
something like this after the fact. There will be applications busted 
when/if cancel becomes a true exception shared with C++, because someone 
will have catch(...) blocks that simply assume they'll never see an 
unexpected (new) exception. I call that bad design, but that's life.

There are those who have all along argued that cancel (and thread exit) 
should run C++ destructors but not be catch()-able. One (weird) option 
might be to make them catchable by "name", but not anonymously... they'd 
ignore any catch(...) clauses. I don't like it, but it'd solve the 
compatibility issue without preventing savvy code from finalizing a 
cancel or exit where it really makes sense in the context of the 
application. The same could be done for longjmp_unwind. It might be nice 
to have a standard way to catch all "forced_unwind" exceptions without 
needing to name each one.

Essentially, that twists the usage of "forced unwind" around a bit; they 
CAN be finalized, but only by code that at least explicitly states 
(correctly or not) that it knows what it's doing.

Throw specs add another interesting wrinkle. If cancel/exit/unwind are 
"exceptions", then it's illegal to propagate through an empty throw() 
spec, or any that doesn't identify them. Which means that everything 
calling a cancellation point needs to propagate the throw(cancel). (And 
don't forget thread exit! And what about longjmp_unwind?) Many think 
there's no need to worry about that if they're "forced unwinds" instead 
of "exceptions", but that depends a lot on the semantic intent of the 
throw() spec. Should it really be taken to be literally only "C++ 
exceptions", or should it be taken as a limitation on the reasons for 
abnormally unwinding that frame? That is, when I call a routine with a 
throw() spec, should I not be expecting that control will return to me 
only when the called function returns normally or when one of the listed 
throw conditions occurs? The "forced unwind" isn't transparent -- it'll 
run destructors, maybe at least some catch(...) clauses. There's not 
much use in throw() specifications if they can be so easily violated. 
(E.g., as someone pointed out, empty throw() clauses might provide an 
optimization opportunity to avoid generating unwind information... if 
unwinds can occur anyway you can't do that.)

The subject here is extending C++ to know about and somehow rationally 
deal with foreign exceptions (and/or unwind if you really want to make 
that distinction). The language semantics, or at least usage 
conventions, (and perhaps syntax), NOT just the runtime, needs to be 
changed to do that in a way that's consistent and useful.

-- 
/--------------------[ David.Butenhof at hp.com ]--------------------\
| Hewlett-Packard Company       Tru64 UNIX & VMS Thread Architect |
|     My book: http://www.awl.com/cseng/titles/0-201-63392-2/     |
\----[ http://homepage.mac.com/dbutenhof/Threads/Threads.html ]---/





More information about the cxx-abi-dev mailing list