Strange issue with Assembly.Load*

2

I have strange problem with loading assembly from file and unfortunately I can't reproduce it with simple easy-to-share code. I describe what I'm doing right below.

I have 4 applications:

  1. "Complex Invoker" - the foreign(not mine) open source application, which is the computer game actually, for which I'm writing a plugin. This application could invoke several function from dll(see Proxy definition below). To be simple it calls 3 functions init(); release(); DoSomething(); actual names are a little bit different but it doesn't matter. Complex invoker is written in pure unmanaged c\c++ and compiled with MSVC.

  2. "Simple Invoker" - the application I wrote from scratch to test if the Problem (see below) occurs in any way. It does the same - calls 3 functions as in Complex Invoker. Simple invoker is written in pure unmanaged c\c++ and compiled with MSVC.

  3. "Proxy" - the dll which is being called by both of Invokers. It exports the functions init(); release(); DoSomething(); for calling them by Invokers. From the other part here is a managed (CLR) part which is called by init() function. The actual managed class is used to call Assembly.Load(Byte[],Byte[]); This function call loads an Assembly from file (see below) to instantiate class from that assembly. The class from assembly implements the interface "SomeInterface" which is also defined in "Proxy" ("Assembly" has a reference to "Proxy"). Proxy is written in mixed mode(managed+unmanaged) C++ in MSVC with /clr flag.

  4. "Assembly" is dll(managed assembly) with single class which implements "SomeInterface" from Proxy. It is very simple and written with c#.

Now here are my goals. I want the Invoker (complex one specifically) to call proxy which in turn loads Assembly and invoke functions within the class (in Assembly) instance. The key requirement is to be able to "reload" Assembly on demand without reexecuting Invoker. The Invoker has the mechanism to signal the need to reload to Proxy, which in turn do Assembly.Load(Byte[],Byte[]) for Assembly.dll.

So now is the Problem. It works very well with "Simple Invoker" and fails to work with "Complex Invoker"! With Simple Invoker I was able to "reload" (actually load number of Assemblies) the Assembly more then 50 times and with "Complex Invoker" the Assembly.Load() method fails on the first attempt to reload an assembly, i.e. the Proxy loads the Assembly at first time and fails to reload it on demand. It is interesting that Simple and Complex loader do EXACTLY the same function call flow, i.e. LoadLibraryA("Pathto\Proxy.dll"); GetProcAddress for init, release, handleEvent; calling these functions; and after that FreeLibrary(). And I see the problem with inter-operation(dunno what kind) between exactly the Complex Invoker and .NET libs. Complex Invoker has something in it's code what breaks MS .NET correctness. I'm 99% sure it's not my fault as a coder.

Just before I go into deeper details about Exceptions I got and approaches I took to try to overcome such problems (like using AppDomains), I want to clarify If someone on this forum has the ability, time and will to go deep into the System.Load() internals (i.e. this should be the one with System and System.Reflection sources). This would require the installation of "Complex Invoker" and both Proxy and Assembly (don't think it is too tough task).

I'm posting here relevant code snippets.

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 (the Proxy)

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_;

    }

And finally 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
    }
}

If one manage to compile all 3 project it would get it work. It's simple invoker scenario.

Also I have the open-source game which do exactly the same as in Simple Invoker. But after reload is requested (handleEvent(0, 4, NULL) is invoked) I'm getting an error inside the Assembly::Load(Byte[],Byte[]) <-- you can find it in proxy code

The exception is shown below:

"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"

InnerException:

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

Just to be as more precise as possible this code does work with simple Invoker scenario and does work once(first Init) in complex one.


Unfortunately it seems I wasn't as clear as I should have been. I'll try once again.

Imagine you have a "black box" which is doing something it's my proxy.dll. it loads assembly.dll, instantiate object of class from assembly and run it.

Black box has an interface to outside these are functions init, release, DoSomething. if i touch this interface with simple application (no threads, no net code, no mutexes, critical section, etc.) the whole construction DOES work. It means what the black box is done well. In my case I was able "reload" assembly several times (50 times) to be more specific. From the other side I have the complex application which does the EXACTLY the same call flow. But somehow it interferes the way CLR works: the code inside black box stops working. The complex app has threads, tcp/ip code, mutexes, boost, etc. It's very hard to say what exactly prevents correct behavior between application and Proxy.dll. That is why I'm asking if someone has seen what is going on inside the Assembly.Load since I'm getting weird exception when I'm calling exactly this function.

.net
assembly.load
asked on Stack Overflow Feb 11, 2010 by IvanD • edited Dec 22, 2013 by Mat

1 Answer

2

You are probably running in to a load context problem. This blog entry is the best summary out there of how this works. Your use of Assembly.Load(byte[]), I think, might cause multiple instances of the same assembly to end up in the same AppDomain. Though this might seem like what you want, I think it's a recipe for disaster. Consider creating multiple AppDomains instead.

answered on Stack Overflow Feb 11, 2010 by erikkallen

User contributions licensed under CC BY-SA 3.0