Assembly.Load* 的奇怪问题

Posted

技术标签:

【中文标题】Assembly.Load* 的奇怪问题【英文标题】:Strange issue with Assembly.Load* 【发布时间】:2010-02-11 12:06:46 【问题描述】:

我在从文件加载程序集时遇到了奇怪的问题,不幸的是,我无法使用简单且易于共享的代码来重现它。我在下面描述了我正在做的事情。

我有 4 个应用程序:

    “Complex Invoker” - 外国(不是我的)开源应用程序,实际上是电脑游戏,我正在为此编写插件。此应用程序可以从 dll 调用多个函数(请参阅下面的代理定义)。为简单起见,它调用了 3 个函数 init();释放();做点什么();实际名称略有不同,但没关系。复杂的调用程序是用纯非托管 c\c++ 编写并使用 MSVC 编译的。

    “Simple Invoker” - 我从头开始编写的应用程序,用于测试问题(见下文)是否以任何方式发生。它的作用相同 - 调用 3 个函数,就像在 Complex Invoker 中一样。简单的调用程序是用纯非托管 c\c++ 编写并用 MSVC 编译的。

    “代理” - 被两个调用者调用的 dll。它导出函数 init();释放();做点什么();调用者调用它们。这里的另一部分是由 init() 函数调用的托管 (CLR) 部分。实际托管类用于调用 Assembly.Load(Byte[],Byte[]);此函数调用从文件(见下文)加载程序集以从该程序集中实例化类。来自程序集的类实现接口“SomeInterface”,该接口也在“Proxy”中定义(“Assembly”引用了“Proxy”)。代理是在带有 /clr 标志的 MSVC 中以混合模式(托管+非托管)C++ 编写的。

    “Assembly”是具有单个类的 dll(托管程序集),它从 Proxy 实现“SomeInterface”。很简单,用c#写的。

现在这是我的目标。我希望调用者(特别是复杂的)调用代理,代理依次加载程序集并调用类(在程序集中)实例中的函数。关键要求是能够按需“重新加载”程序集,而无需重新执行 Invoker。 Invoker 具有指示需要重新加载到 Proxy 的机制,而后者又会为 Assembly.dll 执行 Assembly.Load(Byte[],Byte[])。

所以现在是问题所在。它与“Simple Invoker”配合得很好,但不能与“Complex Invoker”配合使用! 使用简单调用程序,我能够“重新加载”(实际加载程序集的数量)超过 50 次,使用“复杂调用程序”,Assembly.Load() 方法在第一次尝试重新加载程序集时失败,即代理加载大会第一次并未能按需重新加载。 有趣的是,简单和复杂加载器执行完全相同的函数调用流程,即 LoadLibraryA("Pathto\Proxy.dll"); GetProcAddress 用于 init、release、handleEvent;调用这些函数;之后是 FreeLibrary()。而且我看到了复杂调用程序和 .NET 库之间的互操作问题(不知道是什么样的)。 Complex Invoker 的代码中有一些东西破坏了 MS .NET 的正确性。我 99% 确定这不是我作为程序员的错。

就在我更深入地了解我遇到的异常以及我为克服这些问题(例如使用 AppDomains)而采取的方法之前,我想澄清一下,如果这个论坛上的某个人有能力、时间和意愿深入研究System.Load() 内部(即,这应该是具有 System 和 System.Reflection 源的那个)。这将需要安装“Complex Invoker”以及 Proxy 和 Assembly(不要认为这是太艰巨的任务)。

我在这里发布相关代码sn-ps。

InvokerSimple

DWORD WINAPI DoSomething( LPVOID lpParam )

    HMODULE h=LoadLibraryA("PathTo\\CSProxyInterface.dll"); //this is Proxy!


    initType init=(initType)GetProcAddress(h,"init");
    releaseType release=(releaseType)GetProcAddress(h,"release");
    handleEventType handleEvent=(handleEventType)GetProcAddress(h,"handleEvent");    



    init(0,NULL);

    int r;

    for (int i=0;i<50;i++)
    
        r=handleEvent(0,0,NULL); //causes just Assembly.SomeFunction invoke
        r=handleEvent(0,0,NULL); //causes just Assembly.SomeFunction invoke
        r=handleEvent(0,0,NULL); //causes just Assembly.SomeFunction invoke
        r=handleEvent(0,4,NULL); //causes Assembly reload (inside the Proxy)
    

    release(0);

    FreeLibrary(h);

    return 0;



int _tmain(int argc, _TCHAR* argv[])

    bool Threaded=true; //tried both Threaded and unThreaded. They work fine!

    if (Threaded)
    
        DWORD threadID;
        HANDLE threadHNDL;
        threadHNDL=CreateThread(NULL,0,&DoSomething,NULL,0, &threadID);
        WaitForSingleObject(threadHNDL,INFINITE);
    
    else
    
        DoSomething(NULL);
    

    return 0;

CSProxyInterface(代理)

stdafx.h

int Safe_init(void);
int Safe_release(void);

extern "C" __declspec(dllexport) int __cdecl init(int teamId, const void* callback);
extern "C" __declspec(dllexport) int __cdecl release(int teamId);
extern "C" __declspec(dllexport) int __cdecl handleEvent(int teamId, int topic, const void* data);

stdafx.cpp

#include "stdafx.h"
#include "WrapperClass.h"

#include <vcclr.h>

using namespace System;
using namespace System::Diagnostics;
using namespace System::Threading;

// TODO: reference any additional headers you need in STDAFX.H
// and not in this file

#define CSMAXPATHLEN 8192

static gcroot<WrapperClass^> wc;
static char* my_filename="PathTo\\Assembly.dll";

int __cdecl init( int teamId, const void* callback )

    return Safe_init();


int Safe_init( void )

    Safe_release();
    wc=gcnew WrapperClass(gcnew String(my_filename));
    return 0;


int __cdecl release( int teamId )

    return Safe_release();


int Safe_release( void )

    if (static_cast<WrapperClass^>(wc)!=nullptr)
    
        delete wc;
        wc=nullptr;
    
    return 0;


int __cdecl handleEvent( int teamId, int topic, const void* data )


    if (topic!=4)
    
        int r=wc->Do(topic);
        return r;
    
    else
               
        Safe_init();
        return 0;
    


WrapperClass.h

#pragma once

using namespace System;
using namespace System::Reflection;

#include "SomeInterface.h"

ref class WrapperClass

private:
    ResolveEventHandler^ re;
    Assembly^ assembly;
    static Assembly^ assembly_interface;
    SomeInterface^ instance;
private:
    Assembly^ LoadAssembly(String^ filename_dll);
    static Assembly^ MyResolveEventHandler(Object^ sender, ResolveEventArgs^ args);
    static bool MyInterfaceFilter(Type^ typeObj, Object^ criteriaObj);
public:
    int Do(int i);
public:
    WrapperClass(String^ dll_path);
    !WrapperClass(void);
    ~WrapperClass(void) this->!WrapperClass();;
;

WrapperClass.cpp

#include "StdAfx.h"
#include "WrapperClass.h"    
WrapperClass::WrapperClass(String^ dll_path)
    
        re=gcnew ResolveEventHandler(&MyResolveEventHandler);
        AppDomain::CurrentDomain->AssemblyResolve +=re;

        assembly=LoadAssembly(dll_path);

        array<System::Type^>^ types;

        try
               
            types=assembly->GetExportedTypes(); 
        
        catch (Exception^ e)
        
            throw e;        
        

        for (int i=0;i<types->Length;i++)
        
            Type^ type=types[i];

            if ((type->IsClass))
            
                String^ InterfaceName = "SomeInterface";

                TypeFilter^ myFilter = gcnew TypeFilter(MyInterfaceFilter);
                array<Type^>^ myInterfaces = type->FindInterfaces(myFilter, InterfaceName);

                if (myInterfaces->Length==1) //founded the correct interface                
                
                    Object^ tmpObj=Activator::CreateInstance(type);
                    instance = safe_cast<SomeInterface^>(tmpObj);
                

            
        
    

    bool WrapperClass::MyInterfaceFilter(Type^ typeObj, Object^ criteriaObj)
    
        return (typeObj->ToString() == criteriaObj->ToString());
    

    WrapperClass::!WrapperClass(void)
    
        AppDomain::CurrentDomain->AssemblyResolve -=re;
        instance=nullptr;
        assembly=nullptr;
    

    int WrapperClass::Do( int i )
    
        return instance->Do();
    

    Assembly^ WrapperClass::MyResolveEventHandler(Object^ sender, ResolveEventArgs^ args)
    
        Assembly^ return_=nullptr;

        array<Assembly^>^ assemblies=AppDomain::CurrentDomain->GetAssemblies();

        for (int i=0;i<assemblies->Length;i++)
        
            if (args->Name==assemblies[i]->FullName)
            
                return_=assemblies[i];
                break;
            
        


        return return_;
    


    Assembly^ WrapperClass::LoadAssembly(String^ filename_dll)
    
        Assembly^ return_=nullptr;

        String^ filename_pdb=IO::Path::ChangeExtension(filename_dll, ".pdb");

        if (IO::File::Exists(filename_dll))
        
            IO::FileStream^ dll_stream = gcnew IO::FileStream(filename_dll, IO::FileMode::Open, IO::FileAccess::Read);
            IO::BinaryReader^ dll_stream_bytereader = gcnew IO::BinaryReader(dll_stream);
            array<System::Byte>^ dll_stream_bytes = dll_stream_bytereader->ReadBytes((System::Int32)dll_stream->Length);
            dll_stream_bytereader->Close();
            dll_stream->Close();

            if (IO::File::Exists(filename_pdb))
            
                IO::FileStream^ pdb_stream = gcnew IO::FileStream(filename_pdb, IO::FileMode::Open, IO::FileAccess::Read);
                IO::BinaryReader^ pdb_stream_bytereader = gcnew IO::BinaryReader(pdb_stream);
                array<System::Byte>^ pdb_stream_bytes = pdb_stream_bytereader->ReadBytes((System::Int32)pdb_stream->Length);
                pdb_stream_bytereader->Close();
                pdb_stream->Close();

                try
                
                    //array<Assembly^>^ asses1=AppDomain::CurrentDomain->GetAssemblies();
                    return_=Assembly::Load(dll_stream_bytes,pdb_stream_bytes);
                    //array<Assembly^>^ asses2=AppDomain::CurrentDomain->GetAssemblies();
                
                catch (Exception^ e)
                   
                    //array<Assembly^>^ asses3=AppDomain::CurrentDomain->GetAssemblies();
                    throw e;
                           
            
            else
            
                try
                
                    return_=Assembly::Load(dll_stream_bytes);
                
                catch (Exception^ e)
                
                    throw e;
                
            
         
        return return_;

    

最后是 Assembly.dll

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace Assembly

    public class Class1:SomeInterface
    
        private int i;

        #region SomeInterface Members

        public int Do()
        

            return i--;
        

        #endregion
    

如果一个人设法编译所有 3 个项目,它就会工作。这是简单的调用程序场景。

我还有一个与 Simple Invoker 完全相同的开源游戏。但是在请求重新加载之后(调用了handleEvent(0, 4, NULL))我在Assembly::Load(Byte[],Byte[]) 中遇到了一个错误

异常如下图:

"Could not load file or assembly '4096 bytes loaded from CSProxyInterface, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null' or one of its dependencies. Exception from HRESULT: 0x800703E6"

内部异常:

0x1a158a78  "Could not load file or assembly 'sorttbls.nlp' or one of its dependencies. Exception from HRESULT: 0x800703E6"

为了尽可能准确,这段代码确实适用于简单的 Invoker 场景,并且在复杂的场景中运行一次(第一次 Init)。


不幸的是,我似乎并没有像我应该做的那样清楚。我会再试一次。

假设您有一个“黑匣子”正在做某事,它是我的 proxy.dll。它加载 assembly.dll,从程序集中实例化类对象并运行它。

黑盒有一个外部接口,这些函数是init、release、DoSomething。如果我用简单的应用程序(没有线程,没有网络代码,没有互斥体,关键部分等)触摸这个界面,整个构造就可以工作。这意味着黑匣子做得很好。在我的情况下,我能够“重新加载”程序集多次(50 次)更具体。另一方面,我有一个复杂的应用程序,它执行完全相同的调用流程。但不知何故,它干扰了 CLR 的工作方式:黑盒内的代码停止工作。复杂的应用程序有线程、tcp/ip 代码、互斥体、boost 等。很难说究竟是什么阻止了应用程序和 Proxy.dll 之间的正确行为。这就是为什么我要问是否有人看到了 Assembly.Load 内部发生的事情,因为当我完全调用这个函数时,我遇到了奇怪的异常。

【问题讨论】:

这些例外情况对于帮助我们帮助您至关重要。 【参考方案1】:

您可能遇到了负载上下文问题。 This blog entry 是对其工作原理的最佳总结。我认为,您对 Assembly.Load(byte[]) 的使用可能会导致同一程序集的多个实例最终出现在同一个 AppDomain 中。虽然这看起来像是你想要的,但我认为这是灾难的根源。考虑改为创建多个 AppDomain。

【讨论】:

以上是关于Assembly.Load* 的奇怪问题的主要内容,如果未能解决你的问题,请参考以下文章

Assembly.Load

多个 Assembly.Load(Byte[]),相同的实例或泄漏?

C# Assembly.Load() 方法加载了错误的 dll?

java 工厂assembly.load怎么用

关于C#反射Assembly.load() .CreateInstance() 未报错, 返回对象为空

如果在 Windows 服务 .net 可执行文件中调用 .NET Assembly.Load/LoadFrom 会失败