Microsoft MVP성태의 닷넷 이야기
글쓴 사람
정성태 (techsharer at outlook.com)
홈페이지
첨부 파일
(연관된 글이 1개 있습니다.)

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;

(첨부 파일은 이 글의 예제 코드를 포함합니다.)




[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]

[연관 글]






[최초 등록일: ]
[최종 수정일: 3/25/2017]

Creative Commons License
이 저작물은 크리에이티브 커먼즈 코리아 저작자표시-비영리-변경금지 2.0 대한민국 라이센스에 따라 이용하실 수 있습니다.
by SeongTae Jeong, mailto:techsharer at outlook.com

비밀번호

댓글 작성자
 




... 121  122  123  124  125  126  [127]  128  129  130  131  132  133  134  135  ...
NoWriterDateCnt.TitleFile(s)
2880정성태3/7/201521678개발 환경 구성: 260. C# Code Coverage 도구 - Semantic Designs 소개
2879정성태3/3/201526685개발 환경 구성: 259. Visual Studio 없이 Visual C++ 컴파일하는 방법
2878정성태2/28/201527568.NET Framework: 503. == 연산자보다는 Equals 메서드의 호출이 더 권장됩니다. [3]파일 다운로드1
2877정성태2/28/201521754.NET Framework: 502. 연산자 재정의(operator overloading)와 메서드 재정의(method overriding)의 다른 점 - 가상 함수 호출 여부 [3]파일 다운로드1
2876정성태2/27/201524217VS.NET IDE: 98. IntegraStudio - Visual Studio에서 Java 프로그램 개발
2875정성태2/26/201522832디버깅 기술: 72. Visual Studio 2013에서의 sos.dll 사용 제한
2874정성태2/26/201519576디버깅 기술: 71. windbg + 닷넷 디버깅 (2) - null 체크 패턴
2873정성태2/25/201537059.NET Framework: 501. FtpWebRequest 타입을 이용해 FTP 파일 업로드 [4]파일 다운로드1
2872정성태2/25/201521241디버깅 기술: 70. windbg + 닷넷 디버깅 (1) - 배열 인덱스 사용 패턴
2871정성태2/24/201525207개발 환경 구성: 258. 윈도우 8.1에서 방화벽과 함께 FTP 서버 여는 (하지만, 권장하지 않는) 방법 [1]
2870정성태2/24/201526344개발 환경 구성: 257. 윈도우 8.1에서 방화벽과 함께 FTP 서버 여는 방법
2869정성태2/23/201520314.NET Framework: 500. struct로 정의한 값 형식(Value Type)의 경우 Equals 재정의를 권장합니다.파일 다운로드1
2868정성태2/23/201524823VS.NET IDE: 97. Visual C++ 프로젝트 디버깅 시에 Step-Into(F11) 동작이 원치 않는 함수로 진입하는 것을 막는 방법 [2]
2867정성태2/23/201518534오류 유형: 273. File History - Failed to initiate user data backup (error 80070005)
2866정성태2/23/201520369오류 유형: 272. WAT080 : Failed to locate the Windows Azure SDK. Please make sure the Windows Azure SDK v2.1 is installed.
1868정성태2/20/201517674오류 유형: 271. The type '...' cannot be used as type parameter 'TContext' in the generic type or method 'System.ServiceModel.DomainServices.EntityFramework.LinqToEntitiesDomainService&lt;T&gt;
1866정성태2/20/201518594오류 유형: 270. "aspnet_regiis -i" 실행 시 0x00000006 오류 해결 방법
1865정성태2/20/201519881.NET Framework: 499. 특정 닷넷 프레임워크 버전 이후부터 제공되는 타입을 사용해야 한다면?
1864정성태2/18/201524864.NET Framework: 498. C#으로 간단하게 만들어 본 ASCII Art 프로그램 [2]파일 다운로드1
1862정성태2/18/201528704.NET Framework: 497. .NET Garbage Collection에 대한 정리 [6]
1861정성태2/18/201524055.NET Framework: 496. 마우스 커서가 놓인 지점의 문자열 얻는 방법 [1]파일 다운로드1
1860정성태2/18/201523878.NET Framework: 495. CorElementType의 요소 값 설명파일 다운로드1
1859정성태2/17/201524291Windows: 106. 컴퓨터를 재부팅하면 절전(Power Saver) 전원 모드로 돌아가는 경우
1858정성태2/16/201534266Windows: 105. 자동으로 로그아웃/잠김 화면 상태로 전환된다면? [2]
1857정성태2/16/201522279.NET Framework: 494. 값(struct) 형식의 제네릭(Generic) 타입이 박싱되는 경우의 메타데이터 토큰 값파일 다운로드1
1856정성태2/15/201521257.NET Framework: 493. TypeRef 메타테이블에 등록되는 타입의 조건파일 다운로드1
... 121  122  123  124  125  126  [127]  128  129  130  131  132  133  134  135  ...