13

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...]
}
MrSparkly
  • 627
  • 1
  • 7
  • 17
  • one-time initialization of what? Since DllMain is procedural, and C# is object oriented, I wonder what initialization you need to perform outside of a class that couldn't be called from inside it? – Ritch Melton Nov 21 '11 at 02:53
  • it also depends on the type of application you are building, console, web, windows client, windows service, etc. – Brian Mains Nov 21 '11 at 02:55
  • 1
    You shouldn't be doing significant initialization in `DllMain`. – ta.speot.is Nov 21 '11 at 03:03
  • @Ritch Melton think about things like plugin loading where the hosting app doesn't know exactly what is in the DLL. In that case you might want your plugin to register with the application so that they can be found when needed without tying the host to the plugin. – Yaur Nov 21 '11 at 03:20

4 Answers4

17

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.

Raymond Chen
  • 44,448
  • 11
  • 96
  • 135
13

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.

mheyman
  • 4,211
  • 37
  • 34
  • Or run a post-build step to tweak your C# assembly: http://stackoverflow.com/questions/1915506 – user423430 Jan 22 '13 at 15:44
  • @mheyman: But the new "wrapper.dll" cannot be used like the inner.dll would have been used by the legacy application. The inner.dll is a c# assembly, my legacy app is calling Assembly.GetTypes() on the inner.dll and if I'm using the wrapper.dll instead, it will return some c++/clr stuff instead of the inner.dll's types. That's a big problem for me, how do I expose the inner.dll's types? – Blub Oct 10 '14 at 08:52
  • @Blud did you find a solution to this problem? – MemCtrl Jun 27 '17 at 13:25
  • The assembly is loaded, so you can find it by iterating all the loaded assemblies. But, using the [Fody](https://www.nuget.org/packages/ModuleInit.Fody) nuget package seems the cleanest solution unless you need to call an unmanaged DLL. – mheyman Jul 06 '17 at 22:16
6

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.

BendEg
  • 20,098
  • 17
  • 57
  • 131
parapura rajkumar
  • 24,045
  • 1
  • 55
  • 85
  • How would you make use of that? – Ritch Melton Nov 21 '11 at 02:55
  • I think you might have to disassemble and insert IL code directly. – parapura rajkumar Nov 21 '11 at 02:58
  • parapura is correct there is no way to do this without modifying the IL directly... hopefully this gets fixed someday. – Yaur Nov 21 '11 at 03:13
  • 2
    See http://stackoverflow.com/a/9739535/240845 (use Einar Egilsson's InjectModuleInitializer) – mheyman Mar 16 '12 at 21:12
  • 1
    Module initializers are not always a good substitute for DllMain functionality because they do not get called until a method gets called in the DLL. If you have a situation where a legacy app just loads a DLL and expects it to do something, module initializers will fail you (as they did me). See http://stackoverflow.com/a/9745422/240845 elsewhere in this question for a solution to this problem. – mheyman Mar 16 '12 at 22:53
3

Even though C# doesn't directly support module initialization we can implement it using reflection and static constructors. To do this we can define a custom attribute and use it find classes that need to be initialized on module loading:

public class InitOnLoadAttribute : Attribute {}

private void InitAssembly(Assembly assembly)
{
    foreach (var type in GetLoadOnInitTypes(assembly)){
        var prop = type.GetProperty("loaded", BindingFlags.Static | BindingFlags.NonPublic); //note that this only exists by convention
        if(prop != null){
            prop.GetValue(null, null); //causes the static ctor to be called if it hasn't already
        }
    }
 }

static IEnumerable<Type> GetLoadOnInitTypes(Assembly assembly)
{
    foreach (Type type in assembly.GetTypes())
    {
        if (type.GetCustomAttributes(typeof(InitOnLoadAttribute), true).Length > 0){
            yield return type;
        }
    }
}

public MyMainClass()
{
    //init newly loaded assemblies
    AppDomain.CurrentDomain.AssemblyLoad += (s, o) => InitAssembly(o.LoadedAssembly); 
    //and all the ones we currently have loaded
    foreach(var assembly in AppDomain.CurrentDomain.GetAssemblies()){
        InitAssembly(assembly);
    }
}

on classes that we need to initialize immediately we add that code to their static constructor (which will be run once even if the property getter is accessed multiple times) and add the custom attribute we added to expose this functionality.

[InitOnLoad]
class foo
{
    private static bool loaded { get { return true; } }
    static foo() 
    {
        int i = 42;
    }
}
Yaur
  • 7,333
  • 1
  • 25
  • 36
  • 1
    This doesn't answer the question as the OP is looking for a way to interact with a legacy application. – Ritch Melton Nov 22 '11 at 00:30
  • @RitchMelton I don't follow. The OP is trying to do one time initialization on managed DLLs either from a managed application or from an unmanaged application that is hosting the CLR. In the first case you would add the code in the MyMainClass constructor near start up and in the second you would most likely put it in the method you initially pass to `ExecuteInDefaultAppDomain`. In either case you get the functionality the OP is looking for. – Yaur Nov 22 '11 at 05:40
  • 1
    @RitchMelton He is trying to replicate the functionality of DllMain in a "C# dll" I still don't see how its relevant whether the hosting process is managed or not. – Yaur Nov 22 '11 at 12:18