COM Tutorial Samples |
Tutorial Home |
Previous Lesson |
Lesson List |
Next Lesson |
The DLLSKEL sample introduces the basic DLL skeleton that can be used as a point of departure for more complex Win32 DLLs (dynamic link libraries). It is used as a base for other COM Tutorial code samples. In this series of COM Tutorial code samples, DLLSKEL works with the DLLUSER code sample to illustrate how DLLSKEL's function services are called by an EXE consumer.
For functional descriptions and a tutorial code tour of DLLSKEL, see the Code Tour section in DLLSKEL.HTM. For details on setting up the programmatic usage of DLLSKEL, see the Usage section in DLLSKEL.HTM. To read DLLSKEL.HTM, run TUTORIAL.EXE in the main tutorial directory and click the DLLSKEL lesson in the table of lessons. You can also achieve the same thing by clicking the DLLSKEL.HTM file after locating the main tutorial directory in the Windows Explorer. See also DLLUSER.HTM in the main tutorial directory for more details on the DLLUSER application and how it works with DLLSKEL.DLL. You must build DLLSKEL.DLL before building DLLUSER. After producing DLLSKEL.DLL and DLLUSER.EXE, the makefile for DLLSKEL copies the necessary DLLSKEL.H, DLLSKEL.LIB, and DLLSKEL.DLL files to the appropriate sibling directories.
In general, to set up your system to build and test the code samples in this COM Tutorial series, see TUTORIAL.HTM for details. The supplied makefile (MAKEFILE) is Microsoft NMAKE-compatible. To create a debug build, issue the NMAKE command at the command prompt.
DLLSKEL is a DLL that you can access from applications by either performing an explicit LoadLibrary call or implicitly loading the DLL by linking to its associated .LIB file. In either case, you need to include DLLSKEL.H to declare the functions that are defined as exported in the DLLSKEL DLL. In the case of this Tutorial lesson, a representative DLLUSER.EXE application is provided to illustrate the programmatic use of DLLSKEL.DLL. DLLUSER is built in the DLLUSER lesson (in sibling directory DLLUSER). See below for more details.
The client sample and other related samples must be compiled before you can run the client. For more details on building the samples, see Building the Code Samples.
If you have already built the appropriate samples, DLLUSER.EXE is the client executable to run for this sample. Click here to run DLLUSER.EXE.
Depending on the security level of your browser you may see a dialog allowing you to either open the .EXE file or save it to disk. Click the "Open it" choice and then click the OK button.
Files Description DLLSKEL.TXT Short sample description. MAKEFILE The generic makefile for building the DLLSKEL.DLL code sample of this tutorial lesson. DLLSKEL.H The include file for declaring as imported or defining as exported the service functions in DLLSKEL.DLL. Meant for eventual use by outside users of the DLL. DLLSKEL.CPP The main implementation file for DLLSKEL.DLL. Has DllMain and the two exported service functions. DLLSKELI.H The include file for the internal class declarations and the identifier definitions for resources stored inside the DLLSKEL.DLL. DLLSKEL.RC The DLL resource definition file. DLLSKEL.ICO The icon resource.
In the context of this tutorial, the goal of DLLSKEL is to illustrate a C++ DLL skeleton that can serve as a point of departure for making more sophisticated DLLs. The resulting DLL is meant to work in conjuntion with DLLUSER.EXE to illustrate how to link to and call services in Win32 DLLs. It is the basic DLL framework for subsequent code samples in this tutorial. Study the code comments to learn more about this Win32 C++ DLL skeleton.
DLLSKEL makes use of many of the utility classes and services provided by APPUTIL. For more details on APPUTIL, study the source code located in the sibling APPUTIL directory APPUTIL.HTM in the main tutorial directory.
In DLLSKEL.CPP, two exported functions are defined to export representative calls to outside consumers: DllHelloBox and DllAboutBox.
After incrementing a shared global counter variable, DllHelloBox uses APPUTIL's CMsgBox facility to show a simple information message box. This box says hello and shows the DLL's user instance count and a total count of the number of times this Hello function has been called by any user. The following DllHelloBox code from DLLSKEL.CPP shows one method to protect the incrementation of a global counting variable in a multitasking environment. The use of the Win32 InterlockedIncrement function enforces mutual exclusion for the incrementing operation when multiple processes attempt to increment a shared variable.
STDENTRY_(BOOL) DllHelloBox( HWND hWnd) { int iHelloCount; CDllShared* pShared = (CDllShared*) g_pvShared; // Increment the cummulative global count of all Hellos. InterlockedIncrement((LONG*) &pShared->iHelloCount); iHelloCount = pShared->iHelloCount; // Now show the user a -Notice- message box and load the display strings // out of this DLL's resources. Use the format string to show the // user instance count of this loaded DLL and the shared hello count. g_pDll->pMsgBox->Init(g_pDll->hDllInst, hWnd); g_pDll->pMsgBox->NoteFmtID( IDS_HELLOCOUNTFMT, g_pDll->iUserCount, iHelloCount); return TRUE; }
The global hello count variable is accessed in a shared memory location that is part of a section of file-mapped memory. A g_pvShared global variable is used to store a pointer to the file-mappped shared memory. This g_pvShared variable is a static global variable within the DLL. It is newly created and assigned for each user process that attaches to the DLL. In the above fragment, the address of the iHelloCount counter within a shared data object (ie, a within CDllShared object) is passed to the InterlockedIncrement function. This use of a CDllShared object and the appropriate (CDllShared*) casting shows how to apply the object as a template over the file-mapped shared memory image pointed to by g_pvShared. See the discussion of DllMain below for more details on how the shared memory object is set up as a file mapping.
The DllAboutBox exported function creates an object of the CAboutBox class, as implemented in the APPUTIL utility library, to load a dialog box from the DLL's resources and then display an About box.
DLLSKEL.CPP defines the DllMain entry function for the entire DLL. To construct a Win32 DLL, you must provide the DllMain entry function and handle the following messages sent to the DLL via that function: DLL_PROCESS_ATTACH, DLL_PROCESS_DETACH, DLL_THREAD_ATTACH, DLL_THREAD_DETACH.
The following code fragment from DLLSKEL.CPP shows how the DllMain function is defined and how the messages are handled. In this sample, no specific actions are taken during thread attach and detach.
BOOL WINAPI DllMain( HINSTANCE hDllInst, DWORD fdwReason, LPVOID lpvReserved) { BOOL bResult = TRUE; // Dispatch this call based on the reason it was called. switch (fdwReason) { case DLL_PROCESS_ATTACH: // The DLL is being loaded for the first time by a given process. // Perform per-process initialization here. If the initialization // is successful, return TRUE; if unsuccessful, return FALSE. bResult = FALSE; if (UnicodeOk()) { // Instantiate a DLL global data encapsulator class. g_pDll = new CDllData; if (NULL != g_pDll) { // Remember the DLL Instance handle. g_pDll->hDllInst = hDllInst; // Create a MsgBox object. g_pDll->pMsgBox = new CMsgBox; if (NULL != g_pDll->pMsgBox) { BOOL fFirst; int iSharedSize = sizeof(CDllShared); // Create a named file mapping object. g_hMapObj = CreateFileMapping( (HANDLE) 0xFFFFFFFF, // Use paging file NULL, // No security attributes PAGE_READWRITE, // Read/Write access 0, // Mem Size: high 32 bits iSharedSize, // Mem Size: low 32 bits DLLSKELSHARED_STR); // Name of map object if (NULL != g_hMapObj) { // Determine if this is the first create of the file mapping. fFirst = (ERROR_ALREADY_EXISTS != GetLastError()); // Now get a pointer to the file-mapped shared memory. g_pvShared = MapViewOfFile( g_hMapObj, // File Map obj to view FILE_MAP_WRITE, // Read/Write access 0, // high: map from beginning 0, // low: 0); // default: map entire file if (NULL != g_pvShared) { CDllShared* pShared = (CDllShared*) g_pvShared; if (fFirst) { // If this is the first attaching process, init the // shared memory. memset(g_pvShared, 0, iSharedSize); pShared->iUserCount = 1; } else { // Increment the cummulative global count of all // attached processes (ie, the count of DLL users). InterlockedIncrement((LONG*) &pShared->iUserCount); } // Save a local instance copy of this user instance // count. Each user process has its own g_pDll instance // data and can thus remember it's user instance count. g_pDll->iUserCount = pShared->iUserCount; bResult = TRUE; } } } } } break; case DLL_PROCESS_DETACH: // The DLL is being unloaded by a given process. Do any // per-process clean up here, such as undoing what was done in // DLL_PROCESS_ATTACH. The return value is ignored. // Unmap any shared memory from the process's address space. UnmapViewOfFile(g_pvShared); // Close the process's handle to the file-mapping object. CloseHandle(g_hMapObj); if (NULL != g_pDll) { // Delete the message box and global DLL instance data. DELETE_POINTER(g_pDll->pMsgBox); DELETE_POINTER(g_pDll); } break; case DLL_THREAD_ATTACH: // A thread is being created in a process that has already loaded // this DLL. Perform any per-thread initialization here. The // return value is ignored. break; case DLL_THREAD_DETACH: // A thread is exiting cleanly in a process that has already // loaded this DLL. Perform any per-thread clean up here. The // return value is ignored. break; default: break; } return (bResult); }
When the DLL is attached to a process, DllMain is called with DLL_PROCESS_ATTACH. During DLL_PROCESS_ATTACH above the UnicodeOk function is called to determine if the DLL will run on the platform when compiled for Unicode strings. If the platform doesn't support Unicode and yet the DLL is compiled for Unicode, the DllMain function fails, and the DLL is unloaded.
The global instance data object is created. This object is pointed to by global variable, g_pDLL. This CDllData object encapsulates instance data that is instantiated separately for each process that attaches. In this sample, this object contains the DLL's instance handle, a place to save the count of the user instance, and a pointer to the Message Box object. The Message Box object is created during DLL_PROCESS_ATTACH. After its creation, the pointer to it is assigned.
The remainder of the work during DLL_PROCESS_ATTACH sets up the file-mapped memory section for a shared CDllShared object. First CreateFileMapping is called to assign a global handle to the mapping object, g_hMapObj. The current operating system virtual memory paging file is used for the file. No security attributes are needed. Read/Write access is specified. A size equal to the size of the shared object is specified. The mapping object is given a name so that it can be explicitly shared by instances of this DLL. The DLLSKELSHARED_STR macro is used to specify the name string "DLLSkelShared" as ANSI or Unicode depending on whether DLLSKEL is being compiled for Unicode or not. This macro is defined immediately before DllMain as follows.
#if defined(UNICODE) #define DLLSKELSHARED_STR L"DLLSkelShared" #else #define DLLSKELSHARED_STR "DLLSkelShared" #endif
The mapping object is then used to map a memory view of the file. This action obtains a pointer to the shared memory and assigns it to global variable g_pvShared. If this is the first creation (rather than a re-open) of the mapping object, then the shared memory area is initialized to zero and the user instance counter is intitialized to 1. If this is not the initial creation of the shared object, then the user instance counter is incremented to keep a cumulative count of the attaching processes. Since this is globally shared memory, the Win32 InterlockedIncrement function is used to ensure mutual exclusion during the increment operation. Once this user counter is incremented the new value is saved in a non-shared variable, g_pDll->iUserCount, that is part of the instance data that is specific to the attaching process.
When the DLL is detached from the process DllMain is called with DLL_PROCESS_DETACH. During DLL_PROCESS_DETACH the mapped memory view is unmapped and the mapping object is closed. The CMsgBox and CDllData objects are then deleted. To perform the delete, the DELETE_POINTER macro is used to delete the memory object pointed to by the pointer. Here is the macro definition from APPUTIL.H:
#define DELETE_POINTER(p)\ {\ if (NULL != p)\ {\ delete p;\ p = NULL;\ }\ }
If the original pointer value was not NULL, the macro deletes the memory object that was pointed to. By then immediately setting the pointer to NULL, the macro prevents other threads from using it.
To build the DLL, you provide specific DLL-related instructions when you compile the modules of the DLL and when you link the modules with the Link command. The following fragment from the DLLSKEL makefile (file MAKEFILE) shows the compilation rule for the DLLSKEL.CPP module.
# Compilation/Dependency rules for the .DLL source files. $(TDIR)\$(DLL).obj: $(DLL).cpp $(DLL).h $(DLL)r.h $(cc) $(cvarsdll) $(cflags) $(cdebug) -Fo$@ $(DLL).cpp
Note the use of the 'cvarsdll' macro when compiling module components of a DLL. This is in contrast to the 'cvars' macro normally used in linking EXE applications. Both of these macros are defined in the makefile include file WIN32.MAK. For more details, see WIN32.MAK in the \MSSDK\INCLUDE directory of the Win32 Platform SDK. All of the COM Tutorial code sample makefiles include WIN32.MAK.
If there are to be resources in the DLL (as there are in the DLLSKEL DLL), the appropriate .RC file is RC-compiled in the normal manner as follows.
# Compile the DLL resources. $(TDIR)\$(DLL).res: $(DLL).rc $(DLL).ico $(DLL)r.h rc $(RCFLAGS) -r -fo$@ $(DLL).rc
To link the modules and the resources together to build the DLL binary, you provide a Link command like the following from the DLLSKEL makefile:
# Link the object and resource binaries into the target DLL binary. # Build the import LIB file so apps can link to and use this DLL. $(DLL).dll: $(DLLOBJS) $(TDIR)\$(DLL).res $(LINK) @<< $(LINKFLAGS) $(dlllflags) -base:0x1C000000 -out:$@ -map:$(TDIR)\$*.map -implib:$*.lib $(DLLOBJS) $(TDIR)\$*.res $(olelibsdll) $(APPLIBS) <<
The 'implib:$*.lib' directive to the Linker instructs it to produce the DLLSKEL.LIB import library file. This .LIB file is used in turn when the DLL is linked with any application that calls the DLL.
Note also that this DLL makes calls to another library, APPUTIL, by statically linking to it. The APPLIBS macro reduces to APPUTIL.LIB. In this case, the compiled content of APPUTIL is contained entirely in APPUTIL.LIB.
APPUTIL itself could have been a DLL. It is a judgement call as to whether you build one library type or another. Usually, if you are exporting C++ class definitions across the library boundary, the exported names are decorated in a manner proprietary to the compiler vendor. This approach restricts the use of such a DLL to applications that are also compiled with the same compiler. If the exported classes are small, it is often more convenient to put them into a static .LIB (as is the case with APPUTIL) than to put them into a .DLL.
Because the DLLSKEL DLL has resources and because it is desirable to provide outside users of the DLL with an .H file of the same name, a separate DLLSKELI.H is used for internal class declarations and resource identifier definitions, while the DLLSKEL.H file is intended solely for the DLL's function import declarations and export definitions.
For the consumer application (in this case DLLUSER.EXE) to link to and properly call the exported functions in the DLL, it must import them. The following fragment from DLLSKEL.H shows how to use the same convenient 'STDENTRY' macros to serve both the consumer application's import declaration and the provider DLL's export definition needs.
#if !defined(_DLLEXPORT_) // If _DLLEXPORT_ is not defined then the default is to import. #if defined(__cplusplus) #define DLLENTRY extern "C" __declspec(dllimport) #else #define DLLENTRY extern __declspec(dllimport) #endif #define STDENTRY DLLENTRY HRESULT WINAPI #define STDENTRY_(type) DLLENTRY type WINAPI // Here is the list of service APIs offered by the DLL (using the // appropriate entry API declaration macros just #defined above). STDENTRY_(BOOL) DllHelloBox (HWND); STDENTRY_(BOOL) DllAboutBox (HWND); #else // _DLLEXPORT_ // Else if _DLLEXPORT_ is defined then we've been told to export. #if defined(__cplusplus) #define DLLENTRY extern "C" __declspec(dllexport) #else #define DLLENTRY __declspec(dllexport) #endif #define STDENTRY DLLENTRY HRESULT WINAPI #define STDENTRY_(type) DLLENTRY type WINAPI #endif // _DLLEXPORT_
The DLLENTRY macro allows you more freedom to specify return types and calling conventions. The STDENTRY assumes the COM/OLE standard HRESULT return type and the WINAPI calling conventions (usually defined as __stdcall). The STDENTRY_ macro makes this same assumption but does allow you to specify a return type.
The '__declspec(dllimport)' and '__declspec(dllexport)' directives are one way to declare DLL imports and to define DLL exports in Win32 programming. Another way is to Link the executable using a module definition (.DEF) file that contains import and export statements. This technique was commonly used in Win16 programming but is often not needed in Win32 programming. Note that the __declspec specifications are Microsoft-specific techniques in C++ programs. DLL exports can also be designated using the export switch on the Linker command line.
Note the use of extern "C" when compiling under C++ (__cplusplus). This tells the compiler, during compilation of both the DLL and the calling code, that the default C++ function name decoration will not be used. This is usually preferred in generic DLLs, whose function calls might be called by applications that could be compiled with a variety of C++ compiler tools, including straight C compilers.
The default behavior of DLLSKEL.H is to serve calling applications that import the functions in the DLL. Such applications simply include DLLSKEL.H during compilation of such a calling module. When the DLL itself is compiled, however, this file is included as in the following fragment from DLLSKEL.CPP.
#define _DLLEXPORT_ #include "dllskel.h"
When _DLLEXPORT_ is thus defined, the behavior of DLLSKEL.H is to serve the DLL itself in defining its exported functions. The following fragment from DLLSKEL.CPP shows such a function definiton.
STDENTRY_(BOOL) DllAboutBox(HWND hWnd); { ... }
When the DllAboutBox function is defined in DLLSKEL.CPP, the STDENTRY_ macro expands to yield the following:
extern "C" __declspec(dllexport) BOOL WINAPI DllAboutBox(HWND hWnd) { ... }
Likewise, when the DllAboutBox function is imported in DLLSKEL.H, the declaration is expanded to the following import function prototype.
extern "C" __declspec(dllimport) BOOL WINAPI DllAboutBox(HWND hWnd);
The WINAPI macro expands to various things, depending on the build environment, but generally stipulates the calling convention (usually __stdcall). See WIN32.MAK and the standard Windows include file WINDEF.H for more details on the WINAPI macro.