none
Problem with exception caught across ExecuteAssemblyByName RRS feed

  • Question

  • I am working on a native code game engine which hosts the CLR (2.0) and uses it to run the high-level game logic.  There is a C++/CLI DLL that implements an AppDomainManager as well as the API layer to the game engine.  The native C++ calls a CIL method in the DLL to bootstrap the game logic.  The main snippet of the method is:

    try
    {
    	AppDomain::CurrentDomain->ExecuteAssemblyByName( "0, Version=1.0.0.0, Culture=neutral, PublicKeyToken=xxxxx, processorarchitecture=msil" );
    }
    catch( Exception ^ e )
    {
    	STRING sErr( L"A managed exception occurred:\r\n" );
    	{
    		String ^exceptionString = e->ToString();
    		PIN_STRING( str, exceptionString )
    		sErr += str;
    	}
    
    	MessageBox( NULL, sErr.c_str(), NULL, MB_OK );
    }

    0.EXE is a pure MSIL assembly written in C# which contains a static Main method that gets called as if it was a standalone app.  It is built against the C++/CLI module to access the engine's API.  It is provided to the CLR via the host assembly store (which draws it from the game resource system).

    This works fine provided the game logic works (I even have a character animating).  The problem occurs if 0.EXE throws an exception and doesn't catch it itself.

    The above code should catch any such error and put up a dialog box (this is just a rough implementation for testing).  However, it does not.  Instead, it crashes.

    If I intentionally place a throw new NullReferenceException() at the beginning of 0.EXE's Main and run the thing in the VS2008 debugger, continuing the first-chance exception from the throw itself, I get another first-chance exception, this time a System.AccessViolationException in mscorlib.dll at the line with the ToString() call.

    The e variable does contain a valid exception object which I can examine in the locals window, but the ToString() call on it crashes.

    Note that the bootstrapping code and 0.EXE are running in the same application domain, so there should not be any problem accessing the exception object from the bootstrap code (and in fact the same DLL that contains the bootstrap code examines all kinds of objects from the game logic with no issues).

    I also know the string marshalling to native code is not at fault (note STRING and PIN_STRING are my macros), because I can replace the call to ExecuteAssemblyByName with throw gcnew NullReferenceException() and the message box appears correctly.  The same macros are used in other places to accept strings from game logic with no issue.

    Any help will be most appreciated.

    Thanks,

    Kevin

     

    Friday, March 26, 2010 6:44 PM

Answers

  • I finally found out what was going on here.  I tolerated the issue for a long time just by having the engine check whether it was running in a debugger and decline to provide the PDB files to the CLR if not.  Recently I was forced to reinvestigate this issue and finally found out the problem.

    It was a bug in my CLR host's implementation of the COM IStream interface which is used to provide the executables and PDBs to the CLR.  It was an embarrasingly simple matter of forgetting to add a reference to the returned object within the implemenation of IUnknown::QueryInterface.

    The bug was so hard to find because the CLR never calls QueryInterface in the ordinary course of executing an assembly or even when running in a debugger.  It does, however, call QueryInterface on the PDB stream when preparing the result of Exception.ToString(), in order to add line numbers to the report.  Once done with the returned interface pointer, it was calling IUnknown::Release, which should not have deleted the stream object but did, so when the CLR went to access it again the memory was invalid, thus the crash inside mscorlib.dll.

    Thanks again for the input.

    Kevin

    • Marked as answer by Love4Brandy Monday, March 19, 2012 2:33 AM
    Monday, March 19, 2012 2:31 AM

All replies

  • The ToString shouldn't really fail. Isn't it something in your PIN_STRING macro? If you just print the exception to console it works. Here's small example:

    // File: Host.cpp
    // Build: cl.exe /clr Host.cpp
    void wmain()
    {
        try
        {
            System::AppDomain::CurrentDomain->ExecuteAssemblyByName("Executable");
        }
        catch (System::Exception ^ e)
        {
            System::Console::WriteLine("Caught exception: {0}", e->ToString());
        }
    }
    
    // File: Executable.cs
    // Build: csc.exe Executable.cs
    class HelloWorld
    {
        static void Main(string [] args)
        {
            PrintString("Hello world C# code!");
            PrintString(null);
        }
        static void PrintString(string value)
        {
            System.Console.WriteLine("PrintString('{0}')", value.ToString());
        }
    }
    
    Run Host.exe:
    PrintString('Hello world C# code!')
    Caught exception: System.NullReferenceException: Object reference not set to an instance of an object.
       at HelloWorld.PrintString(String value)
       at ...

    -Karel

    Tuesday, March 30, 2010 6:21 AM
    Moderator
  • Hi Karel,

    Thank you for the reply.

    It's not the PIN_STRING.  It doesn't even get that far.

    Your example does work correctly if built stand-alone.  However, remember that the problem occurs in a native program that hosts the CLR.  Unfortunately I can't easily modify your example to host the CLR as this involves a fair bit of code.  However, I have been able to use your code within my project (which has, apart from the present concern, been working fine in terms of hosting the CLR, for some time), and reproduce the problem with your code in that way.

    Here's what I did:

    First of all there are three executables, not two.  Your Host.exe becomes part of ManagedHost.dll, which is run via hosting the CLR by NativeHost.exe.  NativeHost.exe is not a .NET assembly at all; it is a purely native Win32 program.

    1.  Add a call to AllocConsole in NativeHost prior to loading the CLR.  This is necessary in order to see the output of your code, as NativeHost.exe is not a console application.

    2.  Add a MessageBox just before NativeHost.exe terminates (after the CLR unloads), so that the display on that console can be seen before the process terminates.

    3.  There is an interface on my AppDomainManager which has a void method that NativeHost.exe calls via COM interop to boot the game's managed code.  I replace the body of this method with the body of your wmain().

    4.  I replace the string "Executable" you pass to ExecuteAssemblyByName with "Executable, Version=1.0.0.0, Culture=neutral, PublicKeyToken=...., processorarchitecture=msil", which is necessary in order for NativeHost to provide the CLR with the assembly - it only works with strong named assemblies.

    5.  I add the following line at the beginning of your Executable.cs (same reasoning):
    [assembly: System.Reflection.AssemblyVersion( "1.0.0.0" )]

    6.  I use the following line to compile your Executable:
    csc /debug /keyfile:blabla.snk Executable.cs

    7.  I place the resulting Executable.exe and Executable.pdb into the game's resource system such that the host assembly store will return their binary contents.

    8.  I make a checked build of NativeHost.exe and ManagedHost.dll in the usual fashion that has always been working.  They build fine.

    I run NativeHost and the following happens:

    1.  A new console appears and shows:
    PrintString('Hello world C# code!')

    2.  A window appears stating "This application has requested the Runtime to terminate it in an unusual way..."

    If I run it in the debugger:

    1.  The new console shows:
    PrintString('Hello world C# code!')

    2.  I get a first-chance exception System.NullReferenceException on what was the 12th line of your Executable.cs, as expected.  I continue it.

    3.  I get a first-chance exception System.AccessViolationException in mscorlib.dll, on what was the 11th line of your Host.cpp.  This seems to be the same problem calling ToString() on the exception object as I was originally complaining about.

    The really wild thing is, if I simply remove Executable.pdb from my game's resource system, such that the host assembly store will not give a PDB to the CLR, everything then runs fine and the console shows the same text as in your example.

    I highly doubt that there is anything wrong with the way my NativeHost provides the PDBs to the CLR, as 1. it uses largely the same code path as it does when returning the executable itself, which obviously has to be working, and 2. I can actually step through game scripts in VS2008 quite nicely if I do place the PDBs into the game resource system.

    I should note that your example does work when running on its own even if I compile it to output a PDB, but in this case the CLR is not hosted and the assemblies are not provided through a host assembly store.

    I can't see how this could possibly be a fault in either your code or mine, and have to suspect it's a bug in the CLR.

    I applied Windows Updates to see if it would make any difference, but it didn't.  My OS is Windows XP Pro SP3 [5.1.2600] and CLR (as reported by IE) is .NET CLR 1.1.4322; .NET CLR 2.0.50727; .NET CLR 3.0.4506.2152; .NET CLR 3.5.30729.

    The host's call to CorBindToRuntimeEx specifies "v2.0.50727", "wks", and flags STARTUP_LOADER_OPTIMIZATION_SINGLE_DOMAIN | STARTUP_CONCURRENT_GC.

    Thank you,
    Kevin
    Thursday, April 1, 2010 11:02 AM
  • I finally found out what was going on here.  I tolerated the issue for a long time just by having the engine check whether it was running in a debugger and decline to provide the PDB files to the CLR if not.  Recently I was forced to reinvestigate this issue and finally found out the problem.

    It was a bug in my CLR host's implementation of the COM IStream interface which is used to provide the executables and PDBs to the CLR.  It was an embarrasingly simple matter of forgetting to add a reference to the returned object within the implemenation of IUnknown::QueryInterface.

    The bug was so hard to find because the CLR never calls QueryInterface in the ordinary course of executing an assembly or even when running in a debugger.  It does, however, call QueryInterface on the PDB stream when preparing the result of Exception.ToString(), in order to add line numbers to the report.  Once done with the returned interface pointer, it was calling IUnknown::Release, which should not have deleted the stream object but did, so when the CLR went to access it again the memory was invalid, thus the crash inside mscorlib.dll.

    Thanks again for the input.

    Kevin

    • Marked as answer by Love4Brandy Monday, March 19, 2012 2:33 AM
    Monday, March 19, 2012 2:31 AM