C# equivalent of DllMain in C (WinAPI)

I have an older app (ca. 2005) which accepts dll plugins. The app was originally designed for Win32 C plugins, but I have a working C# dll template. My problem: I need to do some one-time initialization, which in a Win32 C dll would be done in DllMain:

BOOL APIENTRY DllMain(HANDLE hModule, DWORD ul_reason_for_call, LPVOID lpReserved) {
  [one-time stuff here...]
}

Is there a C# equivalent of this? There is no "DllMain" in the C# template I have. I tried a literal C# interpretation, but no go: the dll works but it won't trigger the DllMain function.

public static bool DllMain(int hModule, int reason, IntPtr lpReserved) {
  [one time stuff here...]
}

Solution 1:

Give your class a static constructor and do your initialization there. It will run the first time anybody calls a static method or property of your class or constructs an instance of your class.

Solution 2:

I've had to interact with a legacy application probably in the same situation as you have. I've found a hacky way to get DllMain functionality in a CLR assembly. Luckily it isn't too hard. It requires an additional DLL but it doesn't require you to deploy an additional DLL so you can still have the "put a DLL in that directory and the app will load it" paradigm.

First, you make a simple regular C++ DLL that looks like the following:

dllmain.cpp:

#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#include "resource.h"
extern void LaunchDll(
  unsigned char *dll, size_t dllLength, 
  char const *className, char const *methodName);
static DWORD WINAPI launcher(void* h)
{
    HRSRC res = ::FindResourceA(static_cast<HMODULE>(h),
                   MAKEINTRESOURCEA(IDR_DLLENCLOSED), "DLL");
    if (res)
    {
        HGLOBAL dat = ::LoadResource(static_cast<HMODULE>(h), res);
        if (dat)
        {
            unsigned char *dll =
                static_cast<unsigned char*>(::LockResource(dat));
            if (dll)
            {
                size_t len = SizeofResource(static_cast<HMODULE>(h), res);
                LaunchDll(dll, len, "MyNamespace.MyClass", "DllMain");
            }
        }
    }
    return 0;
}
extern "C" BOOL APIENTRY DllMain(HMODULE h, DWORD reasonForCall, void* resv)
{
    if (reasonForCall == DLL_PROCESS_ATTACH)
    {
        CreateThread(0, 0, launcher, h, 0, 0);
    }
    return TRUE;
}

Note the thread creation. This is to keep Windows happy because calling managed code within a DLL entrypoint is a no-no.

Next, you have to create that LaunchDll function the code above references. This goes in a separate file because it will be compiled as a managed C++ unit of code. To do this, first create the .cpp file (I called it LaunchDll.cpp). Then right click on that file in your project and in Configuration Properties-->C/C++-->General change the Common Language RunTime Support entry to Common Language RunTime Support (/clr). You can't have exceptions, minimal rebuild, runtime checks and probably some other things I forgot about but the compiler will tell you about. When the compiler complains, track down what settings you much change from the default and change them on the LaunchDll.cpp file only.

LaunchDll.cpp:

#using <mscorlib.dll>
// Load a managed DLL from a byte array and call a static method in the DLL.
// dll - the byte array containing the DLL
// dllLength - the length of 'dll'
// className - the name of the class with a static method to call.
// methodName - the static method to call. Must expect no parameters.
void LaunchDll(
    unsigned char *dll, size_t dllLength,
    char const *className, char const *methodName)
{
    // convert passed in parameter to managed values
    cli::array<unsigned char>^ mdll = gcnew cli::array<unsigned char>(dllLength);
    System::Runtime::InteropServices::Marshal::Copy(
        (System::IntPtr)dll, mdll, 0, mdll->Length);
    System::String^ cn =
        System::Runtime::InteropServices::Marshal::PtrToStringAnsi(
            (System::IntPtr)(char*)className);
    System::String^ mn =
        System::Runtime::InteropServices::Marshal::PtrToStringAnsi(
            (System::IntPtr)(char*)methodName);

    // used the converted parameters to load the DLL, find, and call the method.
    System::Reflection::Assembly^ a = System::Reflection::Assembly::Load(mdll);
    a->GetType(cn)->GetMethod(mn)->Invoke(nullptr, nullptr);
}

Now for the really tricky part. You probably noticed the resource loading in dllmain.cpp:launcher(). What this does is retrieve a second DLL that has been inserted as a resource into the DLL getting created here. To do this, create a resource file by doing the right click-->Add-->New Item-->Visual C++-->Resource-->Resource File (.rc) thing. Then, you need to make sure there is a line like:

resource.rc:

IDR_DLLENCLOSED DLL "C:\\Path\\to\\Inner.dll"

in the file. (Tricky, huh?)

The only thing left to do is to create that Inner.dll assembly. But, you already have it! This is what you were trying to launch with your legacy app in the first place. Just make sure to include a MyNamespace.MyClass class with a public void DllMain() method (of course you can call these functions whatever you want to, these are just the values hardcoded into dllmain.cpp:launcher() above.

So, in conclusion, the code above takes an existing managed DLL, inserts it into a resource of an unmanaged DLL which, upon getting attached to a process, will load the managed DLL from the resource and call a method in it.

Left as an exercise to the reader is better error checking, loading different DLLs for Debug and Release, etc. mode, calling the DllMain substitute with the same arguments passed to the real DllMain (the example only does it for DLL_PROCESS_ATTACH), and hardcoding other methods of the inner DLL in the outer DLL as pass through methods.

Solution 3:

Also not easy to do from C# you can have a per module initializers

Modules may contain special methods called module initializers to initialize the module itself. All modules may have a module initializer. This method shall be static, a member of the module, take no parameters, return no value, be marked with rtspecialname and specialname, and be named .cctor. There are no limitations on what code is permitted in a module initializer. Module initializers are permitted to run and call both managed and unmanaged code.