Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / C++

Create a thread from globalstatic void returning a function with any type and number of arguments

3.00/5 (1 vote)
9 Nov 2007CPOL3 min read 1   103  
This is a macro which takes the name of any void returning globalstatic procedure, followed by the list of arguments to be passed to that procedure, and invokes the procedure with the arguments supplied, in a new thread.

Introduction

This is a very general problem. Say, I have a function like:

C++
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:

  1. Make a structure like this:
  2. C++
    struct funcargs
    {
        int m_a;
        int m_b;
    };
  3. Then make a dynamic object of this as:
  4. C++
    funcargs *argobjptr = new funcargs;
  5. Putting arguments in the object like:
  6. C++
    argobjptr->m_a = a; 
    argobjptr->m_b = b;
  7. Create another wrapper function to be used with CreateProcess or any similar method, like this:
  8. C++
    DWORD WINAPI ThreadProc(LPVOID lpParameter)
    {
        funcargs *argobjptr = (funcargs *)lpParameter;
        func(argobjptr->m_a, argobjptr->m_b);
        delete argobjptr
        return 0;
    }
  9. And at last, call this ThreadProc with the previously created "argobjptr" object in CreateThread, somewhat like this:
  10. C++
    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:

C++
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:

C++
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:

C++
#define CALL_IN_THREAD    if(CThreadWrapper tempobj=0) tempobj.FunctionCaller

//Main wrapper class for calling a function in a thread.
//manages variable and data types used in this process
class CThreadWrapper  
{
    //Structure to store function call information
    struct FUNC_CALL_INFO{
        FUNC_TYPE        m_pFuncAddress;
        DWORD            *m_pArgs;
        int                m_pArgLength;
    };

    //Stores value of stack pointer before the call of desired function
    DWORD m_InitialStackAddress;

    //Uses 'FUNC_CALL_INFO' object to call the 
    //desired function with given arguments
    static DWORD WINAPI ThreadFunc( LPVOID lpParam );
public:
    CThreadWrapper(const int);

    // Creates and object of 'FUNC_CALL_INFO' to be used in ThreadFunc
    void FunctionCaller(LPTHREAD_INFORMATION p_pthreadinfo, FUNC_TYPE func_address, ...);

    //helper function for MACRO with if condition
    __forceinline operator const bool(){return true;}
}; 

/*
Constructor function. in addition it also calculate the address of stack pointer 
before the call to this function 
*/

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
}

/*
This functions does following tasks:
1>Copies the arguments passed to a temperory buffer
    a> calculate address of last argument
    b> subtract address of last argument from first to calculate
        number of arguments
    c>allocate a new buffer of the size of all arguments
    d>copies all the arguments to buffer
2>Put all information in a wraaper object 'FUNC_CALL_INFO'
3>Call CreateThread on 'ThreadFunc'
*/

void CThreadWrapper::FunctionCaller(LPTHREAD_INFORMATION p_pthreadinfo, 
                                    FUNC_TYPE func_address, ...)
{
    DWORD CurrentStackAddress = (DWORD)&func_address;
//    _asm mov CurrentStackAddress, ebp;

#ifdef _DEBUG
    printf("\n FunctionAddress = 0x%08X",func_address);
#endif

#ifdef _DEBUG
    printf("\n CurrentStackAddress = 0x%08X",CurrentStackAddress);
#endif

    //Calculate no of arguments passed in 'DWORD'
    DWORD arglen = ( ( m_InitialStackAddress - CurrentStackAddress )
                            - OTHER_STACK_DATA_SIZE ) / STACK_DATA_SIZE;
#ifdef _DEBUG
    printf("\n Number of Arguments = %d",arglen);
#endif

    //Allocate memory on heap to copy arguments from stack
    DWORD *args = (DWORD *) malloc( arglen * STACK_DATA_SIZE );

    //Set pointer to argument on stack
    DWORD *argadd = (DWORD *)(CurrentStackAddress + OTHER_STACK_DATA_SIZE);

    //Copy arguments on heap
    for( unsigned int index=0; index < arglen; argadd++,index++)
        *(args + index) = *argadd;


    //Wrap up address of function to call and arguments in a object
    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;

    //Create a thread with 'ThreadFunc' as wrapper function
    p_pthreadinfo->hThread = CreateThread( NULL, 0, CThreadWrapper::ThreadFunc, finfo, 0, 
                                              &(p_pthreadinfo->dwThreadId));
}

/*
This functions does following tasks:
1>fetch information from FUNC_CALL_INFO object
    a> fetch all arguments
    b> fectch function to be called
2>Push arguments into stack to be used by function to be called
3>Call the function
4>Adjust stack
5>free up allocated memory
*/

DWORD WINAPI CThreadWrapper::ThreadFunc( LPVOID lpParam ) 
{ 
    //Retrieve Function Call information
    FUNC_CALL_INFO *finfo    = (FUNC_CALL_INFO *)lpParam;
    DWORD *args        = finfo->m_pArgs;
    DWORD arglen    = finfo->m_pArgLength;
    FUNC_TYPE funcadd        = finfo->m_pFuncAddress;

    //Push all arguments to stack
    long index            = arglen-1;
    unsigned int value    = 0;
    unsigned stackdisp    = arglen*sizeof(DWORD);

    while( index>=0 )
    {
        value = *( args + index );
        index--;
        __asm push value;
    }

    //Call actual function
    __asm call funcadd; 

    //Rectify stack
    __asm add esp,stackdisp;
    
    //Free up memory
    free(finfo->m_pArgs);
    free(finfo);

    return 0; 
}

Using the code

Using the code is easy. Just look at the following test code:

C++
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;

    //func & func1, would be called in a thread.
    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. :)

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)