MFC

MFC 메시지 맵과 메시지 핸들링 원리

디버그정 2008. 8. 30. 20:53

(아래 발췌내용중 잘못된 부분)


CWnd 클래스의 경우 Notify나 Command를 제외한 윈도우 메시지 처리함수만이 일반멤버함수로 정의되어 있었다. OnCommand나 OnNotify 등의 경우는 가상함수이다. 이런 경우는 인스턴스의 가상함수를 호출할 것이다.

class CWnd : public CCmdTarget
virtual BOOL Create( // 가상함수
   LPCTSTR lpszClassName,
   LPCTSTR lpszWindowName,
   DWORD dwStyle,
   Const RECT& rect,
   CWnd* pParentWnd,
   UINT nID,
   CCreateContext* pContext = NULL
);
afx_msg void OnKeyDown( // 일반 함수
UINT nChar, UINT nRepCnt, UINT nFlags );


 

/////////////////////// 발 췌 내 용 ////////////////////////////////////


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_MAPEND_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() 함수는 CTestViewmessageMap 구조체를 구하는 함수이다.


이 모든 것들은 현재 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)())         // 부모의 메시지 맵까지 순환

{

    // Note: catches BEGIN_MESSAGE_MAP(CMyClass, CMyClass)!

    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:

         CMP     DWORD PTR [EBX+16],0        ; nSig (0이면 메시지맵의 끝)

         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;        // 메시지 구조체 배열의 시작을 리턴

}