A library DLL contains the object code for the functions that are part of the library along with some additional information to allow the DLL to be usable.
However a library DLL does not contain the actual type information needed to determine the specific argument list and types for the functions contained in the library DLL. The main information in a library DLL is: (1) a list of the functions that the DLL exports along with the address information that will connect a call of a function to the actual function binary code and (2) a list of any required DLLs that the functions in the library DLL use.
You can actually open a library DLL in a text editor, I suggest a small one, and scan through the arcane symbols of the binary code until you reach the section that contains the list of functions in the library DLL as well as other required DLLs.
So a library DLL contains the bare minimum information needed to (1) find a particular function in the library DLL so that it can be invoked and (2) a list of other needed DLLs that the functions in the library DLL depend on.
This is different from a COM object which normally does have type information in order to support the ability to do what is basically reflection and explore the COM object's services and how those services are accessed. You can do this with Visual Studio and other IDEs which generate a list of COM objects installed and allow you to load a COM object and explore it. Visual Studio also has a tool that will generate the source code files that provide the stubs and include file for accessing the services and methods of a COM object.
However a library DLL is different from a COM object and all the additional information provided with a COM object is not available from a library DLL. Instead a library DLL package is normally made up of (1) the library DLL itself, (2) a .lib file that contains the linkage information for the library DLL along with the stubs and functionality to satisfy the linker when building your application which uses the library DLL, and (3) an include file with the function prototypes of the functions in the library DLL.
So you create your application by calling the functions which reside in the library DLL but using the type information from the include file and linking with the stubs of the associated .lib file. This procedure allows Visual Studio to automate much of the work required to use a library DLL.
Or you can hand code the LoadLibrary()
and the building of a table of the functions in the library DLL using GetProcAddress()
. By doing hand coding all you really need are the function prototypes of the functions in the library DLL which you then can type in yourself and the library DLL itself. You are in effect doing the work by hand that the Visual Studio compiler does for you if you are using the .lib library stubs and include file.
If you know the actual function name and the function prototype of a function in a library DLL then what you could do is to have your command line utility require the following information:
- the name of the function to be called as a text string on the command
line
- the list of the arguments to be used as a series of text strings on the command line
- an additional parameter that describes the function prototype
This is similar to how functions in the C and C++ runtime which accept variable argument lists with unknown parameter types work. For instance the printf()
function which prints a list of argument values has a format string followed by the arguments to be printed. The printf()
function uses the format string to determine the types of the various arguments, how many arguments to expect, and what kinds of value transformations to do.
So if your utility had a command line something like the following:
dofunc "%s,%d,%s" func1 "name of " 3 " things"
And the library DLL had a function whose prototype looked like:
void func1 (char *s1, int i, int j);
then the utility would dynamically generate the function call by transforming the character strings of the command line into the actual types needed for the function to be called.
This would work for simple functions that take Plain Old Data types however more complicated types such as struct
type argument would require more work as you would need some kind of a description of the struct
along with some kind of argument description perhaps similar to JSON.
Appendix I: A simple example
The following is the source code for a Visual Studio Windows console application that I ran in the debugger. The command arguments in the Properties was pif.dll PifLogAbort
which caused a library DLL from another project, pif.dll, to be loaded and then the function PifLogAbort()
in that library to be invoked.
NOTE: The following example depends on a stack based argument passing convention as is used with most x86 32 bit compilers. Most compilers also allow for a calling convention to be specified other than stack based argument passing such as the __fastcall
modifier of Visual Studio. Also as pointed out in the comments, the default for x64 and 64 bit Visual Studio is to use the __fastcall
convention by default so that function arguments are passed in registers and not on the stack. See Overview of x64 Calling Conventions in the Microsoft MSDN. See as well the comments and discussion in How are variable arguments implemented in gcc?
.
Notice how the argument list to the function PifLogAbort()
is built as a structure that contains an array. The argument values are put into the array of a variable of the struct
and then the function is called passing the entire struct
by value. What this does is to push a copy of the array of parameters onto the stack and then calls the function. The PifLogAbort()
function sees the stack based on its argument list and processes the array elements as individual arguments or parameters.
// dllfunctest.cpp : Defines the entry point for the console application.
//
#include "stdafx.h"
typedef struct {
UCHAR *myList[4];
} sarglist;
typedef void ((*libfunc) (sarglist q));
/*
* do a load library to a DLL and then execute a function in it.
*
* dll name.dll "funcname"
*/
int _tmain(int argc, _TCHAR* argv[])
{
HMODULE dll = LoadLibrary(argv[1]);
if (dll == NULL) return 1;
// convert the command line argument for the function name, argv[2] from
// a TCHAR to a standard CHAR string which is what GetProcAddress() requires.
char funcname[256] = {0};
for (int i = 0; i < 255 && argv[2][i]; i++) {
funcname[i] = argv[2][i];
}
libfunc generic_function = (libfunc) GetProcAddress(dll, funcname);
if (generic_function == NULL) return 2;
// build the argument list for the function and then call the function.
// function prototype for PifLogAbort() function exported from the library DLL
// is as follows:
// VOID PIFENTRY PifLogAbort(UCHAR *lpCondition, UCHAR *lpFilename, UCHAR *lpFunctionname, ULONG ulLineNo);
sarglist xx = {{(UCHAR *)"xx1", (UCHAR *)"xx2", (UCHAR *)"xx3", (UCHAR *)1245}};
generic_function(xx);
return 0;
}
This simple example illustrates some of the technical hurdles that must be overcome. You will need to know how to translate the various parameter types into the proper alignment in a memory area which is then pushed onto the stack.
The interface to this example function is remarkably homogeneous in that most of the arguments are unsigned char
pointers with the exception of the last which is an int
. With a 32 bit executable all four of these variable types have the same length in bytes. With a more varied list of types in the argument list you will need to have an understanding as to how your compiler aligns parameters when it is pushing the arguments onto the stack before doing the call.
Appendix II: Extending the simple example
Another possibility is to have a set of helper functions along with a different version of the struct
. The struct
provides a memory area to create a copy of the necessary stack and the help functions are used to build the copy.
So the struct
and its helper functions may look like the following.
typedef struct {
UCHAR myList[128];
} sarglist2;
typedef struct {
int i;
sarglist2 arglist;
} sarglistlist;
typedef void ((*libfunc2) (sarglist2 q));
void pushInt (sarglistlist *p, int iVal)
{
*(int *)(p->arglist.myList + p->i) = iVal;
p->i += sizeof(int);
}
void pushChar (sarglistlist *p, unsigned char cVal)
{
*(unsigned char *)(p->arglist.myList + p->i) = cVal;
p->i += sizeof(unsigned char);
}
void pushVoidPtr (sarglistlist *p, void * pVal)
{
*(void * *)(p->arglist.myList + p->i) = pVal;
p->i += sizeof(void *);
}
And then the struct
and helper functions would be used to build the argument list like the following after which the function from the library DLL is invoked with the copy of the stack provided:
sarglistlist xx2 = {0};
pushVoidPtr (&xx2, "xx1");
pushVoidPtr (&xx2, "xx2");
pushVoidPtr (&xx2, "xx3");
pushInt (&xx2, 12345);
libfunc2 generic_function2 = (libfunc2) GetProcAddress(dll, funcname);
generic_function2(xx2.arglist);
argv[3]...argv[argc-1]
as function arguments), this won't do it, and doing it right gets complicated quickly. – WhozCraigif I could pass a void-pointer-list to the function-pointer
. If the function you are calling is defined as ... MyFunction(void *), then yes, you can call it like this, otherwise you would not be able to. Also, make sure it is labeled asdeclspec(stdcall)
. – seva titov