Подтвердить что ты не робот

DLL, загруженные из неправильной AppplicationBase при попытке загрузить смешанные библиотеки С# и С++/CLI в новый AppDomain

У нас есть большое .NET-решение с проектами С# и С++/CLI, которые ссылаются друг на друга. У нас также есть несколько проектов тестирования модулей. Недавно мы обновили Visual Studio 2010 и .NET 4.0 до Visual Studio 4.5 и .NET 4.5, и теперь, когда мы пытаемся запустить модульные тесты, по-видимому, возникает проблема с загрузкой некоторых DLL во время теста.

Проблема возникает, потому что модульное тестирование выполняется на отдельном AppDomain. Процесс модульного тестирования (например, nunit-agent.exe) создает новый AppDomain с AppBase, установленным для местоположения тестового проекта, но, согласно журналу Fusion, некоторые библиотеки DLL загружаются с исполняемым каталогом nunit в качестве AppBase вместо AppDomain AppBase.

Мне удалось воспроизвести проблему с более простым сценарием, который создает новый AppDomain и пытается запустить там тест. Вот как это выглядит (я изменил имена классов unit test, методы и расположение 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);
        }
    }
}

Это исключение, которое я получаю при попытке выполнить unit test. Как вы можете видеть, проблема возникает, когда инициализируется dll С++ и пытается загрузить С# 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 [email protected]@[email protected]@[email protected]@@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 unit test). Это происходит для нескольких DLL, включая обе библиотеки DLL, упомянутые в вышеприведенном исключении.

Я также попытался воспроизвести более простой проект unit test (небольшое решение VS2012 с 3 проектами - проект С#, который ссылается на проект С++/CLI, который ссылается на другой проект С#), но проблема не воспроизводилась, и она работала perfecty. Как я уже упоминал ранее, модульные тесты были одобрены до того, как мы обновились до VS2012 и .NET 4.5.

Что я могу сделать? Спасибо!

4b9b3361

Ответ 1

Это, как представляется, ошибка в .NET 4.5.

NUnit создает новый домен приложения для запуска модульных тестов. Если сборка unit test или любая ее ссылка являются сборками в смешанном режиме, то в определенных условиях она также пытается загрузить ссылки на сборку смешанного режима в домене приложения по умолчанию.

Среда выполнения должна инициализировать неуправляемый код С++ сборки смешанного режима, прежде чем он сделает что-либо еще в этой сборке. Он делает это через автоматически скомпилированный класс LanguageSupport (исходный код для этого распространяется вместе с Visual Studio). LanguageSupport::Initialize сначала запускается в статическом конструкторе смешанного режима unit test скомпилированного компилятором класса .module в контексте созданного NUnit appdomain. 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, на самом деле преуспевает в загрузке сборки unit test и ее ссылок (при условии, что у вас нет других проблем), но инициализация 2-го языкаSupport в домене appdomain по умолчанию не работает.

Отбрасывая IL для сборки смешанного режима, я обнаружил, что некоторые неуправляемые классы автоматически генерировали статический метод инициализации - они относятся к методам, которые вызываются в методе InitializeVtables, которые видятся 2-м сверху в стеке вызовов, После некоторого компиляции проб и ошибок я обнаружил, что если неуправляемый класс имеет конструктор и по крайней мере один виртуальный метод с типом .NET в сигнатуре, компилятор будет выдавать статический инициализатор для класса.

LanguageSupport::InitializeVtables вызывает эти статические инициализационные функции. Когда инициализатор работает, это, по-видимому, заставляет CLR пытаться загрузить ссылки, содержащие импортированные типы, найденные в подписях виртуальных методов неуправляемого класса. Поскольку в домене appdomain нет компоновки unit test и его ссылок в базе приложения, вызов завершается с ошибкой и генерируется ошибка, которую вы видите выше.

Что еще, ошибка (в приложении для игрушек, которую я сделал, в любом случае) произойдет только в том случае, если также выполняется другой инициализатор, отличный от 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 (вряд ли, я думаю), вы можете изменить часть <probing> файла app.config файл.
  • Вы можете скопировать nunit и его зависимости в каталог unit test или наоборот
  • Вы можете изменить виртуальные методы в неуправляемых классах С++, чтобы исключить ссылки на типы, которые NUnit не сможет загрузить. Вы можете сделать это, ограничив себя Object^ и отбрасывая фактический тип реализации метода, который является довольно хромым, но работает.
  • Вы можете сделать виртуальный метод под вопросом не виртуальным
  • Вы можете удалить конструктор из неуправляемого класса С++