CreateThread Win32 API에 C++ 클래스의 멤버 함수를 전달하는 방법
(주의: 이 글은, 원리를 좀 더 이해하자는 차원에서 쓰는 것일 뿐 현업에서 이렇게 쓰는 것을 권장하지는 않습니다.)
예전 글에서,
C++ 개발자들을 위한 C# Thread 동작 방식
; https://www.sysnet.pe.kr/2/0/11069
제가 C++ 클래스의 멤버 함수는 CreateThread의 인자에 전달할 수 없다고 했습니다. 예를 들어, 다음과 같은 클래스가 있다고 했을 때,
#include "stdafx.h"
#include <Windows.h>
class MyClass
{
public:
DWORD dummy(void *ptr) { return 0; }
};
int main()
{
LPTHREAD_START_ROUTINE pThreadFunc = (LPTHREAD_START_ROUTINE)(&MyClass::dummy); // 컴파일러 에러
HANDLE hHandle = ::CreateThread(nullptr, 0, pThreadFunc, nullptr, 0, nullptr);
::WaitForSingleObject(hHandle, -1);
return 0;
}
다음과 같은 컴파일 에러 때문에 할 수 없다고 했습니다.
Error C2440 'type cast': cannot convert from 'DWORD (__thiscall MyClass::* )(void *)' to 'LPTHREAD_START_ROUTINE'
사실 이것의 근본적인 문제점은 멤버 함수 포인터를 결국 void * 포인터로 형 변환할 수 없다는 것에서 기인합니다.
void *pVoid = (void *)&MyClass::dummy;
// Error C2440 'type cast': cannot convert from 'DWORD (__thiscall MyClass::* )(void *)' to 'void *'
일단 void *로 변환할 수 있다면 다른 모든 포인터로 형 변환하는 것은 문제가 없기 때문입니다.
그런데 방법이 있습니다. ^^ 예전에 제가 다음과 같은 글을 쓴 적이 있더군요. ^^;;;
클래스 멤버 함수에 대한 포인터를 받는 표현.
; https://www.sysnet.pe.kr/2/0/147
위의 코드에 보면, 멤버 함수 포인터를 void *로 바꾸는 것이 나옵니다. (그렇습니다. 부끄러운 이야기지만 저도 가끔 제가 쓴 글을 기억하지 못해 다시 봐야 아는 경우가 종종 있습니다. ^^;)
위의 코드를 우리의 CreateThread 예제에 적용해 보면 다음과 같습니다.
#include "stdafx.h"
#include <Windows.h>
class MyClass
{
public:
DWORD dummy()
{
printf("dummy called!\n");
return 0;
}
};
int main()
{
{
typedef DWORD (MyClass::*ThisFunc)();
ThisFunc method = &MyClass::dummy;
void *pFunc = (void *)*(int *)&method;
LPTHREAD_START_ROUTINE threadFunc = (LPTHREAD_START_ROUTINE)pFunc;
HANDLE hHandle = ::CreateThread(nullptr, 0, threadFunc, nullptr, 0, nullptr);
::WaitForSingleObject(hHandle, -1);
}
// 한줄 줄여서,
{
typedef DWORD (MyClass::*ThisFunc)();
ThisFunc method = &MyClass::dummy;
LPTHREAD_START_ROUTINE threadFunc = (LPTHREAD_START_ROUTINE)(void *)*(int *)&method;
HANDLE hHandle = ::CreateThread(nullptr, 0, threadFunc, nullptr, 0, nullptr);
::WaitForSingleObject(hHandle, -1);
}
return 0;
}
/*
출력 결과:
dummy called!
dummy called!
*/
그런데, 이게 끝이 아닙니다. 인스턴스를 넘겨 동작시켜 보면 그 이유를 알 수 있습니다.
#include "stdafx.h"
#include <Windows.h>
class MyClass
{
int data = 500;
public:
DWORD dummy()
{
printf("dummy called: %d!\n", data);
return 0;
}
};
int main()
{
MyClass *thisPtr = new MyClass();
{
typedef DWORD(MyClass::*ThisFunc)();
ThisFunc method = &MyClass::dummy;
LPTHREAD_START_ROUTINE threadFunc = (LPTHREAD_START_ROUTINE)(void *)*(int *)&method;
HANDLE hHandle = ::CreateThread(nullptr, 0, threadFunc, thisPtr, 0, nullptr);
::WaitForSingleObject(hHandle, -1);
}
delete thisPtr;
return 0;
}
/*
출력 결과 (data의 값으로 쓰레기 값이 출력됨)
dummy called: 354281!
*/
왜냐하면, CreateThread는 전달된 threadFunc을 실행하면서 thisPtr 인자를 push 명령어로 스택을 통해 전달합니다. 반면, C++의 클래스 멤버 함수는 기본적으로 this 포인터 인자를 ecx 레지스터를 통해서 전달받으므로 dummy 메서드가 실제로 실행된 시점의 ecx 레지스터에는 알 수 없는 주솟값이 들어가 있게 됩니다.
이 문제를 해결하려면, dummy 메서드가 CreateThread가 스택을 통해 넘겨주는 것처럼 this 포인터를 ecx가 아닌 스택을 통해 받아야 합니다. 이를 위해, 호출 규약을 (기본값인 thiscall 대신) stdcall 등을 사용하도록 바꿔주면 됩니다.
#include "stdafx.h"
#include <Windows.h>
class MyClass
{
int data = 500;
public:
DWORD __stdcall dummy()
{
printf("dummy called: %d!\n", data);
return 0;
}
};
int main()
{
MyClass *thisPtr = new MyClass();
{
typedef DWORD(__stdcall MyClass::*StdFunc)();
StdFunc method = &MyClass::dummy;
LPTHREAD_START_ROUTINE threadFunc = (LPTHREAD_START_ROUTINE)(void *)*(int *)&method;
HANDLE hHandle = ::CreateThread(nullptr, 0, threadFunc, thisPtr, 0, nullptr);
::WaitForSingleObject(hHandle, -1);
}
delete thisPtr;
return 0;
}
/*
출력 결과 (data의 값으로 쓰레기 값이 출력됨)
dummy called: 500!
*/
마지막으로, 위의 코드는 32비트에서만 안정적이고 64비트로 오면 비정상 종료할 수 있습니다. 왜냐하면 *(int *)&method; 형 변환에서 int 범위로 값이 잘리기 때문입니다. 따라서, 32/64비트 모두 잘 동작시키려면 다음과 같이 바꿔주어야 합니다.
LPTHREAD_START_ROUTINE threadFunc = (LPTHREAD_START_ROUTINE)(void *)*(__int64 *)&method;
(
첨부 파일은 이 글의 예제 코드를 포함합니다.)
[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]