또한 본 게시물에서 언급하고 있는 예제 소스 코드는 Visual C++ 6.0을 기준으로 작성되어 있기 때문에 후속 버전의 Visual Studio(또는 Visual Studio .NET)에서 자동 생성되는 COM 코드와는 다소 차이가 있음을 감안하고 읽으시기 바랍니다.
필자의 첫 번째 COM 소개 글로써, 필자는 COM을 이제 막 배우기 시작하였거나, 기초를 이해하고자 하는 프로그래머들을 위한 튜토리얼을 작성하였습니다. 본 게시글에서는 서버 사이드 측면에서 COM을 살펴보고, 여러분이 직접 COM 인터페이스와 COM 서버를 작성하기 위해 필요한 단계들에 대해 설명할 것입니다. 물론 COM 라이브러리에서 호출했을 때 COM 서버에서 어떤 일들이 일어나는지도 자세히 살펴볼 것입니다.
도입
독자 여러분이 이미 필자의 이전 글을 읽어 보셨다면, 여러분은 클라이언트로서 COM을 사용할 때 어떤 것들이 포함되어야 하는지에 대해 정통할 것입니다.
이제 COM을 다른 측면에서 접근해 볼 차례입니다. 이미 컴파일되어 숨어있는 COM 서버입니다. 필자는 이제 아무 클래스 라이브러리도 포함되어 있지 않은 백지 상태의 C++ 언어 프로젝트에서부터 어떻게 COM 서버를 작성하는지에 대해 차근차근 설명하겠습니다.
물론 요즘에는 이러한 접근법이 큰 필요성은 없습니다만, 컴파일되어 숨게 되는 COM 서버를 만드는데 있어 전체적인 코드를 살펴보는 것은, COM 서버에서 일어나는 모든 일들을 확실히 이해하는데 가장 좋은 방법입니다.
본 글에서는 여러분이 C++ 언어를 습득하였다고 보고, 또한 필자가 앞서 적은 글을 통해 COM에서 사용하는 용어와 개념들을 숙지하였다고 보고 내용을 작성하겠습니다. 본 글은 다음과 같은 절(section)으로 구성되어 있습니다.
또한 본 게시물에서 언급하고 있는 예제 소스 코드는 Visual C++ 6.0을 기준으로 작성되어 있기 때문에 후속 버전의 Visual Studio(또는 Visual Studio .NET)에서 자동 생성되는 COM 코드와는 다소 차이가 있음을 감안하고 읽으시기 바랍니다.
필자는 SUCCEEDED와 FAILED 매크로를 사용하여, 오류를 처리하는 방법 몇 가지를 간단하게 언급하였습니다. 지금부터는 COM 메소드가 반환하는 HRESULT를 가지고 좀 더 자세히 다루어 보겠습니다.
HRESULT는 32비트 부호 있는 정수입니다. 음수가 아닌 값은 성공하였음을 나타내고, 음수 값은 실패를 나타냅니다. HRESULT는 (1) 성공 또는 실패를 나타내는 ‘심각도 비트(severity bit)’, (2) ‘패실리티 코드(facility code)’, (3) ‘상태 코드(status code)’의 세 가지 필드를 갖습니다(역자: facility code의 적절한 우리말을 찾지 못하여 그냥 그대로 적었습니다).
‘facility’는 HRESULT가 유래된 컴포넌트 또는 프로그램을 의미합니다. 마이크로소프트는 다양한 컴포넌트에 facility code를 배정하고 있습니다. COM은 이 중 하나이고, 작업 스케줄러도 이 중 하나입니다. ’코드’는 16비트 필드이며 그 자체로 어떤 의미를 내재하지는 않습니다. 즉, 코드는 숫자가 될 수도 있고, GetLastError가 반환하는 값과 비슷하게 어떤 의미가 될 수도 있는데 이는 그때그때 다릅니다.
여러분이 winerror.h 파일을 살펴본다면, 수 많은 HRESULT들이 적혀 있음을 알 수 있습니다. 그리고 이들의 이름은 [facility]_[severity]_[description]의 구조로 되어 있음을 알 수 있습니다. 아무 컴포넌트에서나 반환 가능한 일반적인 HRESULT는 E_OUTOFMEMORY와 같이 그 이름에 facility를 갖고 있지 않습니다.
예를 들어,
REGDB_E_READREGDB
패실리티(facility)가 REGDB(레지스트리 데이터베이스), 심각도는 E(오류), 코드는 이 경우 상황을 설명하는 READREGDB입니다. 조합하면 중대한 오류로서, 데이터베이스를 읽지 못함을 의미합니다.
S_OK
패실리티(facility)는 일반(generic)이고, 심각도는 S(성공)입니다. OK는 현 상황의 상태를 나타내며, 조합하면 모든 것이 정상이라는 뜻입니다.
다행히도, winerror.h를 열어놓지 않아도 HRESULT의 의미를 찾아볼 수 있습니다. 윈도우 운영체제에 내장된 패실리티(facility)는 ‘오류 값 찾기 도구(Error Lookup Tool)’를 이용하여 찾을 수 있습니다.
여러분이 CoCreateInstance를 호출하기 전에 CoInitialize 호출하는 것을 깜빡하였다면, CoCreateInstance는 0x800401F0을 반환할 것입니다. 독자 여러분은 Error Lookup 도구에 이 값을 검색할 것이고, “CoInitialize가 호출되지 않았습니다.”라는 설명을 보게 됩니다.
Error Lookup 도구를 사용하면 HRESULT 값을 문자열로 된 설명으로 확인할 수 있다.
여러분은 또한 디버거에서 HRESULT 설명을 찾을 수 있습니다. 여러분의 HRESULT 형 변수를 hres라고 이름 붙였다면, 변수 감시 창(Watch 창)에 “hres,hr”을 입력함으로써 해당 변수의 값을 볼 수 있습니다. “,hr”은 Visual C++에게 그 변수의 값을 HRESULT로 간주하고 이에 해당하는 텍스트 설명으로 보여주도록 지정합니다.
디버그 중일 때 Watch 화면을 통해 HRESULT 값을 상수 이름으로 볼 수 있다.
참고자료
《Essential COM》 by Don Box (ISBN 0-201-63446-5)
여러분이 한 번쯤은 알고 싶어하실 COM 사양과 IDL(interface, definition language)에 대한 모든 것이 나와 있습니다. 특히 처음 두 챕터에는 COM 스펙과 그리고 이것이 해결하고자 했던 문제에 대해 자세히 나와 있습니다.
《MFC Internals》 by George Shepherd and Scot Wingo (ISBN 0-201-40721-3)
MFC의 COM 지원에 대해 심도 깊은 내용을 포함하고 있습니다.
《Beginning ATL 3 COM Programming》 by Richard Grimes 외 공저 (ISBN 1-861001-20-7)
이 책은 ATL을 사용하여 여러분이 직접 COM 컴포넌트를 개발할 경우에 대하여 상세히 적고 있습니다.
또한 본 게시물에서 언급하고 있는 예제 소스 코드는 Visual C++ 6.0을 기준으로 작성되어 있기 때문에 후속 버전의 Visual Studio(또는 Visual Studio .NET)에서 자동 생성되는 COM 코드와는 다소 차이가 있음을 감안하고 읽으시기 바랍니다.
첫 번째 예제는 하나의 인터페이스를 노출하고 있는 COM 객체를 어떻게 사용하는지를 보여주고 있습니다. 이 예제는 여러분이 앞으로 마주치게 될 소스 코드 중 가장 간단한 것입니다. 이 코드는 현재 바탕화면으로 지정된 그림파일의 이름을 얻기 위하여 쉘에 부속된 액티브 데스크톱 coclass를 사용합니다. 이 코드가 작동되기 위해서 여러분은 윈도우 운영체제에 액티브 데스크톱 기능이 설치되어 있어야 합니다.
이 예제에는 다음과 같은 단계가 포함됩니다.
1. COM 라이브러리를 초기화하기
2. 액티브 데스크톱과 상호작용하기 위해 사용되는 COM 객체를 생성하고, IActiveDesktop 인터페이스에 대한 포인터를 얻기
3. COM 객체에 있는 GetWallpaper 메소드를 호출하기
4. GetWallpaper 호출이 성공하면 바탕화면으로 지정된 그림파일의 이름을 얻기
5. 인터페이스를 포인터에 대해 Release 호출하기
6. COM 라이브러리를 정리하고 사용 해제하기
WCHAR wszWallpaper[MAX_PATH];
CString strPath;
HRESULT hr;
IActiveDesktop * pIAD;
// 1. COM 라이브러리를 초기화하기(윈도우 운영체제로 하여금 DLL 적재하게 하기)
// 일반적으로 여러분은 이 작업을 InitInstance나 기타 초기 호출 코드에서 할 것입니다.
// MFC에서는 AfxOleInit()을 대신 사용합니다.
CoInitialize(NULL);
// 2. 쉘이 제공하는 액티브 데스크톱 coclass를 사용하여 COM 객체를 생성합니다.
// 4번째 파라미터는 우리가 어떤 인터페이스를 원하는지를 지정하는 옵션입니다.
hr = CoCreateInstance(CLSID_ActiveDesktop,
NULL,
CLSCTX_INPROC_SERVER,
IID_IActiveDesktop,
(void **)&pIAD);
if (SUCCEEDED(hr)) {
// 3. COM 객체 생성되었다면, GetWallpaper를 호출합니다.
hr = pIAD->GetWallpaper(wszWallpaper, MAX_PATH, 0);
if (SUCCEEDED(hr)) {
// 4. GetWallpaper 호출이 성공하였다면 파일 이름을 출력합니다
// 유니코드 문자열을 출력하기 위해 wcout을 사용할 것입니다.
// wcout은 cout과 동일하나 유니코드 버전입니다.
std::wcout << L"Wallpaper path is \n" << wszWallpaper << std::endl << std::endl;
} else {
std::cout << "GetWallpaper() failed." << std::endl << std::endl;
}
// 인터페이스 포인터에 대해 사용 해제하기
pIAD->Release();
} else {
std::cout << "CoCreateInstance failed." << std::endl << std::endl;
}
// COM 라이브러리의 사용을 해제합니다.
// MFC 개발의 경우 MFC자 알아서 하기 때문에 이 과정이 필요 없습니다.
CoUninitialize()
위 예제 코드에서 필자는 유니코드 문자열인 wszWallpaper를 출력하기 위하여 std::wcout을 사용하였습니다.
다중 인터페이스를 갖는 COM 객체 사용하기
두 번째 예제는 하나의 인터페이스를 노출하고 있는 COM 객체에 대해 QueryInterface를 어떻게 사용하는지를 보여주고 있습니다. 이 예제에서는 앞서 우리가 확인한 바탕화면 그림파일의 경로를 가지고 바로가기 아이콘을 만들기 위하여 쉘에 포함된 Shell Link coclass를 사용합니다.
이 예제는 다음과 같은 단계를 포함합니다.
1. COM 라이브러리를 초기화하기
2. 바로가기 아이콘을 만드는 COM 객체를 생성하고 IShellLink 인터페이스 포인터를 얻기
3. IShellLink 인터페이스의 SetPath 메소드 호출하기
4. COM 객체에 대해 QueryInterface를 호출하여 IPersistFile 인터페이스 포인터를 얻기
5. IPersistFile의 Save 메소드를 호출하기
6. 모든 인터페이스 포인터에 대해 Release하기
7. COM 라이브러리를 사용 해제하기
CString sWallpaper = wszWallpaper; // 바탕화면 경로를 ANSI 문자열로 변환
IShellLink * pISL;
IPersistFile * pIPF;
// 1. COM 라이브러리를 초기화하기(윈도우 운영체제로 하여금 DLL 적재하게 하기)
// 일반적으로 여러분은 이 작업을 InitInstance나 기타 초기 호출 코드에서 할 것입니다.
// MFC에서는 AfxOleInit()을 대신 사용합니다.
CoInitialize(NULL);
// 2. COM 객체를 사용하기 위하여 쉘에서 제공하는 Shell Link coclass를 사용합니다.
// 4번째 파라미터로 우리가 필요로 하는 인터페이스 포인터가 IShellLink임을 알려줍니다.
hr = CoCreateInstance(CLSID_ShellLink,
NULL,
CLSCTX_INPROC_SERVER,
IID_IShellLink,
(void **)&pISL);
if (SUCCEEDED(hr)) {
// 3. 바탕화면 그림파일의 경로를 바로가기 대상으로 지정합니다.
hr = pISL->SetPath(sWallpaper);
if (SUCCEEDED(hr)) {
// 4. 두 번째 인터페이스인 IPersistFile에 대한 포인터를 얻습니다.
hr = pISL->QueryInterface(IID_IPersistFile, (void **)&pIPF);
if (SUCCEEDED(hr)) {
// 5. 바로가기 파일을 저장합니다. 첫 번째 파라미터는 유니코드 문자열입니다.
hr = pIPF->Save(L”C:\\wallpaper.lnk”, FALSE);
if (SUCCEEDED(hr)) {
// 저장 성공한 경우 처리할 내용
} else {
// 저장 실패한 경우 처리할 내용
}
// 6. IPersistFile 인터페이스에 대한 포인터를 참조 해제합니다.
pIPF->Release();
} else {
// QueryInterface 실패한 경우 처리할 내용
}
// 6. IShellLink 인터페이스에 대한 포인터를 참조 해제합니다.
pISL->Release();
} else {
// SetPath 실패한 경우 처리할 내용
}
또한 본 게시물에서 언급하고 있는 예제 소스 코드는 Visual C++ 6.0을 기준으로 작성되어 있기 때문에 후속 버전의 Visual Studio(또는 Visual Studio .NET)에서 자동 생성되는 COM 코드와는 다소 차이가 있음을 감안하고 읽으시기 바랍니다.
COM 코드에서 문자열을 다루는 방법을 설명하기 위하여 잠시 다른 이야기를 해볼까 합니다. 독자 여러분이 Unicode와 ANSI 문자열이 어떻게 작동되는지에 대해 잘 알고 있고, 이 두 문자열 사이에서 어떻게 변환해야 하는지도 잘 알고 있다면 이 절을 건너뛰어도 됩니다. 그렇지 않은 경우 이 절을 주의 깊게 읽어보시기 바랍니다.
COM 메소드가 문자열을 반환할 때, 그 문자열은 유니코드로 되어있을 것입니다(물론 모든 메소드가 COM 기술 사항을 그대로 만족하면서 작성된다면 말입니다). 유니코드는 아스키코드처럼 문자를 인코딩하는 스키마 중 하나로서, 대부분의 문자는 2바이트 길이를 갖습니다(역자: 원문에는 모든 문자라 되어있지만, 자주 쓰이지 않는 한자(벽자)라든가 최근 도입되는 이모지 문자처럼 UTF-16으로 인코드할 때 1글자가 2바이트 이상인 문자도 있습니다). 독자 여러분이 문자열을 보다 처리하기 용이한 형태로 얻기 위해서 COM 메소드가 반환하는 문자열을 TCHAR 형 문자열로 변환하여야 합니다.
TCHAR라든가 _t로 시작하는 함수들(예를 들어 _tcscpy)은 컴파일러 설정에 따라 여러분이 하나의 소스코드로 유니코드 문자열과 ANSI 문자열 모두를 다룰 수 있도록 디자인된 것입니다. 대부분의 경우 여러분은 ANSI 문자열과 ANSI형 Windows API를 사용하는 코드를 작성하게 될 것입니다(역자: Windows NT, 2000 및 XP 이후로는 굳이 컴파일러의 설정을 바꾸지 않는 이상 TCHAR 형이 WCHAR 형에 맞추어져 있으므로 유니코드 문자열이 기본입니다. 그러나 printf 등의 C 함수들을 사용할 경우를 대비하여 유니코드 문자열을 ANSI 문자열로 변환하는 방법을 알아둘 필요는 있습니다.). 때문에 이 글의 대부분에서, 필자는 간결함을 위해 TCHAR 대신 char을 사용할 것입니다. 여러분은 다른 누군가가 작성한 코드를 마주하였을 때를 대비하여 TCHAR 타입에 대하여 확실하게 숙지하시기 바랍니다.
여러분이 COM 메소드로부터 유니코드 문자열을 전달받았을 때 여러분은 이를 다음 몇 가지 방법으로 char 형 문자열로 변환할 수 있습니다.
1. WideCharToMultiByte Windows API를 호출하기
2. CRT 함수인 wcstombs 호출하기
3. MFC에 한하여 CString 생성자 또는 할당 연산자 사용하기
4. ATL 문자열 변환 매크로 사용하기
WideCharToMultiByte
WideCharToMultiByte Windows API를 사용하여 여러분은 유니코드 문자열을 ANSI 문자열로 변환할 수 있습니다. 원형은 다음과 같습니다.
int WideCharToMultiByte(
UINT CodePage,
DWORD dwFlags,
LPCWSTR lpWideCharStr,
int cchWideChar,
LPSTR lpMultiByteStr,
int cbMultiByte,
LPCSTR lpDefaultChar,
LPBOOL lpUsedDefaultChar);
각 파라미터의 의미는 다음과 같습니다.
CodePage
유니코드 문자열을 ANSI 문자열로 변환할 때 대상이 되는 코드페이지입니다. 운영체제에서 현재설정된 ANSI 코드페이지로 변환하고자 할 때 여러분은 CP_ACP를 전달하면 됩니다(역자: 기본 로마자가 1바이트이고, 한글/한자/특수기호가 2바이트인 일반적인 한국어 ANSI 문자열은 CP 949입니다). 코드페이지는 256개의 문자 집합으로 되어 있습니다. 0부터 127번 문자는 항상 표준 아스키코드 문자에 대응합니다. 128번부터 255번 문자는 코드페이지마다 달라지며, 그래픽문자나 발음기호를 포함하는 로마자가 포함될 수 있습니다. 각 언어 또는 지역은 각자의 코드페이지를 갖습니다. 그러므로 강세표시된 문자를 올바르게 표시하기 위해서는 정확한 코드페이지를 지정하고 사용할 필요가 있습니다(역자: 한국어의 경우 CP 949를 맞추어 주어야 글자가 깨지지 않습니다).
dwFlags
조합된 유니코드 문자를 어떻게 다룰 것인지에 대한 방법을 지정합니다. 여기서 조합된 문자란, 기본 로마자에 강세표시나 발음기호 문자 등이 붙는 경우입니다. 예를 들어 è라는 문자에 대해 보겠습니다. 지정된 코드페이지에 이러한 문자가 정의되어 있다면, 아무 문제 없이 그대로 코드값만 바뀝니다. 그러나 해당 코드페이지에 이 문자가 정의되어 있지 않은 경우 Windows 운영체제는 이 문자를 다른 무언가로 바꾸어 나타낼 필요가 있습니다. WC_COMPOSITECHECK 옵션을 전달하면, 이 API는 지정된 코드페이지에 그러한 문자가 정의되어 있는지를 사전에 검사할 수 있습니다. WC_SEPCHARS 옵션을 전달하면 Windows 운영체제는 위와 같은 글자를 기본 로마자(e)와 강세 표시(`)라는 2개의 문자로 쪼개어 표현할 수 있습니다. WC_DISCARDNS 옵션을 전달하면 Windows 운영체제는 위와 같은 문자에 대해 강세표시나 발음기호를 제거하고 기본 문자인 e로만 변환합니다. WC_DEFAULTCHAR 옵션을 전달하면 Windows 운영체제는 후술할 lpDefaultChar 파라미터로 전달된 기본 문자로 치환합니다. 이 파라미터의 기본값은 WC_SEPCHARS입니다.
lpWideCharStr
특정 코드페이지의 ANSI 문자열로 변환할 원본 유니코드 문자열입니다.
cchWideChar
유니코드 문자열인 lpWideCharStr의 길이입니다. 여러분은 대개 -1을 전달하게 될 것입니다. 이 경우 지정된 문자열이 NULL 문자로 끝난다는 것을 의미하고 API가 알아서 문자열의 길이를 측정합니다.
lpMultiByteStr
ANSI 문자열로 변환된 결과가 보관될 버퍼입니다.
cbMultiByte
lpMultiByteStr 버퍼의 크기입니다. 단위는 바이트입니다.
lpDefaultChar
이는 선택적으로 전달되는 파라미터입니다. dwFlags 매개변수가 WC_COMPOSITECHECK | WC_DEFAULTCHAR를 포함하고 있을 때, 특정 코드페이지에서 정의되지 않았던 문자는 이 매개변수에서 제시한 문자로 치환됩니다. 여러분이 이 매개변수에 NULL을 전달하면 운영체제에서 기본으로 정의된 문자로 치환될 것인데, 대체로 물음표(?) 문자가 나타날 것입니다.
lpUsedDefaultChar
이는 선택적으로 전달되는 파라미터입니다. BOOL 형 변수에 대한 포인터로서, ANSI 문자열로 변환할 때 호환되지 않는 문자가 있어서 앞서 설명한 lpDefaultChar가 사용되었다면 여기로 전달한 BOOL 형 변수의 값은 TRUE로 바뀔 것입니다. 이 정보가 필요없다면 NULL을 전달해도 됩니다.
매우 길고도 많은 매개변수 설명이었습니다. MSDN 문서에는 이보다 더 복잡한 설명들이 포함되어 있습니다. 이제 이 API의 사용 예를 보겠습니다.
// wszSomeString으로 유니코드 문자열을 이미 가지고 있다고 가정
wchar_t wszSomeString[MAX_PATH];
char szANSIString[MAX_PATH];
WideCharToMultiByte(CP_ACP, // 운영체제에 현재 설정된 ANSI 코드 페이지
WC_COMPOSITECHECK, // 강세표시된 문자가 있는지 체크함
wszSomeString, // 원본 유니코드 문자열
-1, // wszSomeString이 NULL 문자로 끝나는 문자열임
szANSIString, // char형 버퍼
sizeof(szANSIString), // 버퍼의 크기(바이트)
NULL, // 변환 실패 시 치환할 기본 문자 없음
NULL); // 변환 실패한 문자 여부를 가져올 필요 없음
위와 같이 호출하면 szANSIString은 원본 유니코드 문자열에 대한 ANSI 버전의 사본을 갖게 됩니다.
wcstombs
CRT 함수인 wcstombs는 호출이 간단하지만 결국 WideCharToMultiByte를 호출하게 됩니다. 그러므로 결과는 동일합니다. wcstombs의 원형은 다음과 같습니다.
MFC CString 클래스는 유니코드 문자열을 받아들일 수 있는 생성자와 할당 연산자를 포함하고 있습니다. 때문에 독자 여러분은 CString이 알아서 변환을 하도록 그냥 두면 됩니다. 예를 들면 다음과 같습니다.
CString str1(wscSomeString); // 생성자를 사용하여 변환
CString str2 = wszSomeString; // 대입 연산자를 사용하여 변환
ATL 매크로
ATL은 문자열 변환을 위한 유용한 매크로 집합을 포함하고 있습니다. 유니코드 문자열을 ANSI 문자열로 변환하기 위하여 W2A(wide to ANSI) 매크로를 사용합니다. 보다 더 정확하게 말하자면, 여러분은 OLE2A를 사용해야 합니다. 여기서 OLE는 COM이나 OLE에서 전달된 문자열을 의미합니다. 어쨌든, 이들 매크로를 사용하는 방법은 다음과 같습니다.
#include <atlconv.h>
// wszSomeString을 이미 가지고 있다고 가정
wchar_t wszSomeString[MAX_PATH];
char szANSIString[MAX_PATH];
USES_CONVERSION; // 매크로에서 사용되는 지역변수들을 선언
lstrcpy(szANSIString, OLE2A(wszSomeString));
OLE2A 매크로는 변환된 문자열에 대한 포인터를 반환합니다. 그런데 이 변환된 문자열은 임시변수인 스택 위치에 있는 버퍼에 보관되어 있습니다. 그러므로 우리는 lstrcpy를 사용하여 복사본을 만들어야 합니다. 또 다른 매크로로서 독자 여러분이 아셔야 할 것은 W2T(유니코드 문자열을 TCHAR 형 문자열로 변환) 및 W2CT(유니코드 문자열을 const TCHAR 형 문자열로 변환)가 있습니다.
위 예제코드에서 설명했던 OLE2A와 마찬가지로 OLE2CA(유니코드 문자열을 const char 형 문자열로 변환) 매크로도 있습니다. 위 예제코드의 경우 사실 OLE2CA를 사용하는 것이 더 알맞습니다. 왜냐하면 lstrcpy의 두 번째 파라미터가 const char * 형 문자열을 받기 때문입니다. 그러나 필자는 여러분에게 한번에 너무 과도한 정보를 전달하고 싶지는 않습니다.
유니코드로 고정하기
앞서 설명한 것과는 반대로, 여러분은 문자열에 대한 너무 복잡한 처리과정을 피하고자 COM으로부터 받은 문자열을 유니코드 문자열 그대로 보존할 수도 있습니다. (역자: 원 글이 작성된 시기가 2000년 ~ 2001년 사이임을 감안하여야 합니다. 이 당시 일반 사용자 운영체제는 ANSI 문자열이 기본인 Windows 9x가 주류였고, 원 글 작성자의 모국어가 영어인 만큼 1문자당 1바이트면 충분했습니다. 현재에는 윈도우 운영체제 자체가 유니코드에 기반한 다국어 환경을 기본으로 지원하기 때문에 TCHAR = WCHAR = wchar_t = 2 바이트가 시스템 기본 문자형입니다.) 특히 여러분이 콘솔 어플리케이션을 작성할 때 여러분은 std::wcout라는 전역 객체를 사용하여 유니코드 문자열을 출력할 수 있습니다. 사용 예는 다음과 같습니다.
std::wcout << wszSomeString;
std::wcout은 모든 문자열이 유니코드 문자열일 것이라 간주하고 있음을 숙지하시기 바랍니다. 여러분이 보통의 문자열(ANSI 문자열)을 출력하고자 할 때는 std::cout으로 출력합니다. 문자열 리터럴(문자열 상수)을 출력하고자 할 때 문자열 상수 앞에 접두어 L을 붙임으로써 유니코드 문자열임을 표시합니다. 예를 들어 다음과 같습니다.
또한 본 게시물에서 언급하고 있는 예제 소스 코드는 Visual C++ 6.0을 기준으로 작성되어 있기 때문에 후속 버전의 Visual Studio(또는 Visual Studio .NET)에서 자동 생성되는 COM 코드와는 다소 차이가 있음을 감안하고 읽으시기 바랍니다.
모든 COM 인터페이스는 IUnknown으로부터 파생됩니다. 이 인터페이스의 이름은 다소 오해의 소지가 있는데, 사실 이것은 ‘알 수 없는(unknown)’ 인터페이스라는 뜻이 아닙니다. 모든 COM 객체는 IUnknown을 구현하고 있기 때문에 여러분이 어떤 COM 객체에 대해 IUnknown 포인터를 가지고 있다면, 여러분은 그 포인터가 정확히 어떤 형식의 객체를 가리키고 있는지 알 수 없다는 것을 이 이름이 의미하고 있습니다.
IUnknown은 세 가지 메소드들을 갖습니다.
AddRef
COM 객체에게 레퍼런스 카운트를 증가할 것을 알려줍니다. 여러분이 인터페이스 포인터의 복사본을 만들고, 원래의 포인터와 복사된 포인터를 함께 사용하려 할 때 이 메소드를 호출할 수 있습니다. 그러나 본 글에서 우리는 AddRef를 굳이 사용할 필요는 없습니다.
Release
COM 객체에게 레퍼런스 카운트를 감소할 것을 알려줍니다. 앞서 언급한 Release 호출 예제를 참고하시기 바랍니다.
QueryInterface
COM 객체에게 인터페이스 포인터를 요청합니다. 여러분은 coclass가 하나 이상의 인터페이스를 구현하고 있을 때 이 메소드를 사용할 수 있습니다.
우리는 이미 예제 코드에서 Release의 사용을 보았습니다. 그런데 QueryInterface는 무엇일까요? 여러분이 CoCreateInstance를 사용하여 COM 객체를 만들 때, 여러분은 인터페이스 포인터를 얻게 됩니다. COM 객체가 IUnknown을 제외하고 하나 이상의 인터페이스를 구현하고 있을 때, 여러분은 QueryInterface를 사용하여 여러분이 필요로 하는 추가적인 인터페이스 포인터를 얻을 수 있습니다. QueryInterface의 원형은 다음과 같습니다.
인터페이스 포인터에 대한 포인터입니다. COM 객체가 iid 파라미터로 지정한 인터페이스를 구현하고 있다면, 그에 대한 인터페이스 포인터가 이 매개변수를 통해 반환됩니다.
앞서 언급한 쉘 바로가기 예제를 봅시다. 쉘 바로가기를 만들 수 있는 coclass는 IShellLink와 IPersistFile을 구현하고 있습니다. 여러분이 이미 해당 COM 객체에 대해 IShellLink형의 인터페이스 포인터를 가지고 있다면, 같은 COM 객체에게 IPersistFile형의 인터페이스 포인터도 얻을 수 있습니다. 그 방법은 다음과 같습니다.
그 다음 QueryInterface가 정상적으로 작동하였는지 확인하기 위하여 SUCCEEDED 또는 FAILED 매크로를 사용하여 hr의 값을 검사합니다. 다른 형식의 인터페이스 포인터를 얻는데 성공하였다면, 여러분은 이 새로운 인터페이스 포인터도 사용할 수 있습니다. 단, 다른 인터페이스처럼 사용이 끝났을 때 여러분은 필히 pIPF->Release()를 호출하여야 합니다.
또한 본 게시물에서 언급하고 있는 예제 소스 코드는 Visual C++ 6.0을 기준으로 작성되어 있기 때문에 후속 버전의 Visual Studio(또는 Visual Studio .NET)에서 자동 생성되는 COM 코드와는 다소 차이가 있음을 감안하고 읽으시기 바랍니다.
모든 언어는 객체를 다루기 위한 고유한 방법을 가지고 있습니다. 예를 들어 C++에서 여러분은 객체를 스택에 정적할당할 수도 있고, new 키워드를 사용해 동적할당할 수도 있습니다. COM은 언어 중립적이기 때문에 COM 라이브러리는 자체적인 객체 관리 기능을 제공하고 있습니다. COM 객체와 C++ 객체는 다음과 같이 비교 가능합니다.
《객체를 새로 만들 때》
- C++에서는 new 연산자로 동적할당 하거나, 스택에 정적할당 합니다.
- COM에서는 COM 라이브러리에 있는 API를 호출합니다.
《객체를 삭제할 때》
- C++에서는 delete 연산자로 할당을 해제하거나, 스택에 정적할당된 객체에 대해서는 스코프(scope) 바깥으로 프로그램의 흐름을 이동합니다.
- COM에서 모든 객체는 자신의 레퍼런스 카운트를 가지고 있습니다. 호출자는 객체에게 언제 호출자가 실행되었는지를 반드시 알려주어야 합니다. COM 객체는 레퍼런스 카운트가 0이 되었을 때 스스로를 할당 해제합니다.
이제, 객체를 생성하고 파괴하는 두 단계 사이에 독자 여러분은 실질적으로 이 COM 객체를 사용할 수 있습니다. 독자 여러분이 COM 객체를 생성할 때, 여러분은 COM 라이브러리에게 어떤 인터페이스가 필요한지를 알려주게 됩니다. 또한 독자 여러분은 보통의 C++ 객체처럼 포인터를 사용하여 메소드들을 호출할 수 있습니다.
COM 객체를 생성하기
COM 객체를 생성하고 객체로부터 인터페이스를 얻기 위하여, 독자 여러분은 COM 라이브러리의 API인 CoCreateInstance를 호출하게 됩니다. CoCreateInstance의 원형은 다음과 같습니다.
coclass의 CLSID입니다. 예를 들어 바로가기 아이콘을 만들 수 있는 COM 객체를 생성하기 위하여 여러분은 CLSID_ShellLink를 전달할 수 있습니다.
pUnkOuter
이 파라미터는 COM 객체들을 결합시킬 때 사용합니다. 여기서 결합은 이미 존재하는 coclass에 새로운 메소드들을 덧붙이는 작업을 의미합니다. 객체 결합을 하지 않을 것이므로 우리는 여기에 NULL을 전달할 것입니다.
dwClsContext
우리가 사용하고자 하는 COM 서버의 종류를 지정합니다. 본 글에서 우리는 가장 단순한 형태의 서버인 ‘인 프로세스 DLL(in-process DLL)’만을 사용할 것이기 때문에, 우리는 여기에 CLSCTX_INPROC_SERVER를 전달합니다. 한가지 주의할 것은, 독자 여러분은 ATL에서는 기본값으로 쓰이는 CLSCTX_ALL 속성을 사용해서는 안 된다는 것입니다. 왜냐하면 DCOM이 설치되지 않은 Windows 95 운영체제에서는 실패할 것이기 때문입니다.
riid
독자 여러분이 얻고자 하는 인터페이스의 IID입니다. 예를 들어, IShellLink 인터페이스의 포인터를 얻고자 할 때 독자 여러분은 IID_IShellLink를 전달할 수 있습니다.
ppv
인터페이스 포인터의 주소입니다. COM 라이브러리는 이 파라미터를 통해 요청한 인터페이스를 전달할 것입니다.
독자 여러분이 CoCreateInstance를 호출할 때, 이 함수는 레지스트리에서 CLSID를 탐색할 것입니다. 그리고 서버의 위치를 읽을 것이고 메모리로 서버를 적재하여 여러분이 요청한 coclass에 해당하는 인스턴스를 만들 것입니다.
아래 코드는 호출 예제입니다. CLSID_ShellLink에 해당하는 객체를 생성 및 초기화하고, 그 객체의 IShellLink 인터페이스 포인터를 요청하게 됩니다.
HRESULT hr;
IShellLink * pISL;
hr = CoCreateInstance(CLSID_ShellLink, // coclass의 CLSID
NULL, // 통합과 관련된 것이므로 사용하지 않음
CLSCTX_INPROC_SERVER, // 서버의 종류
IID_IShellLink, // 인터페이스의 IID
(void **)&pISL); // 우리가 얻고자 하는 인터페이스 포인터에 대한 포인터
if (SUCCEEDED(hr)) {
// pISH을 사용한 메소드 호출
} else {
// COM 객체를 생성할 수 없는 경우 hr은 오류 코드를 갖습니다
}
먼저 우리는 CoCreateInstance로부터 반환되는 값을 보관하기 위한 HRESULT를 선언하고, 또한 IShellLink 형식의 포인터도 선언하였습니다.
새로운 COM 객체를 생성하기 위해 CoCreateInstance를 호출합니다. hr이 성공을 나타내는 값을 갖고 있다면 SUCCEEDED 매크로는 TRUE 값을 반환할 것이고, 그렇지 않을 경우 SUCCEEDED 매크로는 FALSE 값을 반환할 것입니다. 코드가 실패했는지 여부를 판단하기 위하여 FAILED 매크로가 또한 제공됩니다.
COM 객체를 해제하기
앞서 설명한 바와 같이 독자 여러분은 COM 객체를 직접 해제할 수는 없습니다. 대신 여러분은 이 객체의 사용을 끝냈다고 알려주기만 합니다. 모든 COM 객체가 구현하고 있는 IUnknown 인터페이스는 Release 메소드를 가지고 있습니다. 독자 여러분은 COM 객체에 이 메소드를 호출함으로써 더 이상 사용하지 않음을 알려주면 됩니다. 독자 여러분이 일단 Release를 호출하였다면, COM 객체가 언제든지 메모리에서 사라질 수 있으므로 이후로 절대 인터페이스 포인터를 사용해서는 안됩니다.
만일 여러분의 어플리케이션이 여러가지 다양한 COM 객체를 사용하고 있다면, 인터페이스의 사용을 끝낼 때마다 Release를 호출하는 것이 매우 중요합니다. 여러분이 인터페이스의 사용을 해제하지 않는다면 COM 객체와 이들의 코드를 가지고 있는 DLL들도 메모리에 계속 남게 되어 여러분의 어플리케이션 실행에 불필요하게 붙어있게 됩니다. 여러분의 어플리케이션이 장시간 실행되고 있을 경우 여러분은 유휴시간동안 CoFreeUnusedLibraries를 실행할 것이 권장됩니다. 이 API는 더 이상 참조되지 않는 COM 서버를 메모리 적재 해제하여 여러분의 어플리케이션의 메모리 사용량을 줄여줍니다.
앞의 예제에 이어서 Release를 이렇게 사용하면 됩니다.
// 앞에서 적은 바와 같이 COM 객체를 생성한 후
if (SUCCEEDED(hr)) {
// pISL을 사용하는 메소드 호출
// COM 객체에게 우리가 사용을 끝냈음을 알림
pISL->Release();
}
또한 본 게시물에서 언급하고 있는 예제 소스 코드는 Visual C++ 6.0을 기준으로 작성되어 있기 때문에 후속 버전의 Visual Studio(또는 Visual Studio .NET)에서 자동 생성되는 COM 코드와는 다소 차이가 있음을 감안하고 읽으시기 바랍니다.
보다 구체적으로 살펴봅시다. ‘인터페이스(interface)’는 쉽게 말하면 함수들의 집합입니다. 또한 인터페이스에 속한 함수들을 ‘메소드(method)’라고 부릅니다. 인터페이스 이름은 대문자 ‘I’로 시작하는데, 예를 들어 IShellLink가 있습니다. C++에서 인터페이스는 순수 가상함수들만을 포함하는 추상 클래스로서 작성됩니다.
인터페이스는 다른 인터페이스에서 상속할 수 있습니다. 상속은 C++의 단일상속과 비슷하게 작동합니다. 인터페이스에서 다중상속은 허용되지 않습니다.
‘coclass(component object class의 약어)’는 DLL 또는 EXE 등에 포함되어 있습니다. 또한 하나 이상의 인터페이스에 대한 코드 비하인드(code behind)를 가지고 있습니다. 때문에 ‘coclass’는 이러한 인터페이스를 “구현한다(implement)”고도 부릅니다. ‘COM 객체(COM object)’는 메모리에서 ‘coclass’의 인스턴스입니다. 종종 COM 클래스의 구현체가 C++ 클래스이기는 하지만, COM 클래스가 C++의 클래스와 같지 않음을 숙지하시기 바랍니다.
‘COM 서버(COM server)’는 하나 이상의 ‘coclass’를 포함하고 있는 DLL, ELE 등의 바이너리입니다.
‘등록(registration)’은 Windows 운영체제에서 COM 서버의 위치를 알려주는 레지스트리 진입점을 생성하는 과정입니다.
‘등록 해제(unregistration)’는 그 반대로서, 레지스트리 진입점을 제거합니다.
‘GUID(glocal unique identifier의 약어)’는 128비트 숫자입니다. GUID는 COM이 대상을 식별하기 위한 언어 독립적인 방법입니다. 각각의 인터페이스와 ‘coclass’는 GUID를 갖습니다. 독자 여러분이 GUID를 생성할 때 COM API를 사용하는 한, GUID는 전세계에서 유일하기 때문에 이름 충돌을 회피할 수 있습니다. 또한 독자 여러분은 ‘UUID(universally unique identifier의 약어)’라는 용어도 접할 수 있는데, 일반적으로 UUID와 GUID는 같습니다.
‘클래스 ID, 또는 ‘CLSID’는 ‘coclass’를 지명하는 GUID입니다. 인터페이스 ID, 또는 ‘IID’는 인터이스를 지명하는 GUID입니다.
COM에서 GUID를 적극적으로 사용하는 이유는 다음과 같이 두 가지로 정리할 수 있습니다.
1. GUID는 내부적으로 숫자에 불과합니다. 따라서 어떤 프로그래밍 언어라도 이를 다룰 수 있습니다.
2. 누가 어떤 장치로 GUID를 생성하든, 모든 GUID는 적절하게 생성되기만 한다면 유일합니다. 그러므로 COM 개발자들은 서로 같지 않은 유일한 GUID를 얻을 수 있습니다. 이는 GUID를 발급하는 중앙 기관이 필요하지 않게 합니다.
‘HRESULT’는 COM이 오류 또는 성공 코드를 반환하기 위해 사용되는 통합된 자료형입니다. 자료형 명칭에 ‘H’ 접두어가 있지만, 어떤 대상과 관계된 ‘핸들’이 아닙니다. 이후에 HRESULT와 이를 어떻게 사용하는지에 대해 충분히 설명하겠습니다.
‘COM 라이브러리(COM library)’는 COM 관련 작업을 수행할 때 독자 여러분이 상호작용하는 운영체제의 일부입니다. 종종 COM 라이브러리를 줄여서 COM이라 부르기도 하지만, 여기에서는 혼동을 피하기 위해 COM 라이브러리라고만 부르겠습니다.
또한 본 게시물에서 언급하고 있는 예제 소스 코드는 Visual C++ 6.0을 기준으로 작성되어 있기 때문에 후속 버전의 Visual Studio(또는 Visual Studio .NET)에서 자동 생성되는 COM 코드와는 다소 차이가 있음을 감안하고 읽으시기 바랍니다.
COM은, 간단히 말해서, 서로 다른 어플리케이션과 프로그래밍 언어 사이에 바이너리 코드를 공유하는 방법입니다. 이것은 소스 코드의 재사용을 추구하는 C++ 방식의 접근법과는 다릅니다. 소스코드의 재사용을 추구하는 완벽한 예로는 ATL이 있습니다. 다만 ATL은 소스코드 단계에서의 재사용이 원활하다고 해도 C++ 언어에서만 사용할 수 있습니다. 이는 또한 중복되는 이름 때문에 충돌할 가능성이 있고 독자 여러분의 프로젝트에서 코드 중복을 일으켜 프로그램 크기가 커지게 할 수도 있습니다.
Windows는 DLL을 사용하여 바이너리 단계에서 코드를 공유할 수 있도록 하였습니다. 바로 Windows 어플리케이션이 kernel32.dll, user32.dll 등을 재사용하여 구동되는 방식입니다. 그러나 이러한 DLL은 C의 인터페이스로 작성되었기 때문에 C 언어 또는 C 호출 규약을 지원하는 언어들에서만 사용이 가능합니다. 이러한 제약은 DLL 그 자체보다도 그 프로그래밍 언어로 작성하는 구현체에 부담을 가져오게 됩니다.
MFC는 ‘MFC 확장 DLL(MFC Extension DLL)’이라는 다른 방식의 바이너리 공유 메커니즘을 도입하였습니다. 그러나 이것은 더욱 큰 제약사항이 존재하는데, 바로 독자 여러분이 MFC 어플리케이션을 개발할 때에만 사용 가능하다는 것입니다.
COM은 ‘바이너리 스탠더드(binary standard)’를 정의함으로써 이러한 문제를 해결하고 있습니다. 이것은 DLL이나 EXE 등의 바이너리 모듈이 특별한 구조에 맞추어 컴파일되어야 한다는 뜻입니다. 그리고 이 표준은 COM 객체가 메모리에서 어떻게 구성되어야 하는지도 정의합니다. 또한 바이너리들은 특정 프로그래밍 언어의 기능에 의존해서도 안 됩니다(예를 들면 C++의 네임 데코레이션 같은 기능). 일단 위와 같은 조건이 만족하면 해당 모듈은 어느 프로그래밍 언어에서도 쉽게 접근이 가능합니다. 바이너리 스탠더드는 바이너리를 생성할 수 있는 컴파일러에게 호환성을 지킬 것을 요구하는데, 이렇게 하면 나중에 입문하는 프로그래머 또는 바이너리를 사용하고자 하는 프로그래머들의 작업이 훨씬 쉬워집니다.
메모리에서 COM 객체의 구조는 가상함수가 사용된 C++ 객체와 같습니다. 이는 대다수의 COM 코드가 C++로 작성되는 이유이기도 합니다. 그러나 꼭 기억하시기 바랍니다. COM 모듈을 작성하는데 어떤 언어가 사용되는지는 중요하지 않습니다. 왜냐하면 출력되는 바이너리는 모든 언어에서 사용 가능하기 때문입니다.
한편, COM은 Win32 전용이 아닙니다. 이론적으로 COM은 유닉스나 기타 운영체제로 포팅이 가능합니다. 그러나 필자는 Windows 이외의 환경에서 COM이 언급된 사실을 본 적이 없습니다.
또한 본 게시물에서 언급하고 있는 예제 소스 코드는 Visual C++ 6.0을 기준으로 작성되어 있기 때문에 후속 버전의 Visual Studio(또는 Visual Studio .NET)에서 자동 생성되는 COM 코드와는 다소 차이가 있음을 감안하고 읽으시기 바랍니다.
필자는 COM에 이제 막 입문하여 기초를 이해하는 데 도움이 필요한 프로그래머를 위하여 이 튜토리얼을 작성하였습니다. 본 글에서는 COM의 기술적인 사항을 간략하게 다루고, COM 용어들에 대해 설명한 다음, 이미 존재하는 COM 구성요소들을 어떻게 재사용하는지에 대해 설명하겠습니다.
도입
COM(Component Object Model)은 가장 유명한 TLA(three-letter acronym, 3글자 약어)로서 오늘날까지 Windows 세계의 어디에나 있는 것처럼 보입니다. 새롭게 출시되는 수 많은 기술들은 COM에 기반하여 존재합니다. 그러한 기술 문서에서는 COM 객체(COM object), 인터페이스(interface), 서버(server) 등의 다양한 용어 설명은 빼고, 독자 여러분이 COM이 어떻게 작동하고 어떻게 사용하는지에 대해 잘 알고 있다고 가정합니다.
본 글에서는 COM을 기초부터 소개하면서, 잠재되어 있는 메커니즘까지 포함하여 설명하겠습니다. 또한 다른 기술(특히 Windows Shell)이 제공하는 COM 객체를 어떻게 사용하는지도 보여드리고자 합니다. 이 글의 마지막에서 여러분은 Windows에 내장되었거나 서드파티가 제공하는 COM 객체를 사용할 수 있게 될 것입니다.
또한 이 글에서는 독자 여러분이 C++을 습득하였을 것이라고 가정합니다. 필자는 본 글에서 약간의 MFC와 ATL 예제 코드를 사용 할 것입니다만, 또한 그 코드들에 대해 상세히 설명해드릴 것입니다. 그러므로 여러분은 MFC와 ATL에 익숙하지 않더라도 이미 숙지한 C++ 언어에 대한 배경지식을 활용하여 필자의 내용에 따라오실 수 있어야 합니다.
이 글에서는 다음과 같은 절(section)이 포함됩니다.
《COM – 이것은 정확히 무엇인가?》
COM 표준을 간략이 소개하고 이것이 만들어지게 된 배경이 되는 문제를 소개할 것입니다. 물론 독자 여러분은 COM을 사용하는데 이러한 내용들을 알아야 될 필요는 없습니다만, 왜 COM을 사용하여 그러한 문제가 해결되는지에 대해 이해할 수 있도록, 이 절을 읽기를 필자는 권장합니다.
스레드간 공유 변수에 접근할 때 EnterCriticalSection으로 크리티컬 섹션에 진입하고 공유 변수 사용이 끝나면 LeaveCriticalSection으로 크리티컬 섹션을 벗어난다. EnterCriticalSection은 타 스레드에서 크리티컬 섹션을 사용하고 있을 때 대기한다. 대기과정(현재 스레드의 중지) 없이 즉시 사용중 여부를 확인하고자 하면 TryEnterCriticalSection을 사용한다. 임계영역에 진입했다면 TRUE를 반환하고 그렇지 못했다면 FALSE를 반환한다.
EnterCriticalSection(&g_stCriticalSection);
{
/* ... g_dwInteger에 대한 읽고 쓰기 */
}
LeaveCriticalSection(&g_stCriticalSection);
실행 예제
다음은 크리티컬 섹션의 실행 예이다. 창에 대한 스레드, 콘솔창에 대한 스레드 총 2개의 스레드가 실행중이며 콘솔창으로 1부터 30까지 센 후 콘솔창은 닫힌다. 창의 버튼을 클릭하면 콘솔창에서 세고 있는 숫자는 1로 리셋된다. 이 과정에서 크리티컬 섹션이 사용된다.
위와 같이 호출하여 dwExitCode의 값이 STILL_ACTIVE이면 해당 스레드는 현재 실행중인 상태임을 의미한다. 해당 스레드가 종료되었다면, 종료 당시 스레드 프로시저가 리턴한 값이 보관된다. 여기서 알 수 있는 사실은, 스레드 프로시저를 작성할 때 종료 코드를 STILL_ACTIVE와 중복시키면 안 된다는 사실이다. STILL_ACTIVE의 실제 값은 정수 259이다.
버튼으로 채워진 창을 하나 열고, 이 창이 열릴 때 오랜 시간이 걸리는 작업을 수행하는 콘솔창도 하나 더 띄운다. 이 콘솔창은 스레드가 종료되면 자동으로 닫힌다. 버튼을 클릭하면 GetExitCodeThread을 실행하여 해당 스레드의 생존 여부를 디버그 출력창으로 보일 것이다.
매개변수에 대한 자세한 내용은 MSDN을 참조한다. 여기에서는 간략한 사용법만을 보기 위해 각종 보안 코드들은 생략한다. 다음은 가장 간단한 호출법이다. lpStartAddress(3번째 매개변수)에 앞서 작성한 프로시저를 지정하고, lpThreadId(마지막 매개변수)에 새로 생성된 스레드의 ID값이 전달된다. 반환되는 값은 생성된 스레드의 핸들(HANDLE)이다.
WM_PAINT(MFC: OnPaint) 이벤트는 창의 내용을 그릴 때 호출되는 이벤트이다. 이 이벤트에 DirectX 9.0을 적용하여 보겠다.
IDirect3DDevice9::TestCooperativeLevel
로스트 상태는, DirectX 객체와 실제 그래픽 어댑터 사이의 연결이 끊어진 상태를 뜻한다. DirectX 객체가 생성된 시점과 이 객체를 이용하여 실제로 그리는 시각 시점의 상대적으로 긴 시간이 있는 경우 이 사이에 여러가지 이유(예: 로그오프, 절전모드 등)로 객체와 장치간 연결이 끊어질 수 있는데 이는 IDirect3DDevice9::TestCooperativeLevel함수로 체크할 수 있다.
만일 DirectX와 장치간 연결이 정상이 아닐 경우 그 원인에 따라 D3DERR_DRIVERINTERNALERROR, D3DERR_DEVICELOST, D3DERR_DEVICENOTRESET 중 하나를 반환하는데, 이 때는 IDirect3DDevice9::CreateDevice에 사용되었던 D3DPRESENT_PARAMETERS 구조체를 다시 사용하여 IDirect3DDevice9::Reset 함수를 실행하여 장치와 객체의 연결을 리셋한다.
/* C++ Source */
IDirect3DDevice * pDirect3DDevice;
D3DPRESENT_PARAMETERS d3dPresentParameters;
HRESULT hResult;
// ...
hResult = pDirect3DDevice->TestCooperativeLevel();
switch (hResult)
{
case D3DERR_DRIVERINTERNALERROR:
pDirect3DDevice->Reset(&d3dPresentParameters); // D3DPRESENT_PARAMETERS *
// Reset 실패 시 각종 조치
break;
case D3DERR_DEVICELOST:
// Reset 실패 시 각종 조치
pDirect3DDevice->Reset(&d3dPresentParameters); // D3DPRESENT_PARAMETERS *
break;
case D3DERR_DEVICENOTRESET:
// Reset 실패 시 각종 조치
pDirect3DDevice->Reset(&d3dPresentParameters); // D3DPRESENT_PARAMETERS *
break;
}
IDirect3DDevice9::Clear
장치의 로스트 상태 확인 및 조치를 완료하였다면 지정된 위치의 버퍼를 특정 색상으로 지운다.
/* C++ Source */
IDirect3DDevice * pDirect3DDevice;
D3DPRESENT_PARAMETERS d3dPresentParameters;
DWORD dwRects;
D3DRECT * pRects;
HRESULT hResult;
// ...
hResult = pDirect3DDevice->Clear
(
0, // DWORD Count : pRects 배열의 원소 수
NULL, // const D3DRECT * pRects
D3DCLEAR_TARGET, //
D3DCOLOR_XRGB(0x00, 0xFF, 0xFF), // D3DCOLOR Color : 리셋 후 버퍼의 색상
0.0f, // float Z
0 // DWORD Stencil
);
if (FAILED(hResult))
{
// Clear 실패 시 각종 조치
}
DirectX 9.0을 시작하기 위해 객체의 생성을 하고 창을 닫을 때의 해제는 다음과 같이 한다. DirectX 객체는 COMComponent Object Model으로 제공되므로 C++ 언어를 사용한다.
Direct3DCreate9, IDirect3D9::Release
DirectX 9.0의 객체 생성은 Direct3DCreate9 함수를 실행한다. 성공하면 LPDIRECT3D9(struct IDirect3D9 *) 형식의 객체를 반환하고, 실패하면 NULL을 반환한다.
/* C++ Source */
#pragma comment(lib, "d3d9.lib") // DirectX 9.0 라이브러리를 링크
#include <Windows.h> // Windows API에서 제공되는 기본 자료형들을 불러오기
#include <d3d9.h> // DirectX 9.0 API를 불러오기
LPDIRECT3D9 lpDirect3D = (LPDIRECT3D9)NULL; // DirectX 9.0 객체
/* 창을 생성하거나 그래픽을 리셋하는 단계에서 */
lpDirect3D = Direct3DCreate9(D3D_SDK_VERSION); // DirectX 9.0 버전으로 객체를 생성한다.
if (!lpDirect3D)
{
// 생성에 실패한 경우 실행 작업
}
/* 생성에 성공한 경우 실행 작업 */
DirectX 9.0 객체의 해제는 IDirect3D9::Release를 호출한다.
/* C++ Source */
#pragma comment(lib, "d3d9.lib") // DirectX 9.0 라이브러리를 링크
#include <Windows.h> // Windows API에서 제공되는 기본 자료형들을 불러오기
#include <d3d9.h> // DirectX 9.0 API를 불러오기
LPDIRECT3D9 lpDirect3D = (LPDIRECT3D9)NULL; // DirectX 9.0 객체
/* 창을 닫거나 그래픽을 종료하는 단계에서 */
lpDirect3D->Release(); // 객체 해제
/* 이후 작업 */
실제 코드에 적용
다음은 MFC에서 DirectX 객체를 생성하고 해제하는 예이다. CTestFrame은 CWndFrame에서 상속된 클래스이고 m_pDirect3D는 사전에 클래스 헤더에 선언된 멤버 변수이다.
/* MFC Source */
int CTestFrame::OnCreate(LPCREATESTRUCT lpCreateStruct)
{
HRESULT hResult = NULL;
int ret = 0;
TRACE(TEXT("[SUCCESS] CTestFrame::OnCreate called.\n"));
ret = CFrameWnd::OnCreate(lpCreateStruct);
if (ret != 0)
{
TRACE(TEXT("[FAILURE] CTestFrame::OnCreate.\n"));
return ret;
}
this->m_pDirect3D = Direct3DCreate9(D3D_SDK_VERSION);
if (this->m_pDirect3D == NULL)
{
TRACE(TEXT("[FAILURE] Direct3DCreate9.\n"));
return ret;
}
return ret;
}
void CTestFrame::OnDestroy()
{
TRACE(TEXT("[SUCCESS] CTestFrame::OnDestroy called.\n"));
if (this->m_pDirect3D != NULL)
{
this->m_pDirect3D->Release();
}
CFrameWnd::OnDestroy();
}
MFC 프로그램의 실행 결과 (아직 빈 창)OnCreate와 OnDestroy이 실행된 흔적
어댑터 종류 열거하기
DirectX를 사용하기 위해 컴퓨터에서 지원 가능한 그래픽 어댑터의 종류를 모두 가져오려면 CreateDXGIFactory 함수로 생성된 IDXGIFactory형 객체를 사용한다.
CreateDXGIFactory
/* C++ Source */
#pragma comment(lib, "d3d9.lib")
#pragma comment(lib, "dxgi.lib")
#include <d3d9.h>
#include <dxgi.h>
IDXGIFactory * pDXGIFactory = NULL;
IDXGIAdapter * pDXGIAdapter = NULL;
SIZE_T nDXGIAdapter = 0;
/* 창을 생성하거나 그래픽을 리셋하는 단계에서 */
hResult = CreateDXGIFactory
(
__uuidof(IDXGIFactory), // REFIID riid
reinterpret_cast<void **>(&pDXGIFactory) // void ** ppFactory
);
// 해당 인덱스 번호에 해당하는 어댑터가 없다면 DXGI_ERROR_NOT_FOUND를 반환하기 시작한다.
// 0번 어댑터부터 검색을 시작한다.
nDXGIAdapter = 0;
while (pDXGIFactory->EnumAdapter(nDXGIAdapter, &pDXGIAdapter) != DXGI_ERROR_NOT_FOUND)
{
nDXGIAdapter++; // 다음 번호의 어댑터를 확인한다.
}
실제 코드에 적용
그래픽 어댑터의 종류를 열거하는 위 코드를 응용하여 디버그 문자열로 그래픽 어댑터의 이름을 출력해보겠다.
/* MFC Source */
int CTestFrame::OnCreate(LPCREATESTRUCT lpCreateStruct)
{
HRESULT hResult = NULL;
int ret = 0;
TRACE(TEXT("[SUCCESS] CTestFrame::OnCreate called.\n"));
ret = CFrameWnd::OnCreate(lpCreateStruct);
if (ret != 0)
{
TRACE(TEXT("[FAILURE] OnCreate::OnCreate.\n"));
return ret;
}
// DirectX 객체를 생성
this->m_pDirect3D = Direct3DCreate9(D3D_SDK_VERSION);
if (this->m_pDirect3D == NULL)
{
TRACE(TEXT("[FAILURE] Direct3DCreate9.\n"));
return ret;
}
// 어댑터를 열거할 수 있는 IDXGIFactory 객체 생성
IDXGIFactory * pDXGIFactory = NULL;
hResult = CreateDXGIFactory
(
__uuidof(IDXGIFactory), // REFIID riid
reinterpret_cast<void **>(&pDXGIFactory) // void ** ppFactory
);
if (FAILED(hResult))
{
TRACE(TEXT("[FAILURE] CreateDXGIFactory.\n"));
return ret;
}
// 열거된 어댑터를 하나씩 배열에 보관
CArray<IDXGIAdapter *> dxgiAdapters;
IDXGIAdapter * pDXGIAdapter = NULL;
UINT i = 0;
while (TRUE)
{
hResult = pDXGIFactory->EnumAdapters
(
i++, // UINT Adapter
&pDXGIAdapter // IDXGIAdapter ** ppAdapter
);
if (hResult == DXGI_ERROR_NOT_FOUND) break;
dxgiAdapters.Add(pDXGIAdapter);
pDXGIAdapter = NULL;
}
// 배열에 보관된 어댑터의 이름을 디버그 문자열로 출력
for (INT_PTR i = 0; i < dxgiAdapters.GetCount(); i++)
{
DXGI_ADAPTER_DESC dxgiAdapterDesc;
TCHAR szBuffer[256];
dxgiAdapters[i]->GetDesc(&dxgiAdapterDesc);
TRACE(TEXT("[%2d] DXGI_ADAPTER_DESC.Description = %s\n"), i, dxgiAdapterDesc.Description);
}
// 사용이 끝난 어댑터 객체와 Factory 객체 해제
for (INT_PTR i = 0; i < dxgiAdapters.GetCount(); i++)
{
dxgiAdapters[i]->Release();
}
dxgiAdapters.RemoveAll();
pDXGIFactory->Release();
return ret;
}
본 컴퓨터에 장착된 그래픽 어댑터의 종류가 열거되는 것을 확인할 수 있다. 다만 [장치 관리자]에는 없는 Microsoft Basic Render Driver가 마지막에 있을 수도 있다.
컴퓨터에 장착된 그래픽 어댑터의 종류가 열거되는 결과
그래픽 어댑터의 색상 포맷의 지원 여부 확인
ARGB, RGB, HSV 등등의 색상 포맷을 사용하고자 할 때 그래픽 어댑터가 이를 지원하는지 여부를 확인한다. 여기서 XRGB는 RGB와 같은 것을 의미한다.
IDirect3D9::EnumAdapterModes
/* C++ Source */
#pragma comment(lib, "d3d9.lib")
#pragma comment(lib, "dxgi.lib")
#include <d3d9.h>
#include <dxgi.h>
LPDIRECT3D9 lpDirect3D = (LPDIRECT3D9)NULL; // DirectX 9.0 객체
UINT uAdapterMode = 0;
/* 창 또는 그래픽을 리셋하는 단계에서 */
uAdapterMode = lpDirect3D->GetAdapterModeCount
(
D3DADAPTER_DEFAULT, // UINT Adapter : 시스템 기본 어댑터를 사용하겠다
D3DFMT_X8R8G8B8 // D3DFORMAT Format : 픽셀당 32비트 색상 포맷을 쓴다. 단, 실제 사용되는 비트는 R, G, B 24비트이다.
);
본 컴퓨터의 경우 RGB(X8R8G8B8) 색상 포맷으로 43가지의 어댑터 모드를 지원한다.
IDirect3D9::EnumAdapterModes
IDirect3D9::EnumAdapterModes 함수는 그래픽 어댑터가 지원 가능한 모드를 상세히 열거하는 함수이다. 지원 모드는 D3DDISPLAYMODE 구조체를 통해 반환된다.
/* C++ Source */
UINT uAdapterMode = this->m_pDirect3D->GetAdapterModeCount
(
D3DADAPTER_DEFAULT, // UINT Adapter
D3DFMT_X8R8G8B8 // D3DFORMAT Format
);
for (UINT i = 0; i < uAdapterMode; i++)
{
D3DDISPLAYMODE d3dDisplayMode;
hResult = this->m_pDirect3D->EnumAdapterModes
(
D3DADAPTER_DEFAULT, // UINT Adapter
D3DFMT_X8R8G8B8, // D3DFORMAT Format
i, // UINT Mode : 어댑터에 대해 얻고자 하는 모드의 번호
&d3dDisplayMode // D3DDISPLAYMODE * pMode : 반환 정보
);
}
실제 코드에 적용
기본 어댑터(D3DADAPTER_DEFAULT)의 RGB 컬러(D3DFMT_X8R8G8B8)로 몇 가지의 어댑터 모드가 지원 가능한지 조회해 보겠다. 실행 결과는 컴퓨터마다 상이하다.
/* MFC Source */
int CTestFrame::OnCreate(LPCREATESTRUCT lpCreateStruct)
{
HRESULT hResult = NULL;
int ret = 0;
TRACE(TEXT("[SUCCESS] CTestFrame::OnCreate called.\n"));
ret = CFrameWnd::OnCreate(lpCreateStruct);
if (ret != 0)
{
TRACE(TEXT("[FAILURE] OnCreate::OnCreate.\n"));
return ret;
}
this->m_pDirect3D = Direct3DCreate9(D3D_SDK_VERSION);
if (this->m_pDirect3D == NULL)
{
TRACE(TEXT("[FAILURE] Direct3DCreate9.\n"));
return ret;
}
IDXGIFactory * pDXGIFactory = NULL;
hResult = CreateDXGIFactory
(
__uuidof(IDXGIFactory), // REFIID riid
reinterpret_cast(&pDXGIFactory) // void ** ppFactory
);
if (FAILED(hResult))
{
TRACE(TEXT("[FAILURE] CreateDXGIFactory.\n"));
return ret;
}
UINT uAdapterMode = this->m_pDirect3D->GetAdapterModeCount
(
D3DADAPTER_DEFAULT, // UINT Adapter
D3DFMT_X8R8G8B8 // D3DFORMAT Format
);
TRACE(TEXT("[SUCCESS] GetAdapterModeCount = %u\n"), uAdapterMode);
for (UINT i = 0; i < uAdapterMode; i++)
{
D3DDISPLAYMODE d3dDisplayMode;
hResult = this->m_pDirect3D->EnumAdapterModes
(
D3DADAPTER_DEFAULT, // UINT Adapter
D3DFMT_X8R8G8B8, // D3DFORMAT Format
i, // UINT Mode : 어댑터에 대해 얻고자 하는 모드의 번호
&d3dDisplayMode // D3DDISPLAYMODE * pMode : 반환 정보
);
if (SUCCEEDED(hResult))
{
TRACE(TEXT("[%2u] EnumAdapterModes\n"), i);
TRACE(TEXT(" D3DDISPLAYMODE.Format = %x\n"), d3dDisplayMode.Format);
TRACE(TEXT(" D3DDISPLAYMODE.RefreshRate = %u\n"), d3dDisplayMode.RefreshRate);
TRACE(TEXT(" D3DDISPLAYMODE.Width = %u\n"), d3dDisplayMode.Width);
TRACE(TEXT(" D3DDISPLAYMODE.Height = %u\n"), d3dDisplayMode.Height);
}
}
return ret;
}
RGB 모드로 지원 가능한 43개의 어댑터 모드에 대해 프레임 속도와 해상도가 열거되고 있다.
어댑터 장치의 성능 확인하기
장치의 성능 중에서 특히 자주하게 확인할 것은 하드웨어로 버텍스 처리가 가능한지 여부이다. 하드웨어에서 버텍스 프로세싱이 가능하면 그대로 이용하고, 그렇지 않을 경우 소프트웨어(CPU)로 버텍스를 연산해야 한다. 이 경우 체감 속도가 다소 느릴 수 있다.
IDirect3D9::GetDeviceCaps
IDirect3D9::GetDeviceCaps 함수가 반환하는 구조체인 D3DCAPS9의 DevCaps 멤버를 통해 확인이 가능하며 D3DDEVCAPS_HWTRANSFORMANDLIGHT 비트의 설정 여부로 하드웨어의 버텍스 처리 성능을 판단할 수 있다.
/* C++ Source */
D3DCAPS9 d3dCaps;
ZeroMemory(&d3dCaps, sizeof(D3DCAPS9));
hResult = this->m_pDirect3D->GetDeviceCaps
(
D3DADAPTER_DEFAULT, // UINT Adapter
D3DDEVTYPE_HAL, // D3DDEVTYPE DeviceType
&d3dCaps // D3DCAPS9 * pCaps
);
if (FAILED(hResult))
{
// 장치 정보 얻기에 실패시 조치
}
if ((d3dCaps.DevCaps & D3DDEVCAPS_HWTRANSFORMANDLIGHT) != 0)
{
// 하드웨어로 버텍스 처리가 가능하다.
}
else
{
// 소프트웨어로 버텍스 처리가 가능하다. (속도 느림)
}
실제 코드에 적용
실제 코드에 적용하여 보겠다.
/* MFC Source */
int CTestFrame::OnCreate(LPCREATESTRUCT lpCreateStruct)
{
HRESULT hResult = NULL;
int ret = 0;
TRACE(TEXT("[SUCCESS] CTestFrame::OnCreate called.\n"));
ret = CFrameWnd::OnCreate(lpCreateStruct);
if (ret != 0)
{
TRACE(TEXT("[FAILURE] OnCreate::OnCreate.\n"));
return ret;
}
this->m_pDirect3D = Direct3DCreate9(D3D_SDK_VERSION);
if (this->m_pDirect3D == NULL)
{
TRACE(TEXT("[FAILURE] Direct3DCreate9.\n"));
return ret;
}
D3DCAPS9 d3dCaps;
ZeroMemory(&d3dCaps, sizeof(D3DCAPS9));
hResult = this->m_pDirect3D->GetDeviceCaps
(
D3DADAPTER_DEFAULT, // UINT Adapter
D3DDEVTYPE_HAL, // D3DDEVTYPE DeviceType
&d3dCaps // D3DCAPS9 * pCaps
);
if (FAILED(hResult))
{
TRACE(TEXT("[FAILURE] IDirect3D9::GetDeviceCaps = %u"), hResult);
return ret;
}
if ((d3dCaps.DevCaps & D3DDEVCAPS_HWTRANSFORMANDLIGHT) != 0)
{
TRACE(TEXT("[DEVCAPS] D3DCAPS9.DevCaps has D3DDEVCAPS_HWTRANSFORMANDLIGHT\n"));
}
else
{
TRACE(TEXT("[DEVCAPS] D3DCAPS9.DevCaps not D3DDEVCAPS_HWTRANSFORMANDLIGHT\n"));
}
return ret;
}
실행 결과 하드웨어 버텍스 처리가 가능하면 위와 같이 출력될 것이다.
IDirect3DDevice9 객체 생성
그래픽 어댑터를 호출할 수 있는 장치 인터페이스인 IDirect3DDevice9 객체를 생성한다. 앞서 확인한 각종 하드웨어 사항과 본인이 선정한 색상 포맷, 기타 필요한 사항들을 조합하여 하나의 컨텍스트를 만든 다음 이를 IDirect3D9::CreateDevice함수에 D3DPRESENT_PARAMETERS 구조체로 전달한다.