Here's a scenario that behaves differently (and incorrectly IMHO) in Rotor
vs .Net.
(Sorry if it's a bit long - code included at the end of the message):
The facts:
1. I have an assembly that starts with managed code in main() and calls
an unmanaged function "UnmanagedException(...)" in a DLL called
UnmanEx.dll.
2. The UnmanagedException(...) function takes a delegate as a parameter
and uses it to perform a call back into managed code (same module where
main() is implemented, not that it make any difference).
3. The code for the callback (implemented in EvHandlerClass::EvHandler
( ... )) does nothing more than throw a new Exception() while inside a
try/finally block. (well, it also prints some messages, but those are for
tracking purposes only - no relevance to the current problem). Also note
that the code for the callback does NOT contain any exception handling
code whatsoever.
4. The code in UnmanEx.dll::UnmanagedException( Delegate ) simply calls
the delegate parameter inside a VC++ style __try/__except block. The
filter for the block just "eats away" any exception it receives (empty
block with EXCEPTION_EXECUTE_HANDLER).
Essentially, the test creates a base layer in managed code (call it A),
that calls a layer in unmanaged code (call it B). Further B sets up a "raw
SEH" exception frame and calls back in managed code layer C which sets up
a finally frame and throws an exception.
Thinking about it, the code should behave as follows:
A calls B. B sets up exception frame ExB and calls C. C throws an
exception but has a finally block FbC. B now must catch the exception
thrown by C and must silently "eat it away" by executing its empty block.
This is the first pass. In the second pass, the finally block for C (Fbc)
should get executed after which the execution should continue without any
exception back into A and out of main(). (I hope the included code would
make this clearer.)
Lo and behold, this is exactly what happens in .NET!
In Rotor however, the VC++ "raw SEH" frame seems to mess things up
royally. Depending on whether you run it in the debugger or at the command
line you get either:
>>>>>>>>>>>>>>>
PAL Assertion failed - pRegistration == NULL || (PVOID)pRegistration >
(PVOID)&pRegistration - line 57 in file .\exception.c
Unhandled exception at 0x77f7f570 in clix.exe: User breakpoint.
PAL Assertion failed - pRegistration == NULL || (PVOID)pRegistration >
(PVOID)&pRegistration - line 63 in file .\exception.c
Unhandled exception at 0x77f7f570 in clix.exe: User breakpoint.
<<<<<<<<<<<<<<<
or the more radical
>>>>>>>>>>>>>>>
Assert failure(PID 2616 [0x00000a38], Thread: 2868 [0xb34]): !
m_fPreemptiveGCDisabled
File: d:\work\dotnet\rotor\sscli\clr\src\vm\threads.h, Line: 899 Image:
D:\Work\dotnet\rotor\sscli\build\v1.x86fstchk.rotor\clix.exe
<<<<<<<<<<<<<<<
I believe the issue is the manner in which PAL's SEH emulation works. PAL
simulates SEH by installing a global exception filter
(PAL_ExceptionFilter) in "win32pal.c" PAL_Startup(...). Furthermore
there are a bunch of macros and helper functions (most notably
PAL_TryHelper/PAL_EndTryHelper in "exception.c") that keep a linked list
of exception frames (essentially glorified setjmp buffers) in a TLS slot.
This creates a parallel structure of exception frames to the one normally
available in fs:[0].
This works fine IF either:
1. PAL_ExceptionFilter gets to see every exception in the system first and
has a chance to do it's dispatching.
2. Everybody using ROTOR uses the PAL implementation of SEH instead of the
VC++ provided one.
Point 2. above is the sticky one.
If I was to think of a solution, I would say that the stubs between
managed and unmanaged code must get an opportunity to filter *all* the
exceptions without having to wait for PAL_ExceptionFilter to kick in. This
implies that managed/unmanaged stubs have their own "raw SEH" frames
linked to fs:[0]. I guess that's how the .NET implementation works, but
I'm afraid that this approach is not portable (which is why it changed in
Rotor?). In fact, the PAL documentation includes a note about the SEH
implementation that's quite helpful.
Originally I assumed that Rotor does exactly what I suggest above by
reading the following in the PAL docs:
"Unfortunately, the try/catch block around the jitted code cannot be the
regular C/C++ SEH try block. It is the native EH record plugged directly
into the fs:[0] chain in the Windows i386 version."
That's not the case though, which opens the question of whether Win32 "raw
SEH" can be used with Rotor at all? This, I guess, would be a big deal for
any "real life" projects.
Thanks for the patience reading all of this :-),
Cristian
// CallUnmanEx.il
.assembly CallUnmanEx {}
.assembly extern mscorlib {}
.module extern UnmanEx.dll
.class public sealed auto autochar EventHandlerDelegate extends [mscorlib]
System.MulticastDelegate
{
.method public hidebysig specialname rtspecialname void .ctor(
class System.Object obj, void* meth ) runtime managed
{}
.method public virtual instance void Invoke( int32 x ) runtime
managed
{}
}
.class public EvHandlerClass
{
.method public hidebysig static pinvokeimpl("UnmanEx.dll" cdecl) void
UnmanagedException(class EventHandlerDelegate x) cil managed preservesig
{}
.method public hidebysig static pinvokeimpl("UnmanEx.dll" cdecl) void
ThrowsException() cil managed preservesig
{}
.method public specialname rtspecialname void .ctor() il managed
{
.maxstack 1
// call super constructor
ldarg.0
call instance void [mscorlib]System.Object::.ctor()
ret
}
.method public instance void EvHandler( int32 notUsed ) cil managed
{
.maxstack 1
.try
{
ldstr "In EvHandler..."
call void [mscorlib]System.Console::WriteLine( class
System.String )
newobj instance void [mscorlib]System.Exception::.ctor()
throw
leave exitSEH
}
finally
{
ldstr "In EvHandler finally..."
call void [mscorlib]System.Console::WriteLine( class
System.String )
endfinally
}
exitSEH:
ret
}
.method instance void function() il managed
{
.maxstack 2
ldstr "In function()...before call to UnmanagedException..."
call void [mscorlib]System.Console::WriteLine( class
System.String )
// load this instance as environment for method
ldarg.0
// load method pointer
ldftn instance void EvHandlerClass::EvHandler( int32 )
// the next line creates an instance of the delegate
newobj instance void EventHandlerDelegate::.ctor(class
System.Object, void*)
call void EvHandlerClass::UnmanagedException( class
EventHandlerDelegate )
ldstr "In function()...after call to UnmanagedException..."
call void [mscorlib]System.Console::WriteLine( class
System.String )
ret
}
}
.method static public void main() il managed
{
.entrypoint
.maxstack 2
newobj instance void EvHandlerClass::.ctor()
call instance void EvHandlerClass::function()
ret
}
// UnmanEx.dll
typedef void (__stdcall *PFN)( int notUsed );
extern "C"
__declspec(dllexport)
void UnmanagedException( PFN delegate )
{
__try
{ delegate( 0 ); }
__except( EXCEPTION_EXECUTE_HANDLER )
{}
}