티스토리 뷰
잘 정리했습니다~ MFC 에 대해서 쓰긴 했지만, ATL도 거의 비슷한 구조를 가지고 있습니다.
ATL은 호출 속도를 빠르게 하려고 꽁수(!)을 부리긴 했네요~
출처 : http://cafe.naver.com/pplus.cafe?iframe_url=/ArticleRead.nhn%3Farticleid=168
MFC에서 윈도우에 관련된 클래스는 CWnd이다. CWnd는 윈도우에 관련된 수백개의
함수를 갖고 있다. MFC로 윈도우를 생성하기 위해서는 반드시 CWnd 클래스를 상속받게
되는데, 이 때 CWnd 클래스의 모든 멤버 함수가 virtual 이라면, 그에 따른 함수테이블이
생성되기 때문에 적지 않은 메모리 낭비가 발생한다. MFC에서는 이런 메모리 낭비를 막기
위해 virtual을 사용하지 않고 모든 함수를 일반 함수로 사용하고 있다. 그리고 CWnd에 전달된
메시지는 자식 클래스로부터 메시지를 처리할 수 있는지 스스로 검색한다.
우선 메모리의 낭비가 심한 경우의 예를 들어보자. CWnd 클래스에 virtual 가상함수가 존재할 때,
아래의 CView 클래스는 가상 함수 테이블을 위해 800바이트정도의 메모리를 더 할당해야
한다. 이 정도 되면 머 그리 아까운 메모리가 아니라는 생각이 들지만, 하나의 MFC 프로그램에는
수없이 많은 윈도우가 사용되고, 그에 따라 수많은 CWnd 클래스가 사용된다.
class CWnd
{
public:
virtual void
OnMove( ... ); // 가상 함수 테이블을 위해 800여 바이트의 메모리를
낭비
// 기타 200여개의 virtual 함수들...
};
class CView : public
CWnd
{
public:
virtual void OnMove( ... );
};
위의 클래스는 메모리의 낭비를 초래할 수 있기 때문에, 다음과 같이 virtual을 사용하지 않는
방법을 MFC는 선택했다.
class CWnd
{
public:
void OnMove( ... ); // 가상 함수 테이블이 생성되지 않음
// 기타 200여개의 함수들...
};
class CView : public CWnd
{
public:
void OnMove( ... );
};
class CTestView : public CView
{
public:
void OnMove( ... );
};
위와 같이 virtual을 사용하지 않음으로 인해, 메모리의 낭비를 막을 수는 있지만, 이럴 경우
CWnd 클래스에 WM_MOVE 메시지가 전달될 때, 이를 다시 CView 클래스에 보낼 수가
없다. 아래 예를 보자.
case WM_MOVE :
CWnd* pWnd;
pWnd->OnMove( ... );
break;
MFC의 내부에서는 WM_MOVE 메시지를 받으면 위와 같이 처리를 할 것이다. 하지만 우리가
코딩을 해야 할 곳이 CTestView 클래스라면, CTestView 클래스는 절대로 호출될 수 없다.
CTestView가 호출될 수 없는 이유는 무엇인가? 그 이유는 MFC 내부 코드는 단순하게 위와
같이 코딩될 수 밖에 없기 때문이다. 만약 OnMove() 함수가 virtual이었다면, 당연히 CTestView
클래스의 OnMove() 함수가 호출될 것이다.
가상 함수(virtual)를 사용하지 않는 대신에 위와 같이 함수 재정의를 할 수 없다면, 이것은 우리가
원하는 결과가 아니다. 그래서 MFC는 이를 해결하기 위해 메시지맵이라는 것을 사용한다.
MFC 코딩을 하다보면 BEGIN_MESSAGE_MAP과 END_MESSAGE_MAP이 있는데, 이
매크로가 가상 함수를 대신해서 CWnd 클래스에 전달된 메시지를 자식 클래스에서 먼저 처리할
수 있도록 해준다. 그럼 어떤 원리로 이것이 가능한지 분석해보자..
우선 CTestView 클래스에 보면 다음과 같은 매크로가 선언되어 있을 것이다.
DECLARE_MESSAGE_MAP()
이 매크로는 아래와 같고, 아래 코드는 메시지맵에 사용될 함수 선언 및 메시지 구조체 배열들이다.
private:
static
const AFX_MSGMAP_ENTRY _messageEntries[];
protected:
static AFX_DATA
const AFX_MSGMAP messageMap;
static const AFX_MSGMAP* PASCAL
_GetBaseMessageMap();
virtual const AFX_MSGMAP*
GetMessageMap() const;
아래의 내용는 그리 중요하지 않기 때문에 그냥 참고만 하자.
참고 시작
위 코드에서 AFX_MSGMAP_ENTRY는 아래와 같이 구성되어 있다.
struct AFX_MSGMAP_ENTRY
{
UINT
nMessage; // 윈도우 메시지
UINT nCode;
// 제어 코드 또는 WM_NOTIFY 코드
UINT
nID; // control ID (또는 윈도우 메시지는 0)
UINT nLastID; // control id의 범위를 정의하기 위한 엔트리로
사용
UINT nSig; // 액션, 메시지 타입 또는 메시지
번호의 포인터
AFX_PMSG pfn; // 호출 루틴 또는 특별한
값
};
위 코드에서 AFX_DATA 는 아래와 같다.
#define AFX_DATA __declspec(dllimport)
위 코드에서 AFX_MSGMAP는 아래와 같이 구성되어 있다.
struct AFX_MSGMAP
{
const AFX_MSGMAP* (PASCAL* pfnGetBaseMap)();
const
AFX_MSGMAP_ENTRY* lpEntries;
};
참고 끝
BEGIN_MESSAGE_MAP과 END_MESSAGE_MAP 매크로는 다음과 같이 선언되어 있다.
#define BEGIN_MESSAGE_MAP(theClass,
baseClass) \
const AFX_MSGMAP* PASCAL theClass::_GetBaseMessageMap() \
{ return &baseClass::messageMap; } \
const AFX_MSGMAP*
theClass::GetMessageMap() const \
{ return &theClass::messageMap; } \
AFX_COMDAT AFX_DATADEF
const AFX_MSGMAP theClass::messageMap = \
{ &theClass::_GetBaseMessageMap, &theClass::_messageEntries[0] }; \
AFX_COMDAT const AFX_MSGMAP_ENTRY theClass::_messageEntries[] =
\
{
#define END_MESSAGE_MAP()
\
{0, 0, 0, 0, AfxSig_end, (AFX_PMSG)0 } \
};
CTestView에서 WM_MOVE를 재정의했다면 아래와 같이 ON_WM_MOVE 매크로가 추가된다.
BEGIN_MESSAGE_MAP(CTestView,
CView)
//{{AFX_MSG_MAP(CCaptureView)
ON_WM_MOVE()
//}}AFX_MSG_MAP
// Standard printing
commands
END_MESSAGE_MAP()
위 모든 매크로를 실제 클래스를 대입시켜 전개하면 아래와 같다.
//
BEGIN_MESSAGE_MAP(CTestView, CView)
const
AFX_MSGMAP* PASCAL CTestView::_GetBaseMessageMap()
{
return &CView::messageMap; // 부모 클래스(CView)의 메시지 맵 구조체
}
const AFX_MSGMAP*
CTestView::GetMessageMap() const
{
return &CTestView::messageMap; // CTestView 클래스의 메시지 맵 구조체
}
AFX_COMDAT AFX_DATADEF const AFX_MSGMAP
CTestView::messageMap =
{
&CTestView::_GetBaseMessageMap,
&CTestView::_messageEntries[0]
};
AFX_COMDAT const AFX_MSGMAP_ENTRY
CTestView::_messageEntries[] =
{
//#define ON_WM_MOVE()
{ WM_MOVE, 0, 0,
0, AfxSig_vvii,
(AFX_PMSG)(AFX_PMSGW)(void
(AFX_MSG_CALL CWnd::*)(int, int))&OnMove
},
// #define
END_MESSAGE_MAP()
{0, 0, 0, 0, AfxSig_end, (AFX_PMSG)0
}
};
위의 코드가 조금은 낯설어 보일 것이다. 이해만 한다는 개념을 갖고 하나씩 살펴보면,
_GetBaseMessageMap() 함수는 부모 클래스(CView)의 메시지 맵을 구하는 함수이고,
GetMessageMap() 함수는 CTestView의 messageMap 구조체를 구하는 함수이다.
이 모든 것들은 현재 CTestView 클래스에 WM_MOVE 메시지가 발생했을 때,
MFC가 내부적으로 호출해서 사용하는 함수 및 구조체들이다.
WM_MOVE 메시지가 발생했다면, MFC는 내부적으로 CTestView 클래스의 메시지 맵에
WM_MOVE가 있는지만 검사하게 되며, 만약 없다면 부모 클래스인 CView 클래스를 같은
방법으로 검사하게 된다. 현재 WM_MOVE 메시지가 _messageEntries에 존재하기 때문에
MFC는 내부적으로 그 메시지와 함께 있는 OnMove 함수를 호출하게 된다. 그래서 WM_MOVE
메시지가 발생되면 아래와 같이 OnMove() 함수가 호출된다.
저 아래의 코드를 보면 _AfxDispatchCmdMsg() 함수가 결과적으로 OnMove() 함수를 호출해
준다. 함수의 매개 변수 중에 lpEntry->pfn에 의해 OnMove() 함수가 전달된다.
void CTestView::OnMove(int x, int y)
{
CView::OnMove(x, y);
// TODO: Add your message
handler code here
}
마지막으로 MFC는 내부에 어떤 코드가 있어서 위의 함수들을 호출하고 있는지 살펴 보자.
아래의 코드는 CCmdTarget 클래스에서 볼 수 있다.
for
(pMessageMap = GetMessageMap(); pMessageMap != NULL;
// 첫 번째 메시지 맵부터
pMessageMap = (*pMessageMap->pfnGetBaseMap)()) // 부모의 메시지 맵까지 순환
{
ASSERT(pMessageMap != (*pMessageMap->pfnGetBaseMap)());
lpEntry =
AfxFindMessageEntry(pMessageMap->lpEntries, nMsg, nCode,
nID);
if (lpEntry !=
NULL)
{
return
_AfxDispatchCmdMsg(this, nID,
nCode, // OnMove() 함수
호출
lpEntry->pfn, pExtra, lpEntry->nSig, pHandlerInfo);
}
}
AfxFindMessageEntry 함수는 해당 클래스의 메시지맵에 WM_MOVE 등의 메시지가 있는지
검사하게 된다. 이 함수는 매우 빈번하게 호출되므로, 속도를 위해 함수 내부가 Assembly 코드로 되어 있다.
const
AFX_MSGMAP_ENTRY* AFXAPI
AfxFindMessageEntry(const
AFX_MSGMAP_ENTRY*
lpEntry,
UINT nMsg, UINT nCode, UINT
nID)
{
ASSERT(offsetof(AFX_MSGMAP_ENTRY,
nMessage) == 0);
ASSERT(offsetof(AFX_MSGMAP_ENTRY, nCode) ==
4);
ASSERT(offsetof(AFX_MSGMAP_ENTRY, nID) ==
8);
ASSERT(offsetof(AFX_MSGMAP_ENTRY, nLastID)
== 12);
ASSERT(offsetof(AFX_MSGMAP_ENTRY, nSig) ==
16);
_asm
{
MOV
EBX,lpEntry
MOV
EAX,nMsg
MOV
EDX,nCode
MOV
ECX,nID
__loop:
JZ __failed
CMP EAX,DWORD PTR [EBX] ; nMessage WM_MOVE 메시지 등인지 비교
JE __found_message ; 찾았으면 goto __founc_message
__next:
ADD EBX,SIZE
AFX_MSGMAP_ENTRY
JMP short
__loop
__found_message:
CMP EDX,DWORD PTR [EBX+4] ;
nCode
JNE __next ; nCode가 틀리면 goto __next
// 메시지와 코드 확인
// ID 확인
CMP ECX,DWORD PTR [EBX+8] ;
nID
JB __next
CMP ECX,DWORD PTR
[EBX+12] ;
nLastID
JA __next
// found a match
MOV lpEntry,EBX ; return
EBX
JMP short
__end
__failed:
XOR EAX,EAX ;
return NULL
MOV
lpEntry,EAX
__end:
}
return lpEntry; // 메시지 구조체 배열의 시작을 리턴
}