Recently, I had an old colleague ask me how to regenerate the source code to a DLL. His customer had been using this DLL to extend the capabilities of an application and needed to change the DLL's behavior. His customer, however, was no longer in possession of the source code to this DLL and wished to somehow retrieve the source, given only the DLL and the header file which allows an application to enumerate the functions in the DLL. I know of no way to recreate the source, but there is an alternative that may make the task of changing the existing DLL easier.
The technique we will examine in this paper is to use a "forwarding" DLL to change the behavior of, or add new functions to, an existing DLL by implementing functions with the same signature as those contained in the original DLL. Functions that need not change simply call their counterparts in the old DLL. Functions that need to change either call into the old DLL and change the result or implement the desired behavior without getting involved with the old DLL. To call into the old DLL, our forwarder needs to dynamically load the original DLL, bind to its functions, call the original functions, and return the result to a client.
To describe the technique of using a forwarding DLL, we first provide some insight into the mechanisms that make it possible to dynamically call functions not defined in the application. We look at several topics including:
Along the way, we will discuss some of the tools and utilities that are useful when implementing a DLL and the applications which will use them.
I used Microsoft Visual C++ 6.0 to build the sources that this paper contains. I didn't use the integrated development environment to automate the task of creating a build mechanism, though. Instead, I hand crafted a make file, where it was appropriate to use an automated tool. I chose this approach, because I think it gives a better understanding of what is really taking place when you build DLL's and applications that call them. For small examples, where an automated build is not necessary, I have included instructions to build the associated modules in comments in the example's source.
I have tested the resulting applications and DLL's on Windows NT™ 4.0 and Windows 98™.
Because my colleague was specifically interested in the use of DLL's to extend the behavior of a particular product, Hewlett-Packard's Visual Engineering Environment (HP VEE), I have included information specific to its environment. All of the examples in this paper can be used with or without this application.
Without further ado, let's get started...
A function pointer is a variable that holds the address of a function. Executing the function pointer causes the program to jump to the function at which the pointer points. One of the benefits of using function pointers is that an application can be dynamically extended or changed, if that application can be convinced to call a function in a DLL you provide.
The most usual way to use a function pointer is to define a new data type that represents the address of a function. Variables declared to be of this new type are function pointers. A function pointer makes it possible to call into an arbitrary function by simply assigning the address of the function of interest to the pointer. As an example, let's say we have a statement that looks like:
typedef void (*p_vv_fn)(void);
The "typedef" keyword introduces a new data type. The "(*p_vv_fn)" tells the C compiler that we are declaring a function pointer type called "p_vv_fn". The construct "(*)", which groups the symbol denoting a pointer type -- the asterisk -- between parentheses, is what the compiler identifies as a function pointer. The "p_vv_fn" is the name by which you may declare variables of this function pointer type. Just like you could declare a variable to be of type pointer-to-int by using the expressions:
typedef int *PINT; /* PINT is the name of a new data type: a pointer to int. */ PINT foo; /* foo is a variable of type pointer to int */ int bar = 85; foo = &bar; /* foo now points at bar. dereferencing foo will yield the contents of bar */ printf("Contents of bar %d\n", *foo);
The "*" in "*PINT" tells the compiler that you are introducing a new pointer type, and the "PINT" tells the compiler how you will identify variable of this new type.
Using the newly-defined function pointer data type allows you to declare and use a function pointer like this:
void func1(void){ printf("In func1\n"); return; } p_vv_fn func_ptr = func1; /* p_vv_fn is the name of our new data type. func_ptr is a variable of this type and points at a function named "func1" */ func_ptr(); /* execute func1 */
These two snippets are available as complete programs in fp.c and foo.c. It makes the preceding discussion much more concrete to compile the examples into debug executables and step through the resultant applications in a debugger. Let's take a look at an example run. Figure 1 is a screen shot of the debugger variables window taken while stepping through the application built from the source file "fp.c". Note that the value of "func_ptr" is 0x00401005. Figure 2 is a screen shot showing the split source and assembly window of the program, highlighting the contents of the address 0x00401005. This address contains an instruction to jump to program location 0x00401030. Figure 3 highlights the contents of the jump target. It is the location of the first instruction in the function "func1". So, our function pointer, "func_ptr", holds the address of an instruction that jumps directly to the function named "func1". If we were to set a break point on the jump instruction, we would see that we do indeed break there when we call through "func_ptr". Stepping from that point puts us in "func1".
The remainder of the function pointer type declaration involves the function return type and arguments types. These, taken together, comprise a function's signature.
A function's "signature" is the phrase used to to describe the layout of return type and argument types any given function or functions may have. In our example, we have two functions, each with a unique signature. When composing a pointer to a function, it is necessary to ensure that the function pointer and the target function signatures match. Note that it is common for any number of functions to share a common signature, allowing us to use one data type definition to point at any number of functions with the same layout. For example, we declared two new function pointer data types using the instructions:
typedef long (*p_lv_fn)(void); /* a pointer to a function which returns a long and takes a void argument */ typedef long (*p_ll_fn)(long); /* a pointer to a function which returns a long and takes a long argument*/
Any time we declare a variable to be of type p_ll_fn1, we are saying that the variable is pointing to a function whose signature includes a return type of long and an argument type list that includes a long. What do you suppose would happen if we declared and used a function pointer like this?
typedef long (*p_lv_fn)(void); /* a pointer to a function returning a long and taking a void argument */ /* a function returning a long and taking a long as an argument */ long foo(long a){ return a + 1; } int main(int argc, char *argv[]){ /* Ruh Roh */ p_lv_fn p_func = (p_lv_fn)foo; long result = 0; /* call a function that expects a long argument with no argument at all */ result = p_func(); return 0; }
Well, the call to the function "foo" through the pointer "p_func" wouldn't pass any arguments to foo. foo would end up adding 1 to who knows what. Incidentally, this is legal code. it compiles, links and runs, with no whining from either the compiler or the linker. The results are a little unpredictable, though.
What a function's signature provides is the ability to properly construct a "stack" for holding arguments and a return value. A stack is a memory area used to communicate between a function and the context that is calling it. Microprocessors provide a stack pointer that gets set to a correct location, given the signature of the function a context is trying to call. Arguments and space to hold return values get pushed onto the stack, which has the effect of changing the value of the stack pointer. After the called function has returned, somebody is supposed to have popped the arguments and return values off the stack, adjusting the stack pointer as appropriate. When you compile a function and the calls to that function, the machine instructions resulting from compilation will include specifics about how to manipulate stack pointer, access variables passed as arguments, and what to do with the return value. Calling a function with an improperly formed stack yields undefined, though rarely beneficial, results. A bug that is the result of a stack misalignment can be difficult to detect and can announce its presence in an area of your program completely unrelated to the function you are calling through a pointer. HP VEE compiled functions require a "definition file", which specifies function signatures, so that the system can properly generate a stack for the function you wish to call.
We said that After the called function has returned, somebody is supposed to have popped the arguments and return values off the stack, adjusting the stack pointer as appropriate. The party responsible for this is the topic of discussion for our next section.
When any context calls a function, the calling context and the called function must agree on two activities:
This agreement is known as a calling convention. In Win32™ programs, there are three common calling conventions (there are more calling conventions, but the ones we list are the most commonly used). The following table details their use.
Calling Convention | Parameter Passing Direction | Stack Cleanup | Comments |
---|---|---|---|
__cdecl | Pushes parameters on the stack right to left. | Caller cleans up the stack. | The default for C and C++ programs. Because the caller cleans up the stack, this convention allows for the use of varargs. |
__stdcall | Pushes parameters on the stack right to left. | Called function cleans up the stack. | Win32™ API functions are called using this convention. The generally-accepted expectation is that functions implemented in DLL's will use this convention. |
thiscall | Parameters are pushed on the stack right to left. | Called function cleans up the stack. | The calling convention used by C++ member functions that do not use varargs. Varargs member functions are declared __cdecl and push the "this pointer" on the stack last. |
If you no longer have the source to a DLL, it may still be possible to know what calling convention a function uses by either looking at the header file or by inspecting the DLL itself. The Name Decorations section of this paper discusses inspecting a DLL to extract calling convention information about a function. Applications that can dynamically attach and call functions in a DLL may need you to specify the interface to the DLL through the use of a header file. HP VEE is one such application. What the HP VEE Import Library object calls a "Definition File" is basically a C-language header file. You can specify the calling convention a DLL function uses by including it in the HP VEE Definition File function prototype. A line like:
long __cdecl initialize(void);
Tells HP VEE that the DLL function "initialize" has been compiled to use C calling conventions
If you don't have the DLL source and the environment you use to call your DLL functions doesn't require the use of a header file, you may still be able to discern the calling convention a function was built with by using a utility called 'dumpbin' that comes with Visual C++®. Let's say that we compiled a DLL (named new.dll) with a function declared the way we declared the function "initialize" above. If we ran the command "dumpbin -exports" on new.dll, we would receive the following display:
[E:/Projects/Vee/forward] dumpbin -exports new.dll Microsoft (R) COFF Binary File Dumper Version 6.00.8168 Copyright (C) Microsoft Corp 1992-1998. All rights reserved. Dump of file new.dll File Type: DLL Section contains the following exports for new.dll 0 characteristics 37819CC4 time date stamp Mon Jul 05 23:05:56 1999 0.00 version 1 ordinal base 1 number of functions 1 number of names ordinal hint RVA name 1 0 00001019 initialize Summary 2000 .data 1000 .debug 1000 .idata 1000 .rdata 1000 .reloc 5000 .text
DLL's may have any number of symbols in them, where "symbols" mean the data and function identifiers in the DLL. Only exported symbols are accessible from outside the DLL which contains the symbol. The above display from dumpbin shows that there is one exported function called "initialize". Note that the function name has no "decorations": characters prepended and/or appended to the symbol name.
long __stdcall initialize(void);
If we were to change the function declaration to that shown above, dumbin would display:
[E:/Projects/Vee/forward] dumpbin -exports new.dll Microsoft (R) COFF Binary File Dumper Version 6.00.8168 Copyright (C) Microsoft Corp 1992-1998. All rights reserved. Dump of file new.dll File Type: DLL Section contains the following exports for new.dll 0 characteristics 378824D4 time date stamp Sat Jul 10 22:00:04 1999 0.00 version 1 ordinal base 1 number of functions 1 number of names ordinal hint RVA name 1 0 00001014 _initialize@0 Summary 2000 .data 1000 .debug 1000 .idata 1000 .rdata 1000 .reloc 5000 .text
Now, the exported function's symbol has a leading "_" and a trailing "@0". The number 0 indicates the number of bytes required to hold the function arguments. If your DLL contains functions with decorations like those seen in the above display, they have been compiled using the __stdcall calling convention.
There is one situation where the exported symbol may not give you enough detail to determine the function's calling convention. If the DLL was linked using a linker definition file (usually a file with the base name of the DLL with a ".def" extension), the linker might have been directed to export an undecorated symbol even though the function had been declared __stdcall. Let's say that we have a function declared as "initialize" was last shown, but we linked the DLL with the command:
link -debug:mapped,full -debugtype:cv -nologo -DLL -subsystem:windows -out: new.dll -def:new.def new.obj libc.lib kernel32.lib user32.lib "D:\Program Files\Hewlett-Packard\VEE 5.0\lib\veesrvcs.lib"
And the contents of our definition file contained:
LIBRARY EXPORTS DllMain initialize cleanup func1 func2
The dumpbin display from the resultant DLL would look like:
[E:/Projects/Vee/forward] dumpbin -exports new.dll Microsoft (R) COFF Binary File Dumper Version 6.00.8168 Copyright (C) Microsoft Corp 1992-1998. All rights reserved. Dump of file new.dll File Type: DLL Section contains the following exports for new.dll 0 characteristics 37882726 time date stamp Sat Jul 10 22:09:58 1999 0.00 version 1 ordinal base 5 number of functions 5 number of names ordinal hint RVA name 1 0 00001005 DllMain 2 1 00001019 cleanup 3 2 0000100A func1 4 3 0000100F func2 5 4 00001014 initialize Summary 2000 .data 1000 .debug 1000 .idata 1000 .rdata 1000 .reloc 5000 .text
Ignoring all the function names other than "initialize", you can see that our __stdcall DLL function's symbol is exported without decorations. The "exports" section of the linker definition file has final say in determining how a function symbol is exported to the rest of the world. It has still been compiled as a __stdcall function, though.
In all of our examples so far, we have used function pointers to call into functions in the same source file. While academically interesting, what we're really trying to get to is the point where we can call into functions in an explicitly-linked DLL to change its behavior. The term "explicitly-linked" relates to the fact that the application which will make use of the DLL functions is not built in a session where the library is available at time we build the application. By way of contrast, an "implicitly-loaded" library is one where the library is available at the time the application is built. The linker resolves, at build time, any references the application may have to functions contained in the implicitly-linked library. You don't need to make special provisions for an application to call functions in an implicitly-linked library. But because a linker can't know in advance where the functions will be located in a library that is not linked at application creation, the application must arrange to dynamically load a library into its address space, then locate the relative addresses of the functions it is interested in. The example application contained in load.c dynamically loads a DLL (which we will discuss in a later section of this paper), then calls one of its functions.
We stated earlier that only a DLL's "exported" functions are visible to other modules. In our exploration of the dumpbin tool and its application in discerning calling conventions, we glossed over what the resultant display was really telling us. When you specify the "-exports" argument to dumpbin, you are telling it to show you a module's export table. The export table enumerates the functions and data made publicly available for another module to see and make use of. A module makes use of a DLL's export table by looking at the address information pertaining to the symbol (data or function) it is interested in. If we look at the display resulting from running dumpbin on "new.dll", we can see that the symbol "func1", which is a function we want to export, is at address 0x100A, relative to the start of the library. When an application dynamically loads new.dll, it knows, by looking at the export table, that "func1" is 0x100A bytes beyond the DLL's starting address.
Exporting a function from a DLL can happen in a couple of ways. One way is to specifically declare a symbol as being exported in the DLL source code. The other way (and the way I tend to prefer, because it minimizes the amount of source code not specifically related to functionality) is to use an "exports" statement in a linker definition file. When you link the object code resulting from the compilation of the DLL source using a directive that includes a linker definition file, the linker will export the symbols named in the exports entry.
An application uses the Win32™ API function "LoadLibrary" to dynamically load a DLL. Once loaded, the operating system keeps track of a library by assigning it a "handle", which is an identifier unique to a given library within a particular application. The handle is in actuality the process-relative address which defines the starting point for the library. Let's first take a look at the declaration for a handle to a dynamically-linked library. The data type "HANDLE" is defined in the compiler-supplied header files. Setting the value of a variable of this type to 0 indicates that the handle does not refer to a valid, currently-loaded library. If the call to LoadLibrary succeeds, it returns a non-zero value, indicating that the DLL we named is in fact now loaded into the application address space.
Once we have loaded our DLL, we may now try to find the address of an interesting function. We bind that address of a function in a DLL to a function pointer in our application by using the Win32™ API function GetProcAddress. A non-zero return value indicates that a function of the name we requested is in the DLL identified by our handle variable and has been exported for use in contexts outside the DLL. Now that we have the address of an interesting function, we may call into it through our function pointer.
Like we did before, let's step through the lifetime of our application in the debugger. We will first attach the DLL to the address space of our debugger to enable us to set breakpoints in the DLL source (assuming that we have the source :-) ). Figure 4 is a screen shot of the Visual C++ project settings dialog that allows us to pre-attach a DLL to our address space. Figure 5 is a screen shot of the debugger configuration dialog that lets you specify which application you wish the debugger to execute when you start a debugging session. Incidentally, you can do this whether the application is built to be debuggable or not. It isn't even necessary to have the calling application source. This latter point is what makes it possible to debug custom DLL's written for use with HP VEE. We will show an example of this in a later section of this paper.
Figure 6 is a screen shot of the "load" application after it has attached the DLL and located its function. The value of the variable "dll_handle" is the application address at which our dynamically-attached DLL is located. Notice that the debugger tells us that the function address is 0x100A bytes beyond to the base address where the library itself was loaded.
When we no longer need a reference to any of the functions in a DLL, we should release it, by making a call to the Win32™ API function FreeLibrary. Once a DLL is released, function pointers which refer to functions in the now-released DLL are invalid and should be set to 0 to indicate that. The other effect of releasing a DLL is that, if no other application needs the DLL, the operating system will remove it from memory, freeing up space for other application needs.
Let's say that the file old.c is the source to a DLL and that this source is now among the missing. There really isn't much to it. new.c is an example illustrating the concepts we have been discussing. It forwards application calls to the function "func1" to old DLL and modifies the result. It passes application call to the function "func2" to the old DLL and returns the results unaltered.
We have declared the old DLL handle as a static global. Every forwarding function checks this handle value to see if the old DLL had been loaded. If not, the forwarding function calls a helper function that loads the DLL. Each forwarding function also stores a static pointer to its counterpart in the old DLL. When the forwarding function runs, it checks to see if it has a valid pointer to its alter ego. If not, the forwarding function manufactures a valid pointer. Because the function pointer is static, each forwarding function must perform this only once, no matter how many times it is called. The first time a forwarding function runs, however, it takes longer to execute than it does on subsequent invocations, due to the time it takes to locate its counterpart in the old DLL.
Doing things this way has a couple of advantages:
It also has disadvantages:
To get an idea for the implications of the preceding bullet, let's take a look at a simple example application. The "Import Library" object attaches a DLL named in its "File Name" field. It uses the contents of the "Definition File" to know what functions in the DLL you want to make calls into. The definition file is essentially a C-language header file containing function prototypes. Stepping through the application program, we see that we first load the "new" DLL. If we list the DLL's the application has loaded after executing the "Import Library" object, we see a list that looks like:2
ListDLLs V2.0 Copyright (C) 1997 Mark Russinovich http://www.ntinternals.com ------------------------------------------------------------------------------ vee.exe pid: a5 Base Size Version Path 0x00400000 0xb000 5.00.0000.0000 d:\program files\hewlett-packard\vee 5.0\vee.exe 0x77f60000 0x5c000 4.00.1381.0130 D:\WINNT\System32\ntdll.dll 0x10000000 0x693000 5.00.0000.0000 d:\program files\hewlett-packard\vee 5.0\veerun50.dll 0x20000000 0x6c000 8.00.0000.0006 d:\program files\hewlett-packard\vee 5.0\LTKRN80N.dll 0x77f00000 0x5e000 4.00.1381.0133 D:\WINNT\system32\KERNEL32.dll 0x77e70000 0x54000 4.00.1381.0133 D:\WINNT\system32\USER32.dll 0x77ed0000 0x2c000 4.00.1381.0115 D:\WINNT\system32\GDI32.dll 0x77dc0000 0x3f000 4.00.1381.0121 D:\WINNT\system32\ADVAPI32.dll 0x77e10000 0x57000 4.00.1381.0131 D:\WINNT\system32\RPCRT4.dll 0x00240000 0x14000 8.00.0000.0001 d:\program files\hewlett-packard\vee 5.0\LTFIL80N.DLL 0x18000000 0x7000 d:\program files\hewlett-packard\vee 5.0\VEESRVCS.dll 0x776d0000 0x8000 4.00.1381.0131 D:\WINNT\System32\WSOCK32.dll 0x776b0000 0x14000 4.00.1381.0133 D:\WINNT\System32\WS2_32.dll 0x78000000 0x40000 6.00.8337.0000 D:\WINNT\system32\MSVCRT.dll 0x776a0000 0x7000 4.00.1381.0031 D:\WINNT\System32\WS2HELP.dll 0x77d80000 0x32000 4.00.1381.0027 D:\WINNT\system32\comdlg32.dll 0x77c40000 0x13c000 4.00.1381.0114 D:\WINNT\system32\SHELL32.dll 0x77aa0000 0x74000 4.72.3609.2200 D:\WINNT\system32\COMCTL32.dll 0x77b20000 0xb5000 4.00.1381.0117 D:\WINNT\system32\ole32.dll 0x65340000 0x92000 2.40.4268.0001 D:\WINNT\system32\OLEAUT32.dll 0x01060000 0x3e000 8.00.0000.0004 d:\program files\hewlett-packard\vee 5.0\LFCMP80N.DLL 0x77780000 0x6000 4.72.3110.0001 D:\WINNT\System32\msidle.dll 0x74ff0000 0xd000 4.00.1381.0131 D:\WINNT\System32\rnr20.dll 0x02b60000 0xc000 E:\Projects\Vee\forward\new.dll
Note that the application has loaded "new.dll" (highlighted in green).
The "Call" objects in this program provide a mechanism for calling the functions in a DLL. The object uses a period to separate the function name from the library which contains it. The library name is assigned in the "Import Library" object, so the string "new_lib.func1" instructs the "Call" object to call the function named "func1" in the library "new_lib". If we step to the point where we have called "func1" from our application, we see that we now have the following result from ListDlls:
ListDLLs V2.0 Copyright (C) 1997 Mark Russinovich http://www.ntinternals.com ------------------------------------------------------------------------------ vee.exe pid: a5 Base Size Version Path 0x00400000 0xb000 5.00.0000.0000 d:\program files\hewlett-packard\vee 5.0\vee.exe 0x77f60000 0x5c000 4.00.1381.0130 D:\WINNT\System32\ntdll.dll 0x10000000 0x693000 5.00.0000.0000 d:\program files\hewlett-packard\vee 5.0\veerun50.dll 0x20000000 0x6c000 8.00.0000.0006 d:\program files\hewlett-packard\vee 5.0\LTKRN80N.dll 0x77f00000 0x5e000 4.00.1381.0133 D:\WINNT\system32\KERNEL32.dll 0x77e70000 0x54000 4.00.1381.0133 D:\WINNT\system32\USER32.dll 0x77ed0000 0x2c000 4.00.1381.0115 D:\WINNT\system32\GDI32.dll 0x77dc0000 0x3f000 4.00.1381.0121 D:\WINNT\system32\ADVAPI32.dll 0x77e10000 0x57000 4.00.1381.0131 D:\WINNT\system32\RPCRT4.dll 0x00240000 0x14000 8.00.0000.0001 d:\program files\hewlett-packard\vee 5.0\LTFIL80N.DLL 0x18000000 0x7000 d:\program files\hewlett-packard\vee 5.0\VEESRVCS.dll 0x776d0000 0x8000 4.00.1381.0131 D:\WINNT\System32\WSOCK32.dll 0x776b0000 0x14000 4.00.1381.0133 D:\WINNT\System32\WS2_32.dll 0x78000000 0x40000 6.00.8337.0000 D:\WINNT\system32\MSVCRT.dll 0x776a0000 0x7000 4.00.1381.0031 D:\WINNT\System32\WS2HELP.dll 0x77d80000 0x32000 4.00.1381.0027 D:\WINNT\system32\comdlg32.dll 0x77c40000 0x13c000 4.00.1381.0114 D:\WINNT\system32\SHELL32.dll 0x77aa0000 0x74000 4.72.3609.2200 D:\WINNT\system32\COMCTL32.dll 0x77b20000 0xb5000 4.00.1381.0117 D:\WINNT\system32\ole32.dll 0x65340000 0x92000 2.40.4268.0001 D:\WINNT\system32\OLEAUT32.dll 0x01060000 0x3e000 8.00.0000.0004 d:\program files\hewlett-packard\vee 5.0\LFCMP80N.DLL 0x77780000 0x6000 4.72.3110.0001 D:\WINNT\System32\msidle.dll 0x74ff0000 0xd000 4.00.1381.0131 D:\WINNT\System32\rnr20.dll 0x02b60000 0xc000 E:\Projects\Vee\forward\new.dll 0x02c80000 0xc000 E:\Projects\Vee\forward\old.dll
Due the the fact that the new DLL loads another called (imaginatively) "old.dll", we show both DLL's. When we run the application program to completion, we see that we have unloaded "new.dll" but that "old.dll" remains:
ListDLLs V2.0 Copyright (C) 1997 Mark Russinovich http://www.ntinternals.com ------------------------------------------------------------------------------ vee.exe pid: a5 Base Size Version Path 0x00400000 0xb000 5.00.0000.0000 d:\program files\hewlett-packard\vee 5.0\vee.exe 0x77f60000 0x5c000 4.00.1381.0130 D:\WINNT\System32\ntdll.dll 0x10000000 0x693000 5.00.0000.0000 d:\program files\hewlett-packard\vee 5.0\veerun50.dll 0x20000000 0x6c000 8.00.0000.0006 d:\program files\hewlett-packard\vee 5.0\LTKRN80N.dll 0x77f00000 0x5e000 4.00.1381.0133 D:\WINNT\system32\KERNEL32.dll 0x77e70000 0x54000 4.00.1381.0133 D:\WINNT\system32\USER32.dll 0x77ed0000 0x2c000 4.00.1381.0115 D:\WINNT\system32\GDI32.dll 0x77dc0000 0x3f000 4.00.1381.0121 D:\WINNT\system32\ADVAPI32.dll 0x77e10000 0x57000 4.00.1381.0131 D:\WINNT\system32\RPCRT4.dll 0x00240000 0x14000 8.00.0000.0001 d:\program files\hewlett-packard\vee 5.0\LTFIL80N.DLL 0x18000000 0x7000 d:\program files\hewlett-packard\vee 5.0\VEESRVCS.dll 0x776d0000 0x8000 4.00.1381.0131 D:\WINNT\System32\WSOCK32.dll 0x776b0000 0x14000 4.00.1381.0133 D:\WINNT\System32\WS2_32.dll 0x78000000 0x40000 6.00.8337.0000 D:\WINNT\system32\MSVCRT.dll 0x776a0000 0x7000 4.00.1381.0031 D:\WINNT\System32\WS2HELP.dll 0x77d80000 0x32000 4.00.1381.0027 D:\WINNT\system32\comdlg32.dll 0x77c40000 0x13c000 4.00.1381.0114 D:\WINNT\system32\SHELL32.dll 0x77aa0000 0x74000 4.72.3609.2200 D:\WINNT\system32\COMCTL32.dll 0x77b20000 0xb5000 4.00.1381.0117 D:\WINNT\system32\ole32.dll 0x65340000 0x92000 2.40.4268.0001 D:\WINNT\system32\OLEAUT32.dll 0x01060000 0x3e000 8.00.0000.0004 d:\program files\hewlett-packard\vee 5.0\LFCMP80N.DLL 0x77780000 0x6000 4.72.3110.0001 D:\WINNT\System32\msidle.dll 0x74ff0000 0xd000 4.00.1381.0131 D:\WINNT\System32\rnr20.dll 0x02c80000 0xc000 E:\Projects\Vee\forward\old.dll
The "Delete Library" object instructs our example application to free the library whose name was assigned in the "Import Library"object. The operating system will unload "old.dll" from memory when it is no longer referenced, which will, in all probability, be when the HP VEE application exits.
There is another point worth mentioning about our example forwarding DLL. We have used a function called veeRaiseError to throw an HP VEE-specific exception when we detect that we can't load an old DLL or can't locate a function in an old DLL. Because the return value from a function can't necessarily be a reliable indicator that something is amiss, we use this function as a definitive way to signal an error. This function raises an exception that propagates back into HP VEE. This exception manifests itself just as any other error would. It can be caught by adding an error pin to the appropriate "Call Function" object. To support this, our make file links against a "services" library that HP VEE ships with.
So there you have it. If I was a real stud, I'd whip up an application that would automatically create a forwarding DLL given a header file. Actually, I'm working on it. If there's enough interest in it, I'll make it available in another paper. Let me know of your interest by sending me an e-mail.
1My, somewhat cryptic, notation for being able to identify the signature a given function pointer type is intended to reference is to use the string "p_" to indicate a pointer, followed by a sequence of characters representing the function return type and argument type list, followed by the string "_fn" to indicate that his pointer points to a function. So p_ll_fn is a pointer to a function that returns a long and takes a long as an argument.
2This listing was generated using a utility titled "ListDlls" available for download from the SysInternals web site.