COM coding시 알아둘점

시간날때 아래는 읽어보도록 하고
https://docs.microsoft.com/en-us/windows/win32/learnwin32/com-coding-practices

__uuidof 연산자

다음과 같은 에러의 의미는
unresolved external symbol "struct _GUID const IID_IDrawable"



GUID 상수가 외부에서 선언되었고, 링커가 해당 상수의 정의를 찾을 수 없음을 의미.
GUID 상수는 대개 정적라이브러리파일에서 노출되는데.
__uuidof 연산자를 사용하면 해당 정적라이브러리파일을 링크하지 않아도 된다(물론 MS-VC++에서만)
이 연산자는 MS 언어 확장이기 때문이다.

이 연산자는 표현식에서 GUID 상수를 리턴해준다. 표현식은 인터페이스 형식 이름, 클래스 이름, 인터페이스 포인터일 수 있다.


CoCreateInstance의 기본 패턴은
CoCreateInstance(개체의 클래스 식별자, ..., 검색할 인터페이스, [OUT] 인터페이스 포인터)

IDirectPlay8Peer* g_pDP=NULL;
CoCreateInstance( CLSID_DirectPlay8, NULL, CLSCTX_INPROC_SERVER,
                        IID_IDirectPlay8Peer, (LPVOID *)&g_pDP );

이를 __uuidof 연산자를 이용하면
IDirectPlay8Peer* g_pDP=NULL;
CoCreateInstance( __uuidof(DirectPlay8Peer), NULL, CLSCTX_INPROC_SERVER,
                         __uuidof(IDirectPlay8Peer), (LPVOID *)&g_pDP );
__uuidof(IDirectPlay8Peer) 대신에 __uuidof(g_pDP)를 사용해도 된다

GUID 값은 형식 이름과 연관되는데, 헤더파일에서 __declspec(uuid(...)) 이런식으로 선언했기 때문이다.

분리된 상속 체인에서의 인터페이스를 요청하려면, 기존 인터페이스를 사용해 개체에게 원하는 인터페이스를 요청해야 한다.

QueryInterface의 기본 패턴은
HRESULT QueryInterface(REFIID riid, void **ppvObject)

riid는 당신이 요청하는 인터페이스 식별자를 의미하는 GUID
데이터 유형 REFIIDconst GUID&에 대한 typedef임.
여기서 클래스 식별자인 CLSID는 필요치 않다. 왜냐하면 개체가 이미 만들어졌기 때문.
그래서 인터페이스 식별자만 필요한 거다.

IFileDialogCustomize* pCustom;
hr = pFileOpen->QueryInterface( IID_IFileDialogCustomize,
                                          reinterpret_cast<void**>(&pCustom) );

이를 IID_PPV_ARGS 매크로 이용하면
hr = pFileOpen->QueryInterface( IID_PPV_ARGV(&pCustom) );

IID_PPV_ARGS 매크로

IFileOpenDialog * pFileOpen;

hr = CoCreateInstance( __uuidof(FileOpenDialog), NULL, CLSCTX_ALL,
                              __uuidof(IFileDialogCustomize),
                              reinterpret_cast<void **>(&pFileOpen) );

IFileDialogCustomize 인터페이스를 요구하나 IFileOpenDialog 포인터를 건네고 있음을 주목해라. reinterpret_cast 연산자는 C++ 유형 시스템을 회피하므로, 컴파일러는 에러를 인지하지 못한다. 해당 상황에서의 최악의 경우는 함수호출이 성공하고 잘못된 포인터를 갖는거다.

포인터 유형이 메모리의 실제 vtable과 일치하지 않는거다.
이를 IID_PPV_ARGS 매크로를 이용하면

IFileOpenDialog * pFileOpen;
hr = CoCreateInstance( __uuidof(FileOpenDialog), NULL, CLSCTX_ALL,
                              IID_PPV_ARGS(&pFileOpen) );

이 매크로는 자동으로 인터페이스 식별자 __uuidof(IFileOpenDialog)를 삽입해주고, 포인터 유형이 일치하는것을 보장해 줍니다.

또한 QueryInterface에서도 사용할 수 있습니다.

IFileDialogCustomize * pCustom;
hr = pFileOpen->QueryInterface(IID_PPV_ARGS(&pCustom));


SafeRelease 패턴

참조카운팅은 프로그래밍에서 기본적으로 쉬우나 또한 지루한 것중에 하나이다. 따라서 잘못되기 쉽다. 기본적인 오류는 다음과 같습니다.

1. 인터페이스 포인터를 사용해 마쳤을때 인터페이스 포인터 해제에 실패합니다. 이러한 유형의 버그는 프로그램이 오브젝트가 소멸되지 않았기 때문에 릭을 유발합니다.
2. 잘못된 포인터를 갖고서 Release 호출하기. 이러한 오류는 오브젝트가 전혀 만들어지지 않은 경우에 발생할 수 있습니다.
3. Release 호출이후에 인터페이스 포인터를 역참조하기.

template <class T> void SafeRelease(T ** ppT)
{
   if (*ppT)
   {
      (*ppT)->Release();
      *ppT = NULL;
   }
}

IFileOpenDialog * pFileOpen;
hr = CoCreateInstance( __uuidof(FileOpenDialog), NULL, CLSCTX_ALL,
                              IID_PPV_ARGS(&pFileOpen) );
if (SUCCEEDED(hr))
{
  // 오브젝트를 사용하는 코드
}

CoCreateInstance가 성공하든 실패하든 상관없이 다음처럼 호출해 주면 된다
SafeRelease(&pFileOpen);


COM Smart Pointers

SafeRelease 함수는 유용한데, 2가지를 기억하는것을 필요로 한다.
1. 모든 인터페이스 포인터를 NULL로 초기화해준다
2. 각각의 포인터가 스코프 범위밖으로 나가기전에 SafeRelease를 호출해준다.

이러한 부분을 쉽게 하기 위해 아래와 같은 논리적 스마트 포인터를 구현해줄필요가 있다.

template <class T>
class SmartPointer
{
   T* ptr;


 pubilc:
      SmartPointer(T * p) : ptr(p) { }
      ~SmartPointer()
      {
          if (ptr) { ptr->Release(); }
      }
};

ATL에서는 이를 위해 CComPtr이 있다.

if (SUCCEEDED(hr))
{
   CComPtr<IFileOpenDialog> pFileOpen;


   hr = pFileOpen.CoCreateInstance( __uuidof(FileOpenDialog) );

   if (SUCCEEDED(hr))
   {
      hr = pFileOpen->Show(NULL); // 오픈대화상자를 보여줌

      if (SUCCEEDED(hr))
      {
          CComPtr<IShellItem> pItem;
          hr = pFileOpen->GetResult(&pItem);
        
          if (SUCCEEDED(hr))
          {
              
          }

          여기가 지나면 pItem 스코프 밖이다. 딱히 해줄게 없다. 스마트 포인터 소멸자
      }
   }

   여기가 지나면 pFileOpen 스코프 밖이다.
}

CComPtr은 클래스 템플릿이다. 템플릿 아큐먼트는 COM 인터페이스 유형이다.
내부적으로 그 유형의 포인터를 들고 있다.

CComPtr은 CComPtr::CoCreateInstance메서드를 정의한다. 기본 파라미터가 가정된다.
필요한 파라미터는 클래스 식별자이다. 다음처럼

hr = pFileOpen.CoCreateInstance( __uuidof(FileOpenDialog) ) ; 클래스 식별자

또 시간이 나면 Error Handling in COM 부분도 읽어봐라
https://docs.microsoft.com/en-us/windows/win32/learnwin32/error-handling-in-com


COM 에러 코드

성공코드 : 0x0 - 0x7FFFFFFF
실패코드 : 0x80000000 - 0xFFFFFFFF

SUCCEEDED 매크로는 HRESULT가 성공코드이면 TRUE, 실패코드이면 FALSE
FAILED 매크로는 반대이다. 즉 HRESULT가 실패코드이면 TRUE, 성공코드이면 FALSE

대표적 HRESULT 상수값

E_FAIL : 명시되지 않은 에러(0x80004005)
E_POINTER : 포인터값으로 NULL이 부정확하게 건네졌다(0x80004003)
E_UNEXPECTED : 예기치않은 상황(0x8000FFFF)
S_OK : 성공(0x0)
S_FALSE : 성공(0x1)

대표적으로 잘못사용하고 있는 에러 핸들링으로는
HRESULT hr = Function();
if (hr != S_OK)
{
  // 여기로 올수 있는 부분으로 다른 성공 코드가 올수도 있다.
}

그래서 위에 부분은 아래와 같이 정확하게 쓰도록 한다.
HRESULT hr = Function();
if (FAILED(hr))
{
  // Error!
}

S_FALSE가 의미에서 주는것처럼 실패를 의미하는게 아니므로 주의가 필요하다.
MS 친구들이 언급하는 의미로는 "실패가 아닌 부정적인 조건을 의미하는"

확인하고 싶은것에 대해서는 직접 검사하고 이후부터는 매크로를 써라.

if (hr == S_FALSE)
{
  // 특별한 경우를 처리
}
else if (SUCCEEDED(hr))
{
  // 일반적 성공코드 처리
}
else
{
  // 실패코드 처리
}






댓글

이 블로그의 인기 게시물

[WinAPI] 모달리스 다이얼로그 설명

[WinDbg] Debugging a stack overflow

[WinDbg] first-chance, second-chance Exception