@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