Modular Programming with DLLs

Home | Up | Search | X-Zone News | Services | Book Support | Links | Feedback | Smalltalk MT | The Scrapyard | FAQ | Technical Articles

 

Modular Programming with DLLs

Written by Robert Dunlop
Microsoft DirectX MVP


Related Articles of Interest:

Locating an Application CD
Synchronization of Game Threads

Introduction

In this article, we will take a look at how to use Dynamic Link Libraries to allow applications to be structured in a modular fashion, even when building a full screen, exclusive mode application under DirectDraw.

Considering the large code base that is encompassed in an entertainment title, it becomes necessary to break the application into functional modules.  Often, a game is divided into multiple modules, corresponding to each level of the game, for example, and also containing peripheral modules such as a main menu and configuration screens.  When building a windows application, we could normally launch separate process to deal with these various modules.

However, in full screen DirectDraw applications, which must set the cooperative level to "exclusive", the screen surfaces are a resource that can utilized by the process that created them.  Thus, we have a problem - to use separate processes, we would need to initialize DirectDraw when entering a new module, including a return to the desktop and toggling of the screen modes.  Such an implementation would look very unpolished - so we must find a way around this.

The answer is to break our program into code modules, and store them in extension DLLs rather than building separate executables. 

Structuring with  Dynamic Link Libraries

The tricky part about this is that while the functions in the DLL operate under the same context, they do not have the same access that we would have if they were written into the code base for our executable.  For example, they do not have access to global variables that are defined in our program, and cannot directly call functions other functions that are in our executable.

Therefore, we must provide the means not only for our executable to communicate with the DLL, but also provide for its needs and handle any tasks that the DLL needs to implement on the global level.  This requires a bit more forethought on our part, and we will fare best if we spend more time analyzing our program before we start to code it.

This problem is compounded by the fact that we call our functions in the DLL "blindly".  When we call functions compiled within a program, we can  rely upon the compiler to notify us if we do not pass the correct calling parameters.  However, when we work with functions exported from a DLL, we have no such safety net.  Prototypes for the functions are set in our source code for the executable, and must match those defined in our DLL, or the parameter's will not properly be interpreted.

Defining Our DLL

When we create our DLL, we must define specific functions that we wish to make available to our shell program.  These functions are known as "exports".

There are two things that we must do to create an exported function:

bulletThe function definition must be defined in the format of
    extern "C" <return type> WINAPI function_name(...)
bulletThe function must be listed in the DEF file as an export when compiling the DLL.

The export section of the DEF file includes the name of each function to be exported, along with an ordinal number that provides a unique identifier for the function within the DLL.  The ordinal number is nothing special, and you can just sequentially number the functions.

When you create a DLL under Visual C++, there may already be existing exports for registration functions.  Simply add your function definitions below them, starting at the next ordinal number.  A sample DEF file is shown below :

LIBRARY    "test.dll"

EXPORTS

init        @1
cleanup     @2
mouse_down  @3
mouse_up    @4
mouse_move  @5
render      @6

Breaking our Application into Tasks

To use DLL based modules, we must first break our application down into bite sized tasks, so that we can handle all functionality through a fixed set of functions.  Once we have a series of modules, any change in function specifications will mean that we have to change and re-compile every module, so requirements should be considered thoroughly at the start.

How this is structured is entirely up to the developer.  However, for the purpose of this lesson, I will base my explanations based upon some assumptions from my own framework.  In any module, I define, at a minimum, three functions :

extern "C" WORD WINAPI init(HWND hWnd);

Called to perform module initialization.  It is passed the top level window handle of the application, which can be stored for later use as needed.

extern "C" WORD WINAPI cleanup();

Called before unloading the module to perform cleanup.

extern "C" WORD WINAPI render(float delt);

Called when it is time to render a frame.  The time that has elapsed since the previous frame is passed to this function.

I also define functions that are called before the Init function, to provide the function with handles to any interfaces that are needed by the module.  For example :

extern "C" WORD WINAPI set_ddraw(LPDIRECTDRAW4 lpdd,
                                 LPDIRECTDRAWSURFACE4 lpDDSBack,
                                 LPDIRECTDRAWSURFACE4 lpDDSPrimary);

These interfaces are created by the shell, and are passed to each module when it is loaded.  The interface pointers are then stored by the module, for later use.

Commanding the Shell

In addition to the shell being able to call functions in our library, we need to allow the library to communicate its needs back to the shell as well.  I handle this by utilizing the return value from any library function as a command value.  If the value returned from a function is non-zero, a handler is passed the return value and determines what action to execute, such as loading another module, or shutting down.

To insure that the shell and the DLLs speak a common language, create a header file that is shared by both code bases, and provides constants for defined actions:

#define    MOD_NULL        0
#define    MOD_EXIT        1
#define    MOD_RUN_MENU    2
#define    MOD_RUN_GAME    3

etc, etc....

Defining Functions in the Shell

Once we have decided on our functions, we must create links to them in our shell program.   To do this, we will create a definition of the function prototype and generate a function pointer that will point into the DLL:

typedef WORD (WINAPI* LPFUNC_INIT)(HWND);
typedef WORD (WINAPI* LPFUNC_CLEANUP)();

LPFUNC_INIT         mod_init=NULL;
LPFUNC_CLEANUP      mod_cleanup=NULL;

Once we have defined the function pointers, we are ready to load our library and put things in place.  The function below provides an example of a module load function.  It is based upon using a single DLL per module, so it will unload any module previously loaded with this function prior to loading a new module.

BOOL load_module(HWND hWnd,LPSTR lib_path) {

static HINSTANCE dll_handle=NULL;

// is there a module currently loaded?

if (dll_handle) {

// yes, call cleanup function if one is available

if (mod_cleanup)

mod_cleanup();

// unload the library

FreeLibrary(dll_handle);
dll_handle=NULL;

}

// load the new module, return on failure

if (!LoadLibrary(lib_path)) return FALSE;

// retrieve pointers to DLL functions

mod_init=(LPFUNC_INIT) GetProcAddress(dll_handle,"init");
mod_cleanup=(LPFUNC_CLEANUP) GetProcAddress(dll_handle,"cleanup");
mod_setddraw=(LPFUNC_SETDDRAW) GetProcAddres(dll_handle,"set_ddraw");

// pass global interfaces if needed

if (mod_setddraw) mod_setddraw(lpdd,lpBack,lpPrimary);

// call initialization routine if one defined

if (mod_init) mod_init(hWnd);

}

Note that GetProcAddress() returns NULL if the function is not found in the DLL.  This is convenient, as it allows us to provide support for optional functions.  Simply test each function pointer before calling, to determine if one is present.
 

Related Articles of Interest:

Locating an Application CD
Synchronization of Game Threads

This site, created by DirectX MVP Robert Dunlop and aided by the work of other volunteers, provides a free on-line resource for DirectX programmers.

Special thanks to WWW.MVPS.ORG, for providing a permanent home for this site.

Visitors Since 1/1/2000: Hit Counter
Last updated: 07/26/05.