원문 : Window Procedures as Class Member Functions
원문위치: http://www.rpi.edu/~pudeyo/articles/wndproc/index.html
원저자 : Oleg Pudeyev
원문 마지막 수정일: 2003년 1월 29일
번역자 : sobahoko
번역시작일 : 2006.3.11
번역 마지막 수정일 : 2006.3.18
클래스 멤버함수를 윈도우 프로시저로 사용하기
Oleg Pudeyev 저
이 문서에서, 나(Oleg Pudeyev)는 클래스 멤버함수를 윈도우 프로시저로 사용하는 몇가지 방법에 대해 살펴볼 것이다. 나는 특정 메시지를 특정 멤버함수에 멥핑하는 방법에 대해서는 언급하지 않을 것이다. 모든 예제 코드에서, 하나의 윈도우 프로시저가 모든 메시지를 처리할것이며, 기본적인 처리를 위해서는 DefWindowProc 함수를 호출할 것이다. 여기서 살펴볼 방법들은 쉬운것에서부터 시작하여 복잡한 것 순으로 나열된다. 이 문서의 목차는 다음과 같다.
기본 코드
정직한 방법
전역 변수
윈도우 고유 데이터
CBT Hooks
전역 헨들 멥
MFC 접근법
ATL 접근법
기본 코드
나는 이 글을 진행하기 위해 간단한 Win32 프로그램을 출발점으로 사용하려고 한다. 이 프로그램은 윈도우를 열고 “Hello, World!”를 WM_PAINT 메시지에 반응하여 출력하는 기능을 가지고 있다. 소스코드는 여기( http://www.rpi.edu/~pudeyo/articles/wndproc/basecode.cpp )에서 볼 수 있다.
정직한 방법
비록 제대로 동작하지 않지만, 윈도우 프로시저를 클래스 내부에 위치시키는 방법은 정직하게 다음처럼 하는 것이다.
class Window
{
LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp);
// ...
};
LRESULT CALLBACK Window::WndProc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp)
{
// Access member variables here
}
// ...
WNDCLASS wc = {
// ...
Window::WndProc,
// ...
};
// ...
이 코드의 전체 소스코드는 여기( http://www.rpi.edu/~pudeyo/articles/wndproc/obvious.cpp )에서 볼 수 있다. MSVC7에서 컴파일러는 컴파일에 실패하고 다음 에러 메시지를 낸다.
obvious.cpp(81) : error C2440: 'initializing' : cannot convert from
'LRESULT (__stdcall Window::* )(HWND,UINT,WPARAM,LPARAM)' to
'WNDPROC'
None of the functions with this name in scope match the target type
MSVC6 는 다음 에러 메시지를 낸다.
obvious.cpp(82) : error C2440: 'initializing' : cannot convert from
'long (__stdcall Window::*)(struct HWND__ *,unsigned int,unsigned int,long)' to
'long (__stdcall *)(struct HWND__ *,unsigned int,unsigned int,long)'
There is no context in which this conversion is possible
이 에러는 멤버함수가 전역함수와 다르기 때문에 발생한다. 멤버함수는 숨겨진 this 파라메터를 가지며, 멤버 함수가 호출되었을 때 그것을 통해서 클래스 인스턴스를 참조한다. 클래스 멤버가 아닌 함수들은 그런 파라메터가 없으므로, 멤버함수와 다르다.
정직한 방법을 간단히 수정
위 에러 메시지는 다음 코드와 같이 멤버함수 WndProc 을 static 으로 만들면 사라진다.
class Window
{
// ...
static LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp);
// ...
};
이것으로 Window::WndProc 함수로 WNDCLASS 구조체를 초기화 하는데 사용할 수 있다. 그러나, WndProc 함수가 static 이므로, 이 함수는 HelloString 변수와 같은 인스턴스 내부의 변수들에 접근할 수 없다. 그러므로 이것은 완전한 해결책이 아니다.
전역 변수
이것을 해결하는 간단한 방법은 메시지를 처리하는 클래스의 전역 인스턴스를 만들고, 메시지를 그 전역 인스턴스의 WndProc 함수로 전달해 주는 보조 함수를 만들어 사용하는 것이다. 그 코드는 아래와 같다.
Window w;
LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp)
{
return w.WndProc(hwnd, msg, wp, lp);
}
이 방법은 명확하며 신뢰할 만한 방법이다. 그리고 그 윈도우 클래스의 인스턴스가 오직 하나일 때 적합한 방법이다. 특별히 응용프로그램에 오직하나의 윈도우가 존재할 때, 이 방법은 좋은 선택이 될 것이다. 그러나 이 방법은 클래스의 인스턴스가 여러개이며 메시지가 각각의 인스턴스에서 다르게 처리되어야 하는 상황에 사용할 수 없다. 이것에 대한 소스코드는 여기 ( http://www.rpi.edu/~pudeyo/articles/wndproc/global.cpp ) 에서 볼 수 있다.
윈도우 고유 데이터
Windows 에 의해서 생성된는 모든 윈도우의 인스턴스에는 메모리블럭을 할당할 수 있다. 이 메모리블럭과 이 블럭에서 참조되는 변수들을 통칭하여 윈도우 고유 데이터(Per-Window Data)라 부른다. WNDCLASS 구조체의 cbWndExtra 멤버에 적절한 값을 주어, 이 데이터 영역의 크기를 지정할 수 있으며, 읽을때는 GetWindowLong 이나 GetWindowLongPtr를 사용하고 쓸때는 SetWindowLong 이나 SetWindowLongPtr를 사용한다. Ptr 가 붙은 버전은 64-bit Windows 와 호환되기 때문에 사용이 권장되며, 상위 호환성을 염두에 둔다면 Ptr 가 붙은 버전을 사용해야 한다.
우리는 윈도우 고유 데이터 영역에 객체의 포인터를 저장하여 특정 Window 객체와 윈도우를 연결시키려고 한다. 즉, 어떤 시점에는 연결을 위해서 다음 함수를 호출하고
SetWindowLong(hwnd, GWL_USERDATA, this);
그 후에는 Window 포인터를 가져오기 위해서 다음함수를 호출한다는 얘기다.
Window *w = (Window *) GetWindowLong(hwnd, GWL_USERDATA);
그러므로 전역 WndProc 함수는 다음과 같을 것이다.
LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp)
{
Window *w = (Window *) GetWindowLong(hwnd, GWL_USERDATA);
if (w)
return w->WndProc(hwnd, msg, wp, lp);
else
return DefWindowProc(hwnd, msg, wp, lp);
}
if 문은 연결이 완료되기 전에 WndProc 함수가 호출되었을 경우를 처리하기위해 필요하다. 연결은 언제 완료되는가? 즉 언제 SetWindowLong 을 호출하는가? 아마도 다음 코드와 같이 CreateWindow를 호출한 직후에 SetWindowLong 을 호출하고 싶은 생각이 들것이다.
Window w;
HWND hwnd = CreateWindow(TEXT("BaseWnd"), TEXT("Hello, World!"), WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, 0, CW_USEDEFAULT, 0, 0, 0, hinst, 0);
if (!hwnd)
return -1;
SetWindowLong(hwnd, GWL_USERDATA, &w);
불행히도, 이 방법은 WM_CREATE 메시지를 포함한 많은 메시지를 처리하지못한다. 이 메시지들은 CreateWindow 함수가 리턴되기 전에 발생하여, 그 결과로 WndProc 함수가 호출되기 때문이다.
그러므로 좀더 이른 시점에 연결을 완료하는 것이 필요하다.
WM_NCCREATE 가 희망을 준다. MSDN 에서는, 이것은 WM_CREATE 전에 보내지며, 우리가 this 포인터를 저장하는데 사용하려고 하는 CREATESTRUCT 의 포인터를 파라메터로 가진다고 말하고 있다. CreateWindow 함수는 lpParam 파라메터가 있다. 이것은 CREATESTRUCT 의 lpCreateParam 멤버와 같은 파라메터이다. Window 객체 w 의 포인터를 lpParam 에 전달함으로써, 우리는 나중에 lpCreateParam 을 통해서 그것을 다시 가져올 수 있다. 포인터를 가지고 있으므로, 우리는 WM_NCCREATE 헨들러에서 SetWindowLong 함수를 호출할 수 있다. WM_CREATE 메시지가 왔을때는, 객체의 포인터를 GetWindowLong 함수를 통해 가져오고, 이것을 통해 멤버함수 호출이 가능하다. 이것에 대한 소스코드는 여기( http://www.rpi.edu/~pudeyo/articles/wndproc/windowdata.cpp )에서 볼 수 있다.
이 접근법에는 한가지 단점이 있다. 그것은 WM_NCCREATE 메시지가 WndProc 함수가 받는 첫 번째 메시지가 아니라는 사실이다. 만약 GetWindowLong에서 반환된 값이 NULL 이 아님을 확인하지 않고 멤버 변수를 사용하면, this 포인터가 NULL 일 때, 프로그램이 다운될 것이다. 정확이 얘기하면 윈도우 프로시저는 WM_NCCREATE 전에 WM_GETMINMAXINFO 메시지를 먼저 받는다.
윈도우 고유 데이터 해결책의 최적화
static WndProc 함수가 항상 if 문장을 평가해야 한다는 것은 쉽게 알 수 있다. 그러나 this 포인터가 설정되고 나면, 어떤 코드가 실행되어야 하는지 우리는 정확히 한다. 두개의 WndProc 함수를 사용하면 코드는 미세하게 빨라진다. 예제 코드는 여기( http://www.rpi.edu/~pudeyo/articles/wndproc/windowdata2.cpp )에서 볼 수 있다.
전역 임시 this 포인터 저장소
이 방법은 전역 WndProc 함수가 받는 모든 메시지에 대해서 멤버 WndProc 함수가 호출되도록 하는 방법이다. 이 아이디어는 Magmai Kai Holmlor 의 것이다. ( GameDev.net thread 참조 : http://www.gamedev.net/community/forums/topic.asp?topic_id=59171 )
이 방법에서는 this 포인터를 임시 전역 변수에 저장한 후 전역 WndProc 함수가 처음으로 실행되었을 때 SetWindowLong 을 호출하는 방법이다. 이 방법이 위에서 언급한 전역변수 방법보다 나은 이유는 전역변수를 짧은 시간 동안만 사용하므로 많은 윈도우를 생성할수 있으며, 모든 윈도우들이 같은 전역변수를 사용하기 때문이다. 이것에 대한 소스코드는 여기( http://www.rpi.edu/~pudeyo/articles/wndproc/gwd.cpp )를 참조할것.
이 방법은 불행히도 쓰레드안정성을 가지고 있지 않다. 만약 두개의 쓰레드가 동시에 윈도우를 생성할 때 데이터가 훼손될 수 있으며 프로그램이 다운될 수 있다. 이것을 피하는 방법은 전역변수를 보호할 critical section 을 사용하는 것이다. 이것에 대한 소스코드는 여기 ( http://www.rpi.edu/~pudeyo/articles/wndproc/gwd2.cpp )를 참조할 것.
여전히 이 코드는 만족스럽지 않다. 다음 시나리오에서 두개의 쓰레드는 데드락상태가 될 수 있다.
• 쓰레드 A 가 윈도우를 생성한다.
• 그 윈도우의 WM_CREATE 헨들러에서, 쓰레드 A 가 쓰레드 B를 생성하고 쓰레드 B에 의해서 이벤트가 설정되기를 기다린다.
• 쓰레드 B 는 다른 윈도우를 생성하고, 이벤트를 설정한다. 그리고 뭔가 다른 일을 한다.
쓰레드 B 가 윈도우를 생성하려고 할 때, 이미 쓰레드 A 가 잡고 있는 critical section 에 진입하려고 한다. 이것은 데드락을 발생시킨다. 이 문제는 두가지 방법으로 해결할 수 있다. 두가지 방법 모두 궁극적으로 각 쓰레드가 전역 critical section 의 락을 잡고 있는 시간을 줄이는 방법을 사용한다.
1. 메시지 헨들러에서 첫 번째 메시지가 들어오면 critical section 을 떠난다. 이 방법은 기존 코드를 그리 많이 수정하지 않아도 된다. 그러나 윈도우 객체의 멤버에 플래그를 추가하는 것이다. 이것에 대한 소스코드는 여기( http://www.rpi.edu/~pudeyo/articles/wndproc/gwd2m.cpp )를 참조할 것. 이 플래그는 윈도우 생성이 성공했든, 실패했든지간에 critical section 이 오직 한번만 해제되었다는 것을 보증하기 위한 것이다. 이 방법은 복잡하며 유지관리가 편하지 않다.
2. 여기 ( http://www.rpi.edu/~pudeyo/articles/wndproc/gwd2c.cpp ) 에 구현된 방법은 보조 컨테이너를 사용하여 쓰레드 아이디와 윈도우 객체 포인터가 저장된 멥에 대한 접근을 동기화시킨다. 이 방법은 동기화 코드를 잘 정의된 범위에 집어 넣어 critical section lock 에 걸리는 시간을 최소화한다. 즉, 멥을 조회하고 멥에 삽입하는 짧은 시간 동안만 락을 건다. 이 방법에서 또 하나의 명확한 부작용은 thread-local storage를 전혀 사용하지 않는다는 것이다.
모든 활성 쓰레드에 대해 전역 포인터를 중복가능하게 하는 대안으로 다음코드와 같이 __declspec(thread) 수정자를 사용할 수 있다.
__declspec(thread) Window *g_pWindow;
불행히도 dll 에 저장된 데이터에 대해서는 __declspec(thread) 수정자를 사용할 수 없다. 이 방법은 오직 실행파일(exe) 에 존재하는 코드에 대해서만 적용가능하다.
CBT Hooks
CBT hooks 는 Window 객체의 포인터를 윈도우 고유 데이터에 저장할 수 있는 또다른 방법을 제시한다. CBT hook 는 윈도우 프로시저에 보내지는 모든 메시지보다 앞서 호출된다. 그러므로 hook에서 윈도우 객체 포인터를 윈도우 헨들에 연결시키는데 관한 모든 작업을 수행하는 것이 가능하다. 윈도우 프로시저는 포인터를 받기만 하면 된다.
윈도우가 생성되기 전, 다음과 같이 hook 을 설정한다.
HHOOK hHook = SetWindowsHookEx(WH_CBT, CBTProc, 0, GetCurrentThreadId());
이 작업은 윈도우가 생성될때마다 수행되는 것이 아니라, 쓰레드 실행중 단 한번만 수행되어야 한다. 그러나 코드를 단순하게 하기위해, 윈도우가 생성될때마다 수행하는 방식으로 예제를 작성하였다. CreateWindow 함수가 호출되면, CBTProc 이 호출된다. CBTProc 내부에서, this 포인터를 받아 전역변수, thread-local storage 또는 다른 기법을 사용한후, SetWindowLog 을 호출한다. 이방법을 보여주는 소스코드는 여기( http://www.rpi.edu/~pudeyo/articles/wndproc/hook.cpp )를 참조할것.
전역 헨들 멥
또 다른 방법은 모든 윈도우에 대해서 헨들-포인터 쌍을 저장하는 단 하나의 전역 멥을 사용하는 것이다. HWND 와 그에 대응하는 Window 포인터로 구성된 것이 Window 가 생성될때마다 멥에 추가되고, Window 객체가 소멸될 때 맵에서 삭제되는 방식이다. 그러면 클래스는 HWND 에 해당하는 Window 포인터를 돌려주는 방법을 제공할 수있다. 그런 예를 소스코드( http://www.rpi.edu/~pudeyo/articles/wndproc/gwd2c.cpp )의 CPtrMap에서 볼 수 있다. 구현은 매우 직관적이지만 모든 메시지가 처리될때마다 포인터를 조회하고, 그 외 필요한 부가작업을 수행하는 것은 잠재적으로 효율이 떨어진다. 응용프로그램에서 많은 수의 윈도우를 열고 있을 때 vector 나 list를 사용하면 선형시간( O(N) )이 소요되고, map 을 사용하면 로그시간( O(log N) )이 소요된다. hash map 은 상수시간 ( O(1) ) 조회를 수행할 수 있지만, 다른 단점를 가지고 있다. MFC에서 하는것처럼 각 쓰레드 마다 분리된 멥을 사용하면 조회시간을 최적화하는 것이 가능하다. 사용자 레벨에서 헨들을 포인터로 변환( MFC 의 FromHandle 과 FromHandlePermanent 함수 )하는 용도의 전역 헨들 맵 방법은 윈도우 관리 코드에 의해 내부적으로 헨들을 포인터에 맵핑하는 다른 해결책과 동시에 사용하는 것이 가능하다. 또한 사용자에게 그런 변환기능을 제공하는 것이 반드시 필요한 것은 아니다. 사실 ATL 에는 그런 변환기능이 없지만 아무 문제가 없다. 마지막으로 이 방법 하나만으로는 실제로 동작하지 않으며 다른 방법들도 마찬가지다. 누군가 헨들과 포인터를 연결시켜야하고, 거기에는 헨들과 거기에 대응하는 포인터가 필요하다. 그러므로, 임시 윈도우 프로시저, hook 또는 다른 방법이 앞서 언급된 연결을 확립하는데 반드시 필요하다. 간단한 예를 들어, CreateWindow 함수를 호출한 다음 맵을 생성할 수도 있다. CreateWindow 가 실행되는 동안 발생되는 메시지를 처리할 필요가 없다면, 이 방법도 사용할 수 있는 해결책이 될것이다.
MFC 접근법
MFC 가 윈도우 헨들을 객체에 연결시키는 방법을 알아보기위해, 나는 다음 MFC 코드를 사용했다.
(http://www.rpi.edu/~pudeyo/articles/wndproc/mfc.cpp)
UI 가 그리 멋진 것은 아니지만, 이 프로그램은 나의 목표를 달성하기에 충분하다. MFC dll 내부를 탐험하기위해 디버거에서 실행시킨다.
첫 번째 눈에 들어오는 것은 wincore.cpp 파일에 있는 AfxHookWindowCreate 함수이다. 이 함수는 윈도우가 CWnd::CreateEx 함수에의해서 생성될 때 함수내부에서 호출되어, 윈도우 객체를 MFC 로 hook 한다. MFC 는 CBT hook 을 사용하여 현재 쓰레드의 윈도우 생성을 hook 하는 것을 볼 수있다. CBT hook 은 위에서 언급했기 때문에 낯설지 않을 것이다. MFC 는 hook handle 을 저장하기 위해 thread state block 을 사용한다. thread state block 은 thread 가 실행되는동안 hook 을 관리하기 때문이다. MFC 가 어떻게 데이터를 전달하는지에 주목하기 바란다. 모든 thread 는 per-thread state structure를 가지고 있다. ( _AFX_THREAD_STATE, afxstat.h ) 여기에 모든 thread-specific MFC data를 저장하는데, 현재 생성중인 윈도우에 대한 정보도 저장된다. thread state structure 는 포인터를 저장하는 동적 배열에 저장된다. 그 배열에 대한 포인터는 TlsXXX 계열의 함수를 통해 저장되고 참조된다. AFX thread-local storage 에 대한 구현상세는 afxtls_.h, afxtls.cpp에서 볼수 있다. hook 와 data exchage 의 얘기로 다시돌아와서, CreateEx 함수는 CWnd 객체의 포인터를 thread state 에 저장하고, _AfxCbtFilterHook 함수는 그것을 thread state 로부터 가져온다.
hook 는 이제 thread state block으로부터 가져온 CWnd 포인터와 그것에 대응하는 HWND를 가지게 된다. CWnd::Attach 함수를 호출하여 CWnd를 HWND 에 연결하고, HWND 와 CWnd 쌍을 per-thread handle map 에 저장한다. ( per-thread 라는 단어에 주목하기 바란다. 이것이 multi-thread 환경에서 CWnd 와 그밖의 window resource wrapper 객체를 공유하지 못하는 이유이다. 특정 thread 의 CWnd 는 다른 thread 의 HWND-CWnd map 에 존재하지 않는다. 만약 CWnd 객체를 생성한 thread 가 아닌 다른 thread에서 CWnd 포인터를 통해 HWND를 lookup 한다면, 그 코드는 실패할 것이다. 그러나 HWND 와 다른 헨들은 thread 간에 전달 할수있으며, CWnd::Attach 함수를 호출하여 CWnd를 HWND 에 연결시킬 수 있다. 그러나 thread 가 다르므로, CWnd 객체는 다른 것이 될것이다. ) 그런다음, MFC 는 윈도우를 subclass 하고 thread 에 고유한 현재 생성중인 윈도우를 가리키는 포인터를 null 로 설정한다. CWnd 는 이제 메시지를 처리할 수 있는 상태가 된다.
윈도우가 메시지를 받으면, 그 메시지는 AfxWndProcBase 함수를 거쳐, AfxWndProc 함수로 보내진다. AfxWndProc 함수는 window handle map 으로부터, 전달받은 HWND로 조회하여 CWnd를 얻기위해 CWnd::FromHandlePermanent 함수를 사용한다. 이 map 은 thread-specific 하다. 그러므로 윈도우를 생성한 thread 만이 그 윈도우에 보내진 메시지를 처리할 수 있다. 그후 HWND 헨들을 받는 AfxWndProc 함수는 AfxCallWndProc 함수를 호출하는데, 이함수는 HWND 헨들대신 CWnd 포인터를 받는다는 점만 다르다. 그후 AfxCallWndProc 함수는 CWnd::WindowProc 함수에게 메시지를 보낸다. 그것을 받은 CWnd::WindowProc 함수는 CWnd::OnWndMsg 함수를 호출한다. CWnd::OnWndMsg 함수는 지정된 윈도우의 메시지맵에서 헨들러를 찾고, 메시지 파라메터를 decode 하고, 헨들러를 호출하여, 사용자가 작성한 OnPaint 함수에 이르게 된다.
ATL 접근법
나는 ATL 메시지 처리 구조를 조사하기위해 다음코드를 사용했다. ( http://www.rpi.edu/~pudeyo/articles/wndproc/atl.cpp ) 디버거에서 실행하고 CWindowImpl::Create ( atlwin.h ) 함수안으로 들어간다. 잠시후 AtlWinModuleAddCreateWndData (atlbase.h) 함수에 이르게 되는데, 이 함수는 _AtlCreateWndData 구조체 포인터를 리스트에 저장한다. 이 구조체 리스트에는 윈도우 객체 포인터들이 저장되는데, WM_NCCREATE 메시지가 도달하기 전에 ATL 의 window procedure 들이 접근하여 사용할 수 있다. 모든 구조체는 thread identifier 와 거기에 속한 윈도우 객체 포인터를 가지고 있다. MFC 에서는 실제로 윈도우를 생성중이건 아니건간에, 모든 thread 가 현재 생성중인 윈도우에 대한 참조를 가지고 있지만, ATL 은 전체 application 에 대하여 단 하나의 리스트가 그것을 관리한다. 이런 방식은 하나의 UI thread 에 많은 worker thread 가 존재할 때 약간의 메모리를 절약 할 수 있다.
이제 static 멤버함수 CWindowImplBaseT::StartWndProc (atlwin.h) 에 이른다. 이것은 현재 윈도우에 보내진 첫 번째 메시지를 처리하는 메시지 헨들러 이다. 여기서 CWindow 와 HWND 의 쌍을 연결하는 작업을 하여, 현재 메시지와 앞으로 올 메시지들이 멤버 윈도우 프로시저로 route 되도록 한다. 지체 없이 create-window-data structure 리스트로부터 AtlWinModuleExtractCreateWndData (atlbase.h) 함수를 사용하여 this 포인터를 가져온다. 이 함수는 current thread identifier 로 리스트를 조회한다. thread identifier 가 일치하면 거기에 대응하는 window object pointer 가 리턴된다. 다른 ATL 코드나 사용자 코드가 실행되기 전에 가져오기 때문에 특정 thread 에는 오직 하나의 window object pointer 가 연결된다. 이로써 CreateWindow 함수는 첫 번째 받은 메시지로부터 호출되게 된다. ATL 은 여러개의 thread 가 동시에 리스트에 접근하는 것을 방지하기 위해 critical section 을 사용한다. window 생성 과정전체 동안이 아니라, create window data를 추가하고 가져오는 동안에만 lock 을 하기 때문에 다른 thread 들이 필요없이 기다리지 않는다.
이제 CWindowImplBaseT::StartWindowProc 함수는 this 포인터를 가지게 되었다. CWindowImplBaseT::GetWindowProc 함수를 사용하여 실제 윈도우 프로시저를 가져온다. 그다음 thunk를 생성하는데, 이것의 목적은 HWND 헨들을 CWindow 포인터로 변환하는 것이다. 그것은 다음과 같이 이루어진다.
• StartWindowProc 함수는 window object 의 instance data 의 일부인 특정 메모리를 특정 코드로 초기화한다. 그 코드는 임시 윈도우 프로시저처럼 동작하는 코드로, 들어오는 HWND를 CWindow 포인터로 멥핑한다. 그런 다음 윈도우 프로시저는 ( SetWindowLong 함수를 통해서 ) 그 메모리 블록의 시작지점을 가리키도록 설정된다.
• 그 특정 메모리는 다음과 같이 평범한 프로토타입의 윈도우 프로시저를 저장하는 어떤 것이다.
LRESULT CALLBACK WindowProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam);
• 윈도우 메시지가 그 프로시저에 도달했을 때, 그 코드는 hwnd parameter를 CWindow object 의 포인터로 변환한다. CWindow object 의 주소는 같은 메모리블럭에 저장된다.
• 그다음 메시지 프로시저는 실제 static member procedure 로 jump 한다. 그것은 WindowProc 함수와 동일한 프로토타입을가지고 있지만 hwnd parameter 가 실제 윈도우 헨들이 아니라, CWindow pointer 이다. 그함수는 포인터를 얻기위해 cast를 수행한다.
CWindowImplBaseT< TBase, TWinTraits >* pThis = (CWindowImplBaseT< TBase, TWinTraits >*)hWnd;
다음은 atlbase.h 에 있는 _stdcallthunk 의 thunk code 이다.
mov dword ptr[esp+4], pThis
jmp WndProc
pThis 가 Init 에 파라메터로 전달된다. 이것은 현재윈도우의 CWindow 객체에 대한 포인터이다. WndProc 또한 파라메터로 전달된다. 이것은 실제 WndProc 의 주소이며, 대부분의 경우 이것은 CWindowImplBaseT::WindowProc (atlwin.h) 가 된다. Init 은 위 코드를 실행시에 주어진 두가지 정보를 가지고 build 한다. 이 코드는 다음과 같은 이유로 CWindow 객체의 일부인 메모리블럭에 저장된다.
1. Code segment 는 읽기전용이며, 메모리에 있는 코드를 수정하는 것은 바이러스처럼 보인다.
2. 윈도우가 다르면 윈도우 프로시저도 다르다. 그리고 윈도우 프로시저가 가진 CWindow 포인터는 모두 다르다. 그러므로 하나의 코드가 종류가 다른 윈도우를 처리하는데 사용될 수는 없고, 코드는 실행시에 생성되거나 복제되어야 한다.
위 코드가 하는 일은 hwnd parameter를 thunk 가 속한 객체의 포인터를 사용하여 window procedure 로 교체하는 일이다. (CWindowImplRoot, atlwin.h) 그리고 static 멤버함수 WndProc 으로 jump 하는데, 이 함수의 첫 번째 파레메터는 HWND를 가장한 CWindow 포인터이다. 물론 컴파일러는 에러를 발생시키지 않는다. ATL 의 구현은 멤버 윈도우 프로시저를 가져오는 부분에 있어서는 MFC 에 비해 매우 효율적이다. MFC 코드는 윈도우 객체를 조회하는데 윈도우의 개수에 비례하는 선형시간이 소요되는 반면, ATL 은 상수시간이 소요된다. ATL 코드는 하나의 move, 하나의 jump를 사용하여, 최대한 효율적이다.
MFC 와 ATL 비교
위에서 언급했듯이, ATL 과 MFC 의 구현에는 다음과 같은 차이점이 있다.
• ATL 은 thread-local storage를 사용하지 않는다. 그러므로 어떠한 초기화도 사용하지 않는다. MFC를 사용하면 AfxWinInit 으로 초기화를 반드시 해야한다. ATL 은 사용자 코드를 직접적으로 수행한다. 게다가 TLS 에 접근하는 것은 동적 배열을 사용하므로, 상각된 상수시간( amotized constant time ) 이 소요되는데, 이것은 MFC 의 실행 속도를 감소시킬 수 있다.
• ATL 의 맵핑은 thread-specific 하지 않다. 프로그래머는 CWindow 객체를 만든 thread에서 뿐만 아니라, 모든 thread에서 사용할 수 있다. MFC 는 CWnd 와 같은 객체를 그것을 생성한 thread 에 bind 한다. MFC 는 포인터를 CWnd* 형으로 전달하는반면, ATL 은 함수 파라메터로 HWND 헨들을 전달한다.
• MFC 의 WndProc 은 간접호출이 많아 오버헤드를 좀더 가진다. ATL 이 조금더 빠르다.
• MFC 는 주어진 HWND 로 CWnd 포인터를 조회하는데 상수시간이 소요된다. 응용프로그램에 윈도우가 다수존재할 경우 이것은 그리 빠른성능을 보이지 않는다. MFC 의 WndProc 은 윈도우의 개수가 많아 질수록 속도가 느려지는 반면, ATL 은 윈도우의 개수에 상관없이 동일한 고성능을 보인다.
'MFC' 카테고리의 다른 글
AfxEndDeferRegisterClass, AfxWndProc - MFC 살펴보기 (0) | 2008.09.01 |
---|---|
클래스 멤버 함수를 윈도우 프로시져로 등록하기 [3/3] (0) | 2008.09.01 |
클래스 맴버함수를 콜백함수로 사용하기 [2/3] (1) | 2008.09.01 |
MFC 메인 윈도우의 생성 흐름 (1) | 2008.09.01 |
MFC 팁들 (0) | 2008.09.01 |
Creating New Documents, Windows, and Views (0) | 2008.09.01 |
CWnd::Create 과정.... (1) | 2008.09.01 |