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

我们有一个大型的.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:的类型初始值设定项引发了异常。
---> .ModuleLoadExceptionHandlerException:在导致C ++模块无法加载的主要异常之后发生了嵌套异常。
---> System.TypeInitializationException:的类型初始值设定项引发了异常。
---> .ModuleLoadException:在vtable初始化期间,C ++模块加载失败。
---> System.IO.FileNotFoundException:无法加载文件或程序集“ CSharpDll,版本= 8.80.0.0,区域性=中性,PublicKeyToken =空”或其依赖项之一。该系统找不到指定的文件。
在?A0xb992d574。?? __ E ?? _ 7CAppletAction @ CPlusPlusDll @ SomeNamespace @@ 6B @@@ YMXXZ()
在_initterm_m((fnptr)* pfbegin,(fnptr)* pfend)中的f:\ dd \ vctools \ crt_bld \ self_x86 \ crt \ src \ puremsilcode.cpp:line 219
在f:\ dd \ vctools \ crt_bld \ self_x86 \ crt \ src \ mstartup.cpp中的.LanguageSupport.InitializeVtables(LanguageSupport *):行331
在f:\ dd \ vctools \ crt_bld \ self_x86 \ crt \ src \ mstartup.cpp中的.LanguageSupport._Initialize(LanguageSupport *):行491
在f:\ dd \ vctools \ crt_bld \ self_x86 \ crt \ src \ mstartup.cpp中的.LanguageSupport.Initialize(LanguageSupport *):第702行
---内部异常堆栈跟踪的结尾---
在f:\ dd \ vctools \ crt_bld \ self_x86 \ crt \ src \ minternal.h:line 194中的.ThrowModuleLoadException(String errorMessage,Exception innerException)
在f:\ dd \ vctools \ crt_bld \ self_x86 \ crt \ src \ mstartup.cpp中的.LanguageSupport.Initialize(LanguageSupport *):行712
在f:\ dd \ vctools \ crt_bld \ self_x86 \ crt \ src \ mstartup.cpp中的.cctor()中:线754
---内部异常堆栈跟踪的结尾---
在System.Runtime.InteropServices.Marshal.ThrowExceptionForHRInternal(Int32 errorCode,IntPtr errorInfo)
在System.Runtime.InteropServices.Marshal.ThrowExceptionForHR(Int32 errorCode)
在.DoCallBackInDefaultDomain(IntPtr function,Void * cookie)中的f:\ dd \ vctools \ crt_bld \ self_x86 \ crt \ src \ minternal.h:406行
在.DefaultDomain.Initialize()中的f:\ dd \ vctools \ crt_bld \ self_x86 \ crt \ src \ mstartup.cpp:277行
在.LanguageSupport.InitializeDefaultAppDomain(LanguageSupport *)中的f:\ dd \ vctools \ crt_bld \ self_x86 \ crt \ src \ mstartup.cpp:line 342
在.LanguageSupport._Initialize(LanguageSupport *)中的f:\ dd \ vctools \ crt_bld \ self_x86 \ crt \ src \ mstartup.cpp:line 539
在f:\ dd \ vctools \ crt_bld \ self_x86 \ crt \ src \ mstartup.cpp中的.LanguageSupport.Initialize(LanguageSupport *):第702行
---内部异常堆栈跟踪的结尾---
在.ThrowNestedModuleLoadException(Exception innerException,Exception nestedException)在f:\ dd \ vctools \ crt_bld \ self_x86 \ crt \ src \ minternal.h:line 184
在.LanguageSupport.Cleanup(LanguageSupport *,Exception innerException)在f:\ dd \ vctools \ crt_bld \ self_x86 \ crt \ src \ mstartup.cpp:line 662
在f:\ dd \ vctools \ crt_bld \ self_x86 \ crt \ src \ mstartup.cpp中的.LanguageSupport.Initialize(LanguageSupport *):行710
在f:\ dd \ vctools \ crt_bld \ self_x86 \ crt \ src \ mstartup.cpp中的.cctor()中:线754
---内部异常堆栈跟踪的结尾---

这是我在Fusion Log中看到的(我已将DLL的名称更改为SomeDLL.dll,而不是原始名称):

*** Assembly Binder日志条目(2013年8月1日下午01:47:48)***

操作失败。
绑定结果:hr = 0x80070002。该系统找不到指定的文件。

程序集管理器从以下位置加载:C:\ Windows \ Microsoft.NET \ Framework \ v4.0.30319 \ clr.dll
在可执行文件c:\ users \ yshany \ documents \ visual studio 2012 \ Projects \ MyTester \ MyTester \ bin \ Debug \ MyTester.exe下运行
---详细的错误日志如下。

===预绑定状态信息===
日志:用户= WF-IL \ yshany
日志:DisplayName = SomeDLL,Version = 8.80.0.0,Culture = neutral,PublicKeyToken = null
(完全指定)
日志:Appbase = file:/// c:/ users / yshany / documents / visual studio 2012 / Projects / MyTester / MyTester / bin / Debug /
日志:初始PrivatePath = NULL
日志:动态基准= NULL
日志:缓存基= NULL
日志:AppName = MyTester.exe
调用程序集:(未知)。
===
日志:此绑定在默认的加载上下文中开始。
日志:使用应用程序配置文件:c:\ users \ yshany \ documents \ visual studio 2012 \ Projects \ MyTester \ MyTester \ bin \ Debug \ MyTester.exe.Config
日志:使用主机配置文件:
日志:使用C:\ Windows \ Microsoft.NET \ Framework \ v4.0.30319 \ config \ machine.config中的计算机配置文件。
日志:目前未将策略应用于引用(私有,自定义,部分或基于位置的程序集绑定)。
日志:尝试下载新的URL文件:/// c:/ users / yshany / documents / visual studio 2012 / Projects / MyTester / MyTester / bin / Debug / SomeDLL.DLL。
日志:尝试下载新的URL文件:/// c:/ users / yshany / documents / visual studio 2012 / Projects / MyTester / MyTester / bin / Debug / SomeDLL / SomeDLL.DLL。
日志:尝试下载新的URL文件:/// c:/ users / yshany / documents / visual studio 2012 / Projects / MyTester / MyTester / bin / Debug / SomeDLL.EXE。
日志:尝试下载新的URL文件:/// c:/ users / yshany / documents / visual studio 2012 / Projects / MyTester / MyTester / bin / Debug / SomeDLL / SomeDLL.EXE。
日志:所有探测URL尝试失败。

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

我还尝试使用一个更简单的单元测试项目(一个小型​​的VS2012解决方案,其中包含3个项目-一个C#项目引用一个C ++ / CLI项目,该C ++ / CLI项目引用另一个C#项目)来重现该问题,但是问题没有重现,并且可以完美运行。如前所述,在升级到VS2012和.NET 4.5之前,单元测试还可以。

我能做什么?
谢谢!

参考方案

这似乎是.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^并强制转换为方法实现中的实际类型来实现此目的,虽然这很la脚,但是可以工作。
您可以将有问题的虚拟方法设为非虚拟方法
您可以从非托管的c ++类中删除构造函数

在Java中,执行“ ++++++++”表达式,编译器未报告任何错误并且可以正确执行? - java

我用eclipse编写了这段代码,用war写过,结果为3d。public static void main(String[] args) { double a = 5d + + + + + +-+3d; System.out.println(a); } 参考方案 您的表情可以改写为(5d) + (+ + + + +-+3d) 其中第一个+是应用于两个操作数的…

将数组从非托管C++传递到C# - c#

我是C#的C ++ / CLI包装新手,在将数组传递给C#时遇到一些问题。原生C ++const double *NDimInversion::GetOutputAverage() const { if ( mOutputOk ) return mAvgY.data().begin(); else throw runtime_error("Erro…

在Python和C++之间传输数据而无需写入Windows和Unix文件 - python

我有预先存在的python和C ++文件,其中python文件定义了许多点,而C ++代码利用其现有库进行了所需的计算。最终产品是C ++代码写入的文件。我正在寻找一种在python中获取2000点列表的方法,将其传递给函数,然后执行所有C ++代码并输出我需要的文件。其他注意事项。这必须是可以在Linux或Windows机器上工作的东西,并且最少安装新插件…

Java值加变量++ - java

考虑以下代码int val1 = 3; val1++; int val2 = val1++; System.out.println(val1); System.out.println(val2); Val1值= 5;Val2值= 4;为什么Val1的值是“ 5”?据我了解,应该为4,因为:在第1行,它的赋值为3,在第2行,通过val1 ++加上1,结果val…

如何锁定终端运行的perl,obj c,c++,python和ruby等脚本的源代码? - python

我想出售我在perl,obj c,c ++,python,ruby,bash,php等中制作的脚本等它们都在终端中运行。 (Linux)如何锁定源代码,以便无需人们访问源代码即可分发我的脚本..?换句话说,如何将在Terminal中运行的程序的源代码锁定,以便人们可以使用该程序(如果该代码已下载到他们的Linux机器上,但他们无法访问实际的源代码)?例:ex…