尝试在新的AppDomain中加载混合的C#和C ++ / CLI dll时从错误的AppplicationBase加载的DLL

时间:2013-08-01 11:38:34

标签: c# unit-testing c++-cli .net-4.5 appdomain

我们有一个大型的.NET解决方案,其中包含相互引用的C#和C ++ / CLI项目。 我们还有几个单元测试项目。我们最近从Visual Studio 2010& .NET 4.0到Visual Studio 4.5& .NET 4.5,现在当我们尝试运行单元测试时,在测试期间加载某些DLL似乎存在问题。

问题似乎发生,因为单元测试是在单独的AppDomain上执行的。单元测试过程(例如nunit-agent.exe)创建一个新的AppDomain,AppBase设置为测试项目的位置,但是根据Fusion Log,一些DLL加载了nunit的可执行文件目录作为AppBase而不是AppDomain的AppBase

我设法用更简单的方案重现问题,这会创建一个新的AppDomain并尝试在那里运行测试。这是它的外观(我更改了单元测试类的名称,方法和dll的位置以保护无辜者):

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);
        }
    }
}

这是我尝试执行单元测试时遇到的异常。正如您所看到的,问题发生在C ++ dll初始化并尝试加载C#dll(我将所涉及的DLL的名称更改为CPlusPlusDll和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 ---

这就是我在Fusion日志中看到的(我已经将DLL的名称更改为SomeDLL.dll而不是原始版本):

*** 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.

正如您所看到的,问题是AppBase是MyTester.exe所在的位置,而不是SomeDLL.dll所在的位置(与单元测试dll的位置相同)。这种情况发生在几个DLL上,包括上面例外中提到的两个DLL。

我还尝试使用更简单的单元测试项目(一个包含3个项目的小型VS2012解决方案 - 一个引用另一个C#项目的C ++ / CLI项目的C#项目)重现,但问题没有重现并且它工作得很完美。正如我之前提到的,在我们升级到VS2012&之前,单元测试还可以。 .NET 4.5。

我该怎么办? 谢谢!

1 个答案:

答案 0 :(得分:13)

这似乎是.NET 4.5中的一个错误。

NUnit创建一个新的应用程序域来运行单元测试。如果单元测试程序集或其任何引用是混合模式程序集,它最终会在某些条件下尝试在默认应用程序域中加载混合模式程序集的引用。

运行时必须初始化混合模式程序集的非托管c ++代码,然后才能在该程序集中执行任何其他操作。它通过自动编译的LanguageSupport类(其源代码随Visual Studio分发)完成此操作。 LanguageSupport::Initialize首先在NUnit创建的appdomain的上下文中在混合模式单元测试程序集的编译器生成的.module类的静态构造函数中运行。 LanguageSupport反过来在默认appdomain中重新触发相同的静态构造函数,最终再次调用LanguageSupport::Initialize。这是上面相同的调用堆栈减去错误处理的东西:

   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* )

NUnit创建的appdomain实际上是在成功加载单元测试程序集及其引用(假设您没有其他问题),但默认appdomain中的第二语言支持初始化失败。

通过转储IL用于混合模式程序集,我发现一些非托管类具有自动生成的静态初始化方法 - 这些是在调用堆栈顶部看到的InitializeVtables方法中调用的方法之一。经过一些试验和错误编译后,我发现如果非托管类在签名中有一个构造函数和至少一个.NET类型的虚方法,编译器将为该类发出一个静态初始值设定项。

LanguageSupport::InitializeVtables调用这些静态初始化函数。当初始化程序运行时,它显然导致CLR尝试加载包含在非托管类的虚方法的签名中找到的导入类型的引用。由于默认appdomain在应用程序库中没有单元测试程序集及其引用,因此调用失败并生成您在上面看到的错误。

更重要的是,错误(无论如何,我在玩具应用程序中)只会出现另一个同时运行的非vtable初始化程序。

以下是我的应用的相关部分:

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) {}
};

解决方法:

  • 如果您的单元测试位于NUnit可执行文件的子目录中(我猜不太可能),您可以modify the <probing> portion of the app.config file
  • 您可以将nunit及其依赖项复制到单元测试目录,反之亦然
  • 您可以修改非托管c ++类中的虚方法,以排除对NUnit无法加载的类型的引用。您可以通过将自己限制为Object^并将其转换为方法实现中的实际类型来实现此目的,这种方法非常蹩脚但有效。
  • 您可以将有问题的虚拟方法设为非虚拟方法
  • 您可以从unamanaged c ++类中删除构造函数