We have a large .NET solution with both C# and C++/CLI projects which reference each other. We also have several unit testing projects. We've recently upgraded from Visual Studio 2010 & .NET 4.0 to Visual Studio 4.5 & .NET 4.5, and now when we try to run the unit tests, there seem to be a problem loading some of the DLLs during the test.
The problem appears to happen because unit testing is performed on a separate AppDomain. The unit testing process (for example nunit-agent.exe) creates a new AppDomain with AppBase set to the test project's location, but according the Fusion Log, some of the DLLs are loaded with nunit's executable's directory as the AppBase instead of the AppDomain's AppBase.
I've managed to reproduce the problem with a simpler scenario, which creates a new AppDomain and tries to run the test there. Here's how it looks (I changed the names of the unit test classes, methods and the location of the dll to protect the innocent):
class Program
{
static void Main(string[] args)
{
var setup = new AppDomainSetup {
ApplicationBase = "C:\\DirectoryOfMyUnitTestDll\\"
};
AppDomain domain = AppDomain.CreateDomain("MyDomain", null, setup);
ObjectHandle handle = Activator.CreateInstanceFrom(domain, typeof(TestRunner).Assembly.CodeBase, typeof(TestRunner).FullName);
TestRunner runner = (TestRunner)handle.Unwrap();
runner.Run();
AppDomain.Unload(domain);
}
}
public class TestRunner : MarshalByRefObject
{
public void Run()
{
try
{
HtmlTransformerUnitTest test = new HtmlTransformerUnitTest();
test.SetUp();
test.Transform_HttpEquiv_Refresh_Timeout();
}
catch (Exception e)
{
Console.WriteLine(e);
}
}
}
This is the exception I get when trying to execute the unit test. As you can see, the problem happens the the C++ dll is initialized and tries to load the C# dll (I changed the names of the DLLs involved to CPlusPlusDll and CSharpDll):
System.TypeInitializationException: The type initializer for '' threw an exception. ---> .ModuleLoadExceptionHandlerException: A nested exception occurred after the primary exception that caused the C++ module to fail to load. ---> System.TypeInitializationException: The type initializer for '' threw an exception. ---> .ModuleLoadException: The C++ module failed to load during vtable initialization. ---> System.IO.FileNotFoundException: Could not load file or assembly 'CSharpDll, Version=8.80.0.0, Culture=neutral, PublicKeyToken=null' or one of its dependencies. The system cannot find the file specified. at ?A0xb992d574.??__E??_7CAppletAction@CPlusPlusDll@SomeNamespace@@6B@@@YMXXZ() at _initterm_m((fnptr)* pfbegin, (fnptr)* pfend) in f:\dd\vctools\crt_bld\self_x86\crt\src\puremsilcode.cpp:line 219 at .LanguageSupport.InitializeVtables(LanguageSupport* ) in f:\dd\vctools\crt_bld\self_x86\crt\src\mstartup.cpp:line 331 at .LanguageSupport._Initialize(LanguageSupport* ) in f:\dd\vctools\crt_bld\self_x86\crt\src\mstartup.cpp:line 491 at .LanguageSupport.Initialize(LanguageSupport* ) in f:\dd\vctools\crt_bld\self_x86\crt\src\mstartup.cpp:line 702 --- End of inner exception stack trace --- at .ThrowModuleLoadException(String errorMessage, Exception innerException) in f:\dd\vctools\crt_bld\self_x86\crt\src\minternal.h:line 194 at .LanguageSupport.Initialize(LanguageSupport* ) in f:\dd\vctools\crt_bld\self_x86\crt\src\mstartup.cpp:line 712 at .cctor() in f:\dd\vctools\crt_bld\self_x86\crt\src\mstartup.cpp:line 754 --- End of inner exception stack trace --- at System.Runtime.InteropServices.Marshal.ThrowExceptionForHRInternal(Int32 errorCode, IntPtr errorInfo) at System.Runtime.InteropServices.Marshal.ThrowExceptionForHR(Int32 errorCode) at .DoCallBackInDefaultDomain(IntPtr function, Void* cookie) in f:\dd\vctools\crt_bld\self_x86\crt\src\minternal.h:line 406 at .DefaultDomain.Initialize() in f:\dd\vctools\crt_bld\self_x86\crt\src\mstartup.cpp:line 277 at .LanguageSupport.InitializeDefaultAppDomain(LanguageSupport* ) in f:\dd\vctools\crt_bld\self_x86\crt\src\mstartup.cpp:line 342 at .LanguageSupport._Initialize(LanguageSupport* ) in f:\dd\vctools\crt_bld\self_x86\crt\src\mstartup.cpp:line 539 at .LanguageSupport.Initialize(LanguageSupport* ) in f:\dd\vctools\crt_bld\self_x86\crt\src\mstartup.cpp:line 702 --- End of inner exception stack trace --- at .ThrowNestedModuleLoadException(Exception innerException, Exception nestedException) in f:\dd\vctools\crt_bld\self_x86\crt\src\minternal.h:line 184 at .LanguageSupport.Cleanup(LanguageSupport* , Exception innerException) in f:\dd\vctools\crt_bld\self_x86\crt\src\mstartup.cpp:line 662 at .LanguageSupport.Initialize(LanguageSupport* ) in f:\dd\vctools\crt_bld\self_x86\crt\src\mstartup.cpp:line 710 at .cctor() in f:\dd\vctools\crt_bld\self_x86\crt\src\mstartup.cpp:line 754 --- End of inner exception stack trace ---
This is what I'm seeing in the Fusion Log (I've changed the name of the DLL to SomeDLL.dll instead of the original):
*** Assembly Binder Log Entry (8/1/2013 @ 01:47:48 PM) *** The operation failed. Bind result: hr = 0x80070002. The system cannot find the file specified. Assembly manager loaded from: C:\Windows\Microsoft.NET\Framework\v4.0.30319\clr.dll Running under executable c:\users\yshany\documents\visual studio 2012\Projects\MyTester\MyTester\bin\Debug\MyTester.exe --- A detailed error log follows. === Pre-bind state information === LOG: User = WF-IL\yshany LOG: DisplayName = SomeDLL, Version=8.80.0.0, Culture=neutral, PublicKeyToken=null (Fully-specified) LOG: Appbase = file:///c:/users/yshany/documents/visual studio 2012/Projects/MyTester/MyTester/bin/Debug/ LOG: Initial PrivatePath = NULL LOG: Dynamic Base = NULL LOG: Cache Base = NULL LOG: AppName = MyTester.exe Calling assembly : (Unknown). === LOG: This bind starts in default load context. LOG: Using application configuration file: c:\users\yshany\documents\visual studio 2012\Projects\MyTester\MyTester\bin\Debug\MyTester.exe.Config LOG: Using host configuration file: LOG: Using machine configuration file from C:\Windows\Microsoft.NET\Framework\v4.0.30319\config\machine.config. LOG: Policy not being applied to reference at this time (private, custom, partial, or location-based assembly bind). LOG: Attempting download of new URL file:///c:/users/yshany/documents/visual studio 2012/Projects/MyTester/MyTester/bin/Debug/SomeDLL.DLL. LOG: Attempting download of new URL file:///c:/users/yshany/documents/visual studio 2012/Projects/MyTester/MyTester/bin/Debug/SomeDLL/SomeDLL.DLL. LOG: Attempting download of new URL file:///c:/users/yshany/documents/visual studio 2012/Projects/MyTester/MyTester/bin/Debug/SomeDLL.EXE. LOG: Attempting download of new URL file:///c:/users/yshany/documents/visual studio 2012/Projects/MyTester/MyTester/bin/Debug/SomeDLL/SomeDLL.EXE. LOG: All probing URLs attempted and failed.
As you can see, the problem is that the AppBase is where MyTester.exe resides, instead of where SomeDLL.dll resides (which is the same location as the unit test dll). This happens for several DLLs, including both of the DLLs mentioned in the exception above.
I also tried to reproduce with a simpler unit test project (a small VS2012 solution with 3 projects - a C# project which references a C++/CLI project which references another C# project), but the problem did not reproduce and it worked perfecty. As I mentioned before, the unit tests were ok before we upgraded to VS2012 & .NET 4.5.
What can I do? Thanks!
This appears to be a bug in .NET 4.5.
NUnit creates a new app domain to run the unit tests. If the unit test assembly or any of its references are mixed-mode assemblies, it ends up trying to load the mixed-mode assembly's references in the default app domain too, under certain conditions.
The runtime has to initialize the unmanaged c++ code of the mixed mode assembly before it does anything else in that assembly. It does this via the automatically compiled-in LanguageSupport class (the source code for this is distributed with Visual Studio). LanguageSupport::Initialize
is first run in the static constructor of the mixed-mode unit test assembly's compiler-generated .module
class, in the context of the NUnit-created appdomain. LanguageSupport in turn re-triggers the same static constructor in the default appdomain, which finally calls LanguageSupport::Initialize
again. Here's the same call stack from above minus the error handling stuff:
at _initterm_m((fnptr)* pfbegin, (fnptr)* pfend)
at .LanguageSupport.InitializeVtables(LanguageSupport* )
at .LanguageSupport._Initialize(LanguageSupport* )
at .LanguageSupport.Initialize(LanguageSupport* )
at .LanguageSupport.Initialize(LanguageSupport* )
at .DoCallBackInDefaultDomain(IntPtr function, Void* cookie)
at .LanguageSupport.InitializeDefaultAppDomain(LanguageSupport* )
at .LanguageSupport._Initialize(LanguageSupport* )
at .LanguageSupport.Initialize(LanguageSupport* )
at .LanguageSupport.Initialize(LanguageSupport* )
The appdomain that NUnit creates is actually succeeding in loading the unit test assembly and its references (assuming you don't have other problems), but the 2nd LanguageSupport initialization in the default appdomain is failing.
By dumping the IL for the mixed mode assembly, I found that some of the unmanaged classes had a static initializer method automatically generated - these are among the methods that get called in the InitializeVtables method seen 2nd from the top of the call stack. After some trial and error compiling, I discovered that if the unmanaged class has a constructor and at least one virtual method with a .NET type in the signature, the compiler will emit a static initializer for the class.
LanguageSupport::InitializeVtables
calls these static initializer functions. When the initializer runs, it's apparently causing the CLR to try to load the references containing the imported types found in the signatures of the virtual methods of the unmanaged class. Because the default appdomain doesn't have the unit test assemblies and its references in the application base, the call fails and generates the error you see above.
What's more, the error (in the toy app I made, anyway) will only occur if there's another non-vtable initializer that also runs.
Here's the relevant part of my app:
class DomainDumper {
public:
DomainDumper() {
Console::WriteLine("Dumper called from appdomain {0}",
AppDomain::CurrentDomain->Id);
}
};
// comment out this line and InitializeVtables succeeds in default appdomain
DomainDumper dumper;
class CppClassUsingManagedRef {
public:
// comment out this line and the dynamic vtable initializer doesn't get created
CppClassUsingManagedRef(){}
virtual void VirtualMethodWithNoArgs() {}
// comment out this line and the dynamic vtable initializer doesn't get created
virtual void VirtualMethodWithImportedTypeRef(ReferredToClassB^ bref) {}
void MethodWithImportedTypeRef(ReferredToClassB^ bref) {}
};
Workarounds:
<probing>
portion of the app.config file.Object^
and casting to the actual type in the method implementation, which is quite lame but works.If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With