|
IntroductionIn 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 LibrariesThe 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 DLLWhen 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:
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
Breaking our Application into TasksTo 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);
extern "C" WORD WINAPI cleanup();
extern "C" WORD WINAPI render(float delt);
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, 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 ShellIn 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 etc, etc.... Defining Functions in the ShellOnce 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); LPFUNC_INIT
mod_init=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) {
} 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.
|
Visitors Since 1/1/2000:
|