DLL Startup

Often, the majority of the executed code is not written in the application itself, but in various DLLs that the application loads. There are significant differences between application startup and DLL startup. When a mixed-code EXE file is loaded to start an application, the CLR is automatically initialized. In mixed-code DLLs, this can be different. Mixed-code DLLs can be used to delay-load the CLR. This means that the CLR is initialized only when managed code is executed. In addition to that, DLL startup code is executed with special restrictions that must be considered when writing mixed-code DLLs. To understand how this delay-loading feature works, and to avoid some critical initialization pitfalls, it is necessary to discuss the startup of DLLs, too.

DLLs can also have a PE entry point. The signature for a DLL entry point is somewhat more complex:

BOOL _stdcall PEEntryPoint_DLL(

HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpvReserved

Unlike an application's entry point, a DLL entry point is called more than once. It is called once when the DLL is loaded into a process and once when it is unloaded. Furthermore, it can be called twice for each thread created after the DLL is loaded: once when the thread is starting up and once when it is shutting down.

Many developers know this signature from a function named DllMain, but precisely spoken, DllMain is usually not the PE entry point of a DLL. For native DLLs, the entry point is usually a function named _DllMainCRTStartup. It is the task of this function to initialize the CRT at startup and to perform the CRT deinitialization when the DLL is unloaded. The programmer can implement a function named DllMain to do custom initialization and uninitialization. When _DllMainCRTStartup has initialized the CRT, it forwards the call to DllMain. When _DllMainCRTStartup is called due to a DLL unloading, it first calls DllMain, and then it performs the uninitialization of the CRT.

For mixed-code DLLs, there is an additional layer. When the linker produces a DLL assembly, a function named _CorDllMain is used as the PE entry point. This function enables the delay-loading of the CLR. Instead of initializing the CLR directly, it patches all thunks for managed functions that can be handed out to the native world. In Chapter 9, I explained that the compiler and linker generate .vtfixup metadata and an interoperability vtable for every managed function that can be called from native code. Each of the interoperability vtables is patched during mixed-code DLL startup. This patch introduces some code that loads the CLR if it has not been loaded already and that performs the initialization of the managed parts of an assembly if this has not been done before.

The following sample code shows a DLL that can delay-load the CLR:

extern "C" _declspec(dllexport)

void _stdcall fManaged()

System::Console::WriteLine("fManaged called");

In this code, a managed function is exported by a DLL. Since the managed function has a native calling convention, the client can be a native application. When Lib1.dll is loaded, _CorDllMain patches the DLL's entry point for fManaged.

In the next example, delay-loading the CLR can occur because a managed method is called inside the DLL's code. Assume you have a DLL with one native exported function.

// Lib2NativeParts.cpp

// compile with "cl /c /MD Lib2NativeParts.cpp" #include <stdio.h>

void _stdcall fManaged(); // implemented in lib2ManagedParts.cpp void _stdcall fNative(); // implemented in this file extern "C" _declspec(dllexport)

fManaged(); else fNative();

void _stdcall fNative()

printf("fNative called\n");

This code exports a native function void __stdcall f(bool b). Since the compiler flag /clr is not used here, all code in Lib2NativeParts.cpp is compiled to native code. Depending on the argument passed, f internally calls fManaged or fNative. Both functions can be called because they both have the native calling convention_stdcall. In this sample, fManaged is defined in a separate file Lib2ManagedParts.cpp:

// Lib2ManagedParts.cpp

// compile with "cl /clr /LD lib2ManagedParts.cpp " (continued in next line) // "/link /out:Lib2.dll lib2NativeParts.obj"

void _stdcall fManaged()

System::Console::WriteLine("fManaged called\n");

As you can see from the comment at the beginning of the file, Lib2NativeParts.obj and Lib2ManagedParts.obj are linked into a DLL named Lib2.dll. When a native client calls the exported function f, there is no need to start the CLR, because f is a native function. When the argument true is passed, f internally calls fManaged. To perform this method call, an unman-aged-to-managed transition has to be made. As discussed in Chapter 9, this transition is done via the interoperability vtable. Due to the patches done in _CorDllMain, the CLR can be delay-loaded before this transition occurs.

There is yet another important scenario for delay-loading the CLR. This scenario is based on virtual functions. Assume you have a native abstract base class that acts as an interface between a DLL and its client.

// Interface.h

// This file is included in the client and the server struct Interface {

The following code declares a class that implements this interface: // InterfaceImpl.h

#include "Interface.h"

class InterfaceImpl : public Interface {

public:

virtual void f(); // overrides Interface::f

The next file implements the method f as a managed function: // InterfaceImpl.cpp

// compile with "CL /c /clr InterfaceImpl.cpp" #include "InterfaceImpl.h"

void InterfaceImpl::f() {

System::Console::WriteLine("InterfaceImpl::f called");

To export such an implementation to a client, a DLL can export a native function returning an interface pointer:

// compile with "CL /LD /MD Lib3.cpp /link InterfaceImpl.obj"

#include "InterfaceImpl.h"

// Interface impl as a global variable InterfaceImpl impl;

// exported method returns address to global variable extern "C" _declspec(dllexport)

Interface* GetInterface() {

return &impl;

Last but not least, let's have a look at a native client for this library: // Lib3Client.cpp

// compile with "CL /MD Lib3Client.cpp"

#include "Interface.h"

#pragma comment (lib, "Lib3.lib")

extern "C" __declspec(dllimport) Interface* GetInterface();

In this sample, the CLR is not initialized when main calls GetInterface. Returning the interface pointer does not require the runtime to be initialized, either. Only when the interface method is called the first time is the CLR initialized. This scenario is important because calling virtual method calls across DLL boundaries is the fundamental method invocation mechanism of COM interfaces. This means that you can delay-load the CLR even if you implement COM objects.

+1 0

Post a comment