COM 코딩 방식
이 항목에서는 COM 코드를 보다 효과적이고 강력하게 만드는 방법을 설명합니다.
__uuidof 연산자
프로그램을 빌드할 때 다음과 같은 링커 오류가 발생할 수 있습니다.
unresolved external symbol "struct _GUID const IID_IDrawable"
이 오류는 GUID 상수가 외부 링크(extern)로 선언되어 링커가 상수의 정의를 찾을 수 없음을 의미합니다. GUID 상수 값은 일반적으로 정적 라이브러리 파일에서 내보냅니다. Microsoft Visual C++를 사용하는 경우 __uuidof 연산자를 사용하여 정적 라이브러리를 연결할 필요가 없습니다. 이 연산자는 Microsoft 언어 확장이며 식에서 GUID 값을 반환합니다. 식은 인터페이스 유형 이름, 클래스 이름 또는 인터페이스 포인터일 수 있습니다. __uuidof를 사용하여 다음과 같이 공통 항목 대화 상자 개체를 만들 수 있습니다.
IFileOpenDialog *pFileOpen;
hr = CoCreateInstance(__uuidof(FileOpenDialog), NULL, CLSCTX_ALL,
__uuidof(pFileOpen), reinterpret_cast<void**>(&pFileOpen));
컴파일러는 헤더에서 GUID 값을 추출하므로 라이브러리 내보내기가 필요하지 않습니다.
참고
GUID 값은 헤더에서 __declspec(uuid( ... ))
을 선언하여 유형 이름과 연결됩니다. 자세한 내용은 Visual C++ 설명서에서 __declspec에 대한 설명을 참조하세요.
IID_PPV_ARGS 매크로
CoCreateInstance와 QueryInterface는 모두 최종 매개 변수를 void** 형식으로 강제 변환해야 합니다. 따라서 형식 불일치가 발생할 가능성이 있습니다. 다음과 같은 코드 조각을 생각해 봅시다.
// Wrong!
IFileOpenDialog *pFileOpen;
hr = CoCreateInstance(
__uuidof(FileOpenDialog),
NULL,
CLSCTX_ALL,
__uuidof(IFileDialogCustomize), // The IID does not match the pointer type!
reinterpret_cast<void**>(&pFileOpen) // Coerce to void**.
);
이 코드는 IFileDialogCustomize 인터페이스를 요청하지만 IFileOpenDialog 포인터를 전달합니다. reinterpret_cast 식은 C++ 형식 시스템을 우회하므로 컴파일러가 이 오류를 catch하지 않습니다. 최상의 경우 개체가 요청된 인터페이스를 구현하지 않으면 호출이 실패합니다. 최악의 경우 함수가 성공하고 일치하지 않는 포인터가 반환됩니다. 즉, 포인터 형식이 메모리의 실제 vtable과 일치하지 않습니다. 예상할 수 있듯이 이 시점에서 좋은 상황이 발생할 수 없습니다.
참고
vtable(가상 메서드 테이블)은 함수 포인터 테이블입니다. vtable은 COM이 런타임에 메서드 호출을 해당 구현에 바인딩하는 방법입니다. 우연치 않게 vtable은 대부분의 C++ 컴파일러가 가상 메서드를 구현하는 방법입니다.
IID_PPV_ARGS 매크로는 이 오류 클래스를 방지하는 데 도움이 됩니다. 이 매크로를 사용하려면 다음 코드를 바꿉니다.
__uuidof(IFileDialogCustomize), reinterpret_cast<void**>(&pFileOpen)
다음 코드로 바꿉니다.
IID_PPV_ARGS(&pFileOpen)
매크로는 인터페이스 식별자에 대해 __uuidof(IFileOpenDialog)
를 자동으로 삽입하므로 포인터 형식과 일치하도록 보장됩니다. 다음은 수정된 올바른 코드입니다.
// Right.
IFileOpenDialog *pFileOpen;
hr = CoCreateInstance(__uuidof(FileOpenDialog), NULL, CLSCTX_ALL,
IID_PPV_ARGS(&pFileOpen));
QueryInterface에서 동일한 매크로를 사용할 수 있습니다.
IFileDialogCustomize *pCustom;
hr = pFileOpen->QueryInterface(IID_PPV_ARGS(&pCustom));
SafeRelease 패턴
참조 계산은 프로그래밍에서 기본적으로 쉽지만 번거롭기도 하여 오류가 발생하기도 쉽습니다. 일반적인 오류는 다음과 같습니다.
- 인터페이스 포인터 사용을 마친 후 해제하지 못합니다. 이런 유형의 버그로 인해 개체가 제거되지 않기 때문에 프로그램에서 메모리 및 기타 리소스 누수가 발생합니다.
- 잘못된 포인터를 사용하여 Release를 호출합니다. 예를 들어 개체를 만들지 않은 경우 이 오류가 발생할 수 있습니다. 이런 범주의 버그로 인해 프로그램 작동이 중단될 수 있습니다.
- Release가 호출된 후 인터페이스 포인터를 역참조합니다. 이 버그로 인해 프로그램 작동이 중단될 수 있습니다. 게다가 나중에 무작위로 프로그램 작동이 중단되어 원래 오류를 추적하기 어려울 수 있습니다.
이러한 버그를 방지하는 한 가지 방법은 포인터를 안전하게 해제하는 함수를 통해 Release를 호출하는 것입니다. 다음 코드는 이 작업을 수행하는 함수를 보여 줍니다.
template <class T> void SafeRelease(T **ppT)
{
if (*ppT)
{
(*ppT)->Release();
*ppT = NULL;
}
}
이 함수는 COM 인터페이스 포인터를 매개 변수로 사용하고 다음을 수행합니다.
- 포인터가 NULL인지 여부를 확인합니다.
- 포인터가 NULL이 아닌 경우 Release를 호출합니다.
- 포인터를 NULL로 설정합니다.
다음은 SafeRelease
사용 방법을 보여 주는 예제입니다.
void UseSafeRelease()
{
IFileOpenDialog *pFileOpen = NULL;
HRESULT hr = CoCreateInstance(__uuidof(FileOpenDialog), NULL,
CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&pFileOpen));
if (SUCCEEDED(hr))
{
// Use the object.
}
SafeRelease(&pFileOpen);
}
CoCreateInstance가 성공하면 SafeRelease
를 호출하여 포인터를 해제합니다. CoCreateInstance가 실패하면 pFileOpen이 NULL로 유지됩니다. SafeRelease
함수는 이를 확인하고 Release 호출을 건너뜁니다.
또한 다음과 같이 동일한 포인터에서 SafeRelease
를 두 번 이상 호출하는 것이 안전합니다.
// Redundant, but OK.
SafeRelease(&pFileOpen);
SafeRelease(&pFileOpen);
COM 스마트 포인터
SafeRelease
함수는 유용하지만 다음 두 가지 사항을 기억해야 합니다.
- 모든 인터페이스 포인터를 NULL로 초기화합니다.
- 각 포인터가 범위를 벗어나기 전에
SafeRelease
를 호출합니다.
C++ 프로그래머로서 이러한 사항을 기억할 필요가 없다고 생각할 수 있습니다. 이런 이유로 인해 C++에 생성자와 소멸자가 있는 것입니다. 기본 인터페이스 포인터를 래핑하고 포인터를 자동으로 초기화하고 해제하는 클래스가 있으면 좋을 것입니다. 즉, 다음과 같은 클래스가 필요합니다.
// Warning: This example is not complete.
template <class T>
class SmartPointer
{
T* ptr;
public:
SmartPointer(T *p) : ptr(p) { }
~SmartPointer()
{
if (ptr) { ptr->Release(); }
}
};
여기에 표시된 클래스 정의는 불완전하며 표시된 대로 사용할 수 없습니다. 최소한 복사 생성자, 할당 연산자 및 기본 COM 포인터에 액세스하는 방법을 정의해야 합니다. 다행히 Microsoft Visual Studio는 이미 ATL(액티브 템플릿 라이브러리)의 일부로 스마트 포인터 클래스를 제공하므로 이 작업을 수행할 필요가 없습니다.
ATL 스마트 포인터 클래스의 이름은 CComPtr입니다. CComQIPtr 클래스도 있지만 여기서는 다루지 않습니다. 다음은 CComPtr을 사용하도록 다시 작성된 열기 대화 상자 예제입니다.
#include <windows.h>
#include <shobjidl.h>
#include <atlbase.h> // Contains the declaration of CComPtr.
int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE, PWSTR pCmdLine, int nCmdShow)
{
HRESULT hr = CoInitializeEx(NULL, COINIT_APARTMENTTHREADED |
COINIT_DISABLE_OLE1DDE);
if (SUCCEEDED(hr))
{
CComPtr<IFileOpenDialog> pFileOpen;
// Create the FileOpenDialog object.
hr = pFileOpen.CoCreateInstance(__uuidof(FileOpenDialog));
if (SUCCEEDED(hr))
{
// Show the Open dialog box.
hr = pFileOpen->Show(NULL);
// Get the file name from the dialog box.
if (SUCCEEDED(hr))
{
CComPtr<IShellItem> pItem;
hr = pFileOpen->GetResult(&pItem);
if (SUCCEEDED(hr))
{
PWSTR pszFilePath;
hr = pItem->GetDisplayName(SIGDN_FILESYSPATH, &pszFilePath);
// Display the file name to the user.
if (SUCCEEDED(hr))
{
MessageBox(NULL, pszFilePath, L"File Path", MB_OK);
CoTaskMemFree(pszFilePath);
}
}
// pItem goes out of scope.
}
// pFileOpen goes out of scope.
}
CoUninitialize();
}
return 0;
}
이 코드와 원래 예제의 주요 차이점은 이 버전이 Release를 명시적으로 호출하지 않는다는 것입니다. CComPtr 인스턴스가 범위를 벗어나면 소멸자가 기본 포인터에서 Release를 호출합니다.
CComPtr은 클래스 템플릿입니다. 템플릿 인수는 COM 인터페이스 유형입니다. 내부적으로 CComPtr은 해당 형식의 포인터를 보유합니다. CComPtr는 클래스가 기본 포인터처럼 작동할 수 있도록 operator->() 및 operator&() 를 재정의합니다. 예를 들어 다음 코드는 IFileOpenDialog::Show 메서드를 직접 호출하는 것과 같습니다.
hr = pFileOpen->Show(NULL);
CComPtr은 몇 가지 기본 매개 변수 값으로 COM CoCreateInstance 함수를 호출하는 CComPtr::CoCreateInstance 메서드도 정의합니다. 다음 예제처럼 유일한 필수 매개 변수는 클래스 식별자입니다.
hr = pFileOpen.CoCreateInstance(__uuidof(FileOpenDialog));
CComPtr::CoCreateInstance 메서드는 순수하게 편의를 위해 제공됩니다. 원하는 경우 COM CoCreateInstance 함수를 계속 호출할 수 있습니다.
다음