Runtime 'Dynamic' DLL calling

Environment:Visual C++ (SP3), Windows NT4 (SP3)

The Problem:

Call a function in a DLL passing some arguments. The DLL, function and arguments are not known until run-time. The argument types are limited to standard types (i.e. char, short, long, float, char*) but not, for the sake of simplicity, doubles (due to their 8 bytes). Further types could be added, I think, but for now we'll leave them alone. I'm still not sure about return types and the solution that I propose can only cope with return types that fit into 4 bytes.

Notes on the problem

  1. I have posted this article not so much as to give a solution but, as an idea that I expect to get ripped to shreds in the hope that what comes out is neat, tidy and useable.
  2. The solution should fix the problem as it stands i.e. we can't say, "just use COM" or "only have functions that take some complex object that can work out its own parameter list".
  3. void func(...) style functions are for the callee not the caller.

Possible Solution

  1. We start by going against rule two and limiting the maximum number of arguments to 10. We do this by creating a structure which contains an array of ten DWORDS and pass the struct by value to the DLL. The DLL will get all the data it needs on the stack (and probably a bit to spare).
    
    typedef struct
    {
    	DWORD dwARG[10];
    }STACK;
    
    typedef DWORD (*PFNDLL)(STACK);
    
    void main()
    {
    	HINSTANCE hDll = LoadLibrary("MyDll.dll")
    	PFDLL pfFunc = (PFDLL)GetProcAddress(hDll,"MyFunc");
    	STACK args;
    	char szBuf[] = "hello world";
    	DWORD dwRet;
    
    	args[0] = 10; // a dword
    	args[1] = (DWORD)szBuf;
    
    	dwRet = (pfFunc)(args);
    }
    
    We sort of solved the problem but had to impose extra limits, so no points there.
  2. This leads to a better solution, put our data on the stack ourselves and then we can have as much or as little as we want, and potentially deal with types other than the simple ones (once we know that the idea is sound). The problem is that this uses two lines of assembler code which is pushing my assembler knowledge by two lines. As far as I can tell this works for the types supported in the demo.
    
    // see the detail in the RuntimeDll.cpp file, all error handling
    // has been removed for brevity
    
    // pointer to function in dll, caller of CallDll
    typedef __declspec(dllimport)DWORD (*pfFunc)(void);
    
    void main()
    {
    	HINSTANCE hDll = ::LoadLibrary("MyDll.dll");
    	pfFunc pFun = NULL;	
    	DWORD dwTemp;
    	DWORD dwRet = 0;
    	float fRet = 0.0f;
    	int i;
    	bool bRetIsFloat = false;
    
    	nArgCount = 4;
    
    	int nArg[4];
    	long nA = 123456;
    	char cB = 20;
    	float fC = 5.678f;
    	char* szD  = "test string";
    
    	// memcpy not cast as a cast will loose data
    	// in the case of a float
    	memcpy(nArg+0, &nA ,sizeof(long));
    	memcpy(nArg+1, &cB ,sizeof(long));
    	memcpy(nArg+2, &fC ,sizeof(long));
    	memcpy(nArg+3, &szD,sizeof(long));
    
    	
    	pFun = (pfFunc)::GetProcAddress(hDll, "MyFunc");
    
    	// load the stack (in reverse order)
    	for( i=nArgCount-1; i>=0; i--)
    	{
    		// copy the data to a temporary holder
    		// (casts can lose data)
    		memcpy(&dwTemp, &(Arg[i].bVal), sizeof(DWORD));
    		// chuck it on the stack
    		_asm push dwTemp
    	}
    
    	// call the function, data already on the stack
    	if (bRetIsFloat)
    	{
    		// float returns are not passed via the EAX register but,
    		// it seem, are on the stack so ...
    		
    		// ok I didn't know this bit of asm I compiled some
    		// code and copied it once I had some idea of what
    		// the instructions do.
    		// call, calls a function
    		// fstp, does a store & pop
    		_asm
    		{
    			call dword ptr [pFun]
    			fstp dword ptr [fRet]
    		}
    	}
    	else
    	{
    		dwRet = (pFun)();
    	}
    
    	// unload the stack
    	for (i = 0; i<nArgCount; i++)
    	{
    		_asm pop dwTemp
    		// copy the data back in case of altered values
    		memcpy(&(Arg[i].bVal), &dwTemp, sizeof(DWORD));				
    	}
    	
    	if (bRetIsFloat)
    		printf("returned %f\n",fRet);
    	else
    		printf("returned %d\n",dwRet);
    }
    
    I have tested this code against a test DLL using various argument lists and return types and everything seems to work nicely, but as I don't know much about assembler I'm not sure what problems this code may cause. If you have any comments I'd be pleased to hear them. The Demo Source code contains this basic idea wrapped up in a class CRuntimeDll and uses CRuntimeDllException.
  3. Some other, better, less hacky, solution?

    Notes on the Demo project

    The demo allows you to call a function in a DLL. Follow the example below for an explanation of how the demo app can be used.

    To call the WINAPI GetWindowText(HWND hwnd,char* szBuffer,long nBufferSize) function:

    1. Set the DLL name to "user32.dll" and press "Load", all being well the "Add" , "Call" and "Set Type" buttons should be enabled and the "Load" button should now read "Unload".
    2. Enter "GetWindowTextA" (for non-unicode version) in the "Function" edit box.
    3. Set the "Value" edit box to that of any hwnd, get the value using Spy++, the value must be in decimal not hex!
    4. Select "unsigned long" on the "Types" list box then press "Add"
    5. Set the "Value" to "12345678901234567890" then add an "unsigned char*".
    6. Finally for nBufferSize set "Value" to 20 and add an "unsigned long".
    7. Press "Call".
    8. Click on the "unsigned char*" on the Args list and the window text (the first 19 bytes) for the window you entered should appear in the "Value" box.
    9. Press "Clear" to call another function in this DLL.

    The return type is always caught in a DWORD and can be converted after the function has been called. This is done by selecting a "Type" and pressing "Set Type", the value and type are displayed at the bottom of the dialog box. The Exception to this is functions that return type float, they must be declared before the function is called, the return type will remain float until it is changed to an integer or pointer type.

    Closing notes

    1. The string to value and value to string functionality should be moved into the CRuntimeDll object as should return type setting and access.
    2. Calling some DLL's with the wrong arguments can cause the demo to crash. I have yet to find a way to stop or catch this. Bear in mind though, that applications with this kind of functionality would normally be used by some sort of developer who expect this sort of thing ;-)

    Downloads

    Download demo application - 7 Kb
    Download demo source - 15 Kb

    History