I got to work on a fun DCR with Chris Brumme back around the time we were shipping Whidbey Beta2. (DCR means Design Change Request, essentially an unplanned change to the design of a component.) We went back and forth as to whether or not to release it with Beta2, but given that the implementation would have been right up against our lock down period, the risk was too high. Thus, it’ll first appear in our next CTP, RC, or whatever release comes out before Whidbey RTMs.
The crux of the problem is this. Lots of code gets written in C# assuming that
catch (Exception)
is sufficient to backstop any exception a piece of CLR
software can generate. It turns out that, while doing so is not CLS compliant,
IL can throw just about anything. The throw instruction will happily take a
reference on the stack to any managed object – not just those whose type falls
into the Exception
type hierarchy – and unwind the stack with it in hand.
A typical user (and even some Framework developers) write exception handling
code that looks like this (without all the Console.WriteLine
s of course :P):
try
{
Console.WriteLine("Inside try...");
F();
Console.WriteLine("Exiting try");
}
catch (Exception e)
{
Console.WriteLine("In catch ({0})", e);
}
Console.WriteLine("Outside try...exiting gracefully");
Now, this will work perfectly fine if F
did as follows:
static void F()
{
// foo...
throw new InvalidOperationException();
// bar...
}
InvalidOperationException
derives from Exception
, so the catch block picks it
up. But what if F
did this?
static void F()
{
// foo...
throw 0;
// bar...
}
Well, thankfully you can’t write that in C#. But you can in verifiable IL:
.method private hidebysig static void F() cil managed
{
.maxstack 1
.locals init (
int32 V_0)
ldc.i4.0
box [mscorlib]System.Int32
throw
}
The specific type, int
in this case, really doesn’t matter. It could be any
other reference type that doesn’t somehow derive from Exception
, a value type,
or even a null
reference!
You might turn your nose up at the idea of catching all exceptions. I did. But
consider if you need to roll back sensitive state that was introduced inside
the try block. I’ve already covered why doing this in the finally block only
might not be
sufficient. If F()
were a virtual method
that a user could override and somehow supply an object of their choosing, a
malicious user could use this (along with an exception filter) to mount a nasty
security attack. Coming from a Java background, I was initially very surprised
how real this problem is… The world becomes much more complex when you interop
so tightly with the OS. For example, the CLR has to work well with SEH
primarily for situations where mixed call stacks make unmanaged-to-managed (and
vice versa) transitions. Suffice it to say that the two pass model introduces
lots of complexities.
Many people think that this is inherently a C# problem. Isn’t it C#, not the
runtime, that forces people to think in terms of Exception
-derived exception
hierarchies? Certainly there is precedent that indicates throwing arbitrary
objects is a fine thing for a language to do. Just take a look at C++ and
Python. And furthermore, C# actually enables you to fix this problem:
try
{
F();
}
catch
{
// ...
}
This approach has two problems. First, the catch-all handler doesn’t expose to
the programmer the exception that was thrown. C# could have changed this (e.g.
with TLS data exposed through a static member, e.g. Exception.GetLastThrown
, or
something like that). That still wouldn’t solve the problem that things that
aren’t exceptions don’t accumulate a stack trace as they pass through the
stack, making them nearly impossible to debug. But probably worse, the average
programmer doesn’t even know this is a problem! Including those who are writing
code for the Frameworks that Microsoft ships. But they really shouldn’t have to
know. This problem spans many languages, and it really made sense for the
runtime to help them out.
We solved the problem by introducing some new behavior inside the exception
subsystem of the CLR. It’s mostly transparent to the user. When something gets
thrown that is not derived from Exception
, we instantiate a new
System.Runtime.CompilerServices.RuntimeWrappedException
, supply the originally
thrown object as an instance field of that puppy, and propagate that instead.
It’s public; most people will never catch such things directly, but you can if
you need to access the thing that got thrown in the first place.
This has some nice benefits. The C# user can continue writing
catch (Exception)
, and – since RuntimeWrappedException
derives from Exception
– will receive any non-CLS exceptions. The try/catch block we had originally written
will just work for free now. And furthermore, we now capture stack trace for
everything, meaning that debugging and crash dumps are immediately much more
useful. Lastly, there’s still a playground for languages that wish to continue
participating in throwing exceptions not derived from Exception
.
This last point actually complicates the design quite a bit. We queried our
language community, and perhaps not-so-surprisingly, there are a lot of
compilers that can throw anything. C++/CLI is one of them. So we had to
preserve the existing semantics for those languages, while still enabling C#
users to get the benefits of this change. Thus was born
System.Runtime.CompilerServices.RuntimeCompatibilityAttribute
. The C# and VB
compilers will auto-decorate any compiled assemblies with this attribute,
setting its property WrapNonClsExceptions
to true
. The runtime keys off of that
to determine whether the old or new behavior is desired. The default is that we
don’t surface the aforementioned wrapping behavior (although as an
implementation detail, we still do it). We expect more of these
compatibility-preserving changes in the future, which resulted in the somewhat
generic attribute naming.
If the attribute is absent, or present and WrapNonClsExceptions
is set to
false
, we still actually wrap the exception internally so we can (1) maintain
good stack traces for debugging and (2) to cleanup and optimize some of the
exception code paths that had to branch based on the type of the exception. But
we unwrap it as we match it against catch handlers. And we unwrap it when we
deliver it to catch filters. So these languages don’t know anything ever
changed.
It’s actually gets a bit more complicated than this, however. For
cross-language call stacks, we actually do the unwrapping based on whatever the
assembly in which the catch clause’s assembly wants to do. Say method M
in
C++/CLI assembly A
throws an int
; this is called by method N
in C# assembly B
.
At throw time, we construct a new RuntimeWrappedException
and use that for
propagation. If assembly A
catches it, all it sees is the int
… It never knows
we wrapped it. But if it leaks, and assembly B
had wrapped the call in M
with a
catch (Exception)
, that handler will actually see a RuntimeWrappedException
.
Furthermore, consider if there were another C++/CLI assembly C
; if N
didn’t
catch the leaked int, it would surface in C
as if it never got wrapped. This is
what users expect to happen, and it composes very nicely.
Most users won’t even know about this change. But hopefully their code gets more secure and robust for free.