@nogc and exceptions

Jakob Ovrum via Digitalmars-d digitalmars-d at puremagic.com
Thu Sep 11 20:37:09 PDT 2014


There is one massive blocker for `@nogc` adoption in D library 
code: allocation of exception objects. The GC heap is an ideal 
location for exception objects, but `@nogc` has to stick to its 
promise, so an alternative method of memory management is 
desirable if we want the standard library to be widely usable in 
`@nogc` user code, as well as enabling third-party libraries to 
apply `@nogc`. If we don't solve this, we'll stratify D code into 
two separate camps, the GC-using camp and the `@nogc`-using camp, 
each with their own set of library code.

I can think of a couple of ways to go:

1) The most widely discussed path is to allocate exception 
instances statically, either in global memory or TLS. Currently, 
this has a few serious problems:

   1a) If the exception is chained, that is, if the same exception 
appears twice in the same exception chain - which can easily 
happen when an exception is thrown from a `scope(exit|failure)` 
statement or from a destructor - the chaining mechanism will 
construct a self-referencing list that results in an infinite 
loop when the chain is walked, such as by the global exception 
handler that prints the chain to stderr. This can easily be 
demonstrated with the below snippet:

---
void main()
{
     static immutable ex = new Exception("");
     scope(exit) throw ex;
     throw ex;
}
---

Amending the chaining mechanism to simply *disallow* these chains 
would neuter exception chaining severely, in fact making it more 
or less useless: it's not realistically possible to predict which 
exceptions will appear twice when calling code from multiple 
libraries.

   1b) Exceptions constructed at compile-time which are then later 
referenced at runtime (as in the above snippet) must be immutable 
(the compiler enforces this), as this feature only supports 
allocation in global memory, not in TLS. This brings us to an 
unsolved bug in the exception mechanism - the ability to get a 
mutable reference to an immutable exception without using a cast:

---
void main()
{
     static immutable ex = new Exception("");
     try throw ex;
     catch(Exception e) // `e` is a mutable reference
     {
         // The exception is caught and `e` aliases `ex`
     }
}
---

Fixing this would likely involve requiring 
`catch(const(Exception) e)` at the catch-site, which would 
require users to update all their exception-handling code, and if 
they don't, the program will happily compile but the catch-site 
no longer matches. This is especially egregious as error-handling 
code is often the least tested part of the program. Essentially 
D's entire exception mechanism is not const-correct.

   1c) Enhancing the compiler to allow statically constructing in 
TLS, or allocating space in TLS first then constructing the 
exception lazily at runtime, would allow us to keep throwing 
mutable exceptions, but would seriously bloat the TLS section. We 
can of course allocate shared instances in global memory and 
throw those, but this requires thread-safe code at the catch-site 
which has similar problems to catching by const.

2) The above really shows how beneficial dynamic memory 
allocation is for exceptions. A possibility would be to allocate 
exceptions on a non-GC heap, like the C heap (malloc) or a 
thread-local heap. Of course, without further amendments the onus 
is then on the catch-site to explicitly manage memory, which 
would silently break virtually all exception-handling code really 
badly.

However, if we assume that most catch-sites *don't* escape 
references to exceptions from the caught chain, we could 
gracefully work around this with minimal and benevolent breakage: 
amend the compiler to implicitly insert a cleanup call at the end 
of each catch-block. The cleanup function would destroy and free 
the whole chain, but only if a flag indicates that the exception 
was allocated with this standard heap mechanism. Chains of 
exceptions with mixed allocation origin would have to be dealt 
with in some manner. If inside the catch-block, the chain is 
rethrown or sent in flight by a further exception, the cleanup 
call would simply not be reached and deferred to the next 
catch-site, and so on.

Escaping references to caught exceptions would be undefined 
behaviour. To statically enforce this doesn't happen, exception 
references declared in catch-blocks could be made implicitly 
`scope`. This depends on `scope` actually working reasonably 
well. This would be the only breaking change for user code, and 
the fix is simply making a copy of the escaped exception.

Anyway, I'm wondering what thoughts you guys have on this nascent 
but vitally important issue. What do we do about this?


More information about the Digitalmars-d mailing list