Introduction
This is a very general problem. Say, I have a function like:
void func(int a, int b)
{
printf("\n a = %d, b = %d",a,b);
}
and I need to call this function in a new thread.
The simplest procedure is:
- Make a structure like this:
struct funcargs
{
int m_a;
int m_b;
};
- Then make a dynamic object of this as:
funcargs *argobjptr = new funcargs;
- Putting arguments in the object like:
argobjptr->m_a = a;
argobjptr->m_b = b;
- Create another wrapper function to be used with
CreateProcess
or any similar method, like this:
DWORD WINAPI ThreadProc(LPVOID lpParameter)
{
funcargs *argobjptr = (funcargs *)lpParameter;
func(argobjptr->m_a, argobjptr->m_b);
delete argobjptr
return 0;
}
- And at last, call this
ThreadProc
with the previously created "argobjptr
" object in CreateThread, somewhat like this:
CreateThread( NULL, 0, ThreadProc, argobjptr, 0, NULL):
So, on the whole, we have to go through this hassle of packing arguments in a temporary wrapper object and then unpacking them inside some wrapper function, because all such thread creating APIs limit us to create threads with a function with only this syntax:
DWORD WINAPI ThreadProc(LPVOID lpParameter)
It would be much better if we could have an API that could create a thread from any function returning anything with any number and type of parameters.
Unfortunately, this is very tough because the scope of problems is very wide; we need simple functions, returning some object\reference\pointer\void, and they could then take some arguments\reference\pointers, and the number of arguments could vary with each function.
Then the function can be inline\static\class member\ virtual......
I have tried to solve this problem to some extent, and I created a macro, which just takes the function name and its parameters (any number and type) as arguments and calls the function upon the given arguments in a new thread.
Like for the above function, the code would simply be:
CALL_IN_THREAD(func, 4, 5);
Of course, this has some limitations too:
- First of all, the macro works only on global\static functions returning
void
. - Second, the arguments shouldn't be reference\pointer to any local variable\object (by simple logic, how could a thread access an object allocated some time back in the context\stack of some other thread?).
- Thirdly, the macro would bang, in case the number of parameters does not match, as expected by the actual function.
- Fourthly, the macro is highly platform\hardware\compiler dependent. I have tested it on an x86 machine with Windows NT versions (NT, 2000, and XP) on Visual Studio 6.0 and Visual Studio 2005 compilers.
In short, keep in mind that this macro uses the same old process described above inside it to accomplish this task.
Behind the scenes
The working of this code is simple.
This code works on the principle that whenever a function is called, to pass arguments to that function, they are pushed on the stack first, before the call to that function would be made.
So if we need to call the same function in the context of some other thread, we need to push the same arguments on the stack of that thread before calling this function there.
What we can do is, we can copy all the arguments from the stack of this function, store them in a temporary heap location, and paste them on the stack of the new thread, and then can call that function. The function would work as if called from the original thread.
This is exactly what we do above in a conventional approach, but the difference here is that the things are just automated (packing\unpacking of arguments and calling that function) here.
But, how would we know the start and end address of the stack memory area holding the argument values?
We can, if we can just some how calculate the address of the stack pointer just before the first argument (last in the argument list, as arguments are pushed from last in the __cdecl calling convention) is passed. The end position would be 4 bytes before the address of the pointer to the function passed (which is pushed as the last argument).
Have a look at the code here:
#define CALL_IN_THREAD if(CThreadWrapper tempobj=0) tempobj.FunctionCaller
class CThreadWrapper
{
struct FUNC_CALL_INFO{
FUNC_TYPE m_pFuncAddress;
DWORD *m_pArgs;
int m_pArgLength;
};
DWORD m_InitialStackAddress;
static DWORD WINAPI ThreadFunc( LPVOID lpParam );
public:
CThreadWrapper(const int);
void FunctionCaller(LPTHREAD_INFORMATION p_pthreadinfo, FUNC_TYPE func_address, ...);
__forceinline operator const bool(){return true;}
};
CThreadWrapper::CThreadWrapper(const int)
{
DWORD ebpvalue;
__asm mov ebpvalue, ebp
m_InitialStackAddress = ebpvalue + 12
#ifdef _DEBUG
printf("\n m_InitialStackAddress = 0x%08X",m_InitialStackAddress)
#endif
}
void CThreadWrapper::FunctionCaller(LPTHREAD_INFORMATION p_pthreadinfo,
FUNC_TYPE func_address, ...)
{
DWORD CurrentStackAddress = (DWORD)&func_address;
#ifdef _DEBUG
printf("\n FunctionAddress = 0x%08X",func_address);
#endif
#ifdef _DEBUG
printf("\n CurrentStackAddress = 0x%08X",CurrentStackAddress);
#endif
DWORD arglen = ( ( m_InitialStackAddress - CurrentStackAddress )
- OTHER_STACK_DATA_SIZE ) / STACK_DATA_SIZE;
#ifdef _DEBUG
printf("\n Number of Arguments = %d",arglen);
#endif
DWORD *args = (DWORD *) malloc( arglen * STACK_DATA_SIZE );
DWORD *argadd = (DWORD *)(CurrentStackAddress + OTHER_STACK_DATA_SIZE);
for( unsigned int index=0; index < arglen; argadd++,index++)
*(args + index) = *argadd;
FUNC_CALL_INFO *finfo = (FUNC_CALL_INFO *)malloc(sizeof(FUNC_CALL_INFO));
finfo->m_pFuncAddress = func_address;
finfo->m_pArgLength = arglen;
finfo->m_pArgs = args;
p_pthreadinfo->hThread = CreateThread( NULL, 0, CThreadWrapper::ThreadFunc, finfo, 0,
&(p_pthreadinfo->dwThreadId));
}
DWORD WINAPI CThreadWrapper::ThreadFunc( LPVOID lpParam )
{
FUNC_CALL_INFO *finfo = (FUNC_CALL_INFO *)lpParam;
DWORD *args = finfo->m_pArgs;
DWORD arglen = finfo->m_pArgLength;
FUNC_TYPE funcadd = finfo->m_pFuncAddress;
long index = arglen-1;
unsigned int value = 0;
unsigned stackdisp = arglen*sizeof(DWORD);
while( index>=0 )
{
value = *( args + index );
index--;
__asm push value
}
__asm call funcadd
__asm add esp,stackdisp;
free(finfo->m_pArgs)
free(finfo)
return 0
}
Using the code
Using the code is easy. Just look at the following test code:
bool complete = false;
void func(char u, char e, int a, int b)
{
while(complete == false)
printf("a = %d, b = %d\n",a,b);
int t=7, y= 8;
int c = t+y;
}
void func1(int a, int b)
{
Sleep(2000);
printf("\n\n\n\n\na = %d, b = %d\n\n\n\n\n\n",a,b);
complete = true;
}
int main(int argc, char* argv[])
{
printf("Hello World!\n");
THREAD_INFORMATION t;
for(int i=0;i<3;i++)
CALL_IN_THREAD(&t, (FUNC_TYPE)func, 'U', 'A', 7+i, 8*i);
CALL_IN_THREAD(&t, (FUNC_TYPE)func1, 4, 5);
while(complete == false){};
getchar();
return 0;
}
Here, the functions 'func
' and 'func1
' has been invoked in a different thread. The THREAD_INFORMATION
object stores the handle and thread ID of the created threads, but it hasn't been used here.
What next???
Next, I'll try to make this macro work on class member functions. Your help and suggestions would be highly appreciated. :)