----------------------------------------------------------------------------------------------
----------------------------------------------------------------------------------------------
- 전병선의 Component Development with Visual C++ with ATL
- 제1장 왜 COM인가
- 제2장 COM 컴포넌트 사용
- 제3장 COM 객체 구현
- 제4장 인-프로세스 서버 COM 컴포넌트
- 제5장 아웃-오브-프로세스 서버 COM 컴포넌트
- 제6장 Visual C++ COM 컴파일러
- 제7장 Active Template Library 개요
- 제8장 ATL 윈도우 클래스와 WTL
- 제9장 자동화와 이중 인터페이스
- 제10장 커넥션 포인트와 이벤트
- 제11장 COM 컴포넌트 재사용
- 제12장 컬렉션 구현
전병선의 Component Development with Visual C++ with ATL
제1장 왜 COM인가
COM등장
1. 메인 프레임 시대 (Mainframe)
2. 클라이언트/서버 시대 (Client/Server)
- 메인 프레임이 처리하던 일을 여러대의 비교적 작은 규모의 서버 컴퓨터에 나누어서 처리하는 방법
예) 프린터 서버, 파일 서버, 데이터베이스 서버 -> 허브 -> 각 데스크톱 PC
- 데이터베이스 서버 -> 인터페이스와 업무 로직을 수행하는 클라이언트
3. 3계층 클라이언트/서버 시대 (3-tier Client/Server)
- 업무 로직이 변경될 때마다 클라이언트를 변경하기는 힘들다.
클라이언트와 서버 사이에 업무 로직을 수행하는 애플리케이션 서버(application server)를 둔다.
예) 사용자 서비스 계층(클라이언트) -> 비지니스 서비스 계층(애플리케이션 서버) -> 데이터 서비스 계층(데이터 베이스 서버)
이에 따라 분산 클라이언트 서버(distributed client/server) 환경에서 애플리케이션을 유기적 통합하고
커뮤니케이셔날 것인가가 주요 관심시가 된다. 컴포넌트 기반 분산 객체 기술(component-based distributed object technology)이 등장.
이러한 컴포넌트 기반 기술에는
OMG(object management group)의 CORBA(common object request broker architecture)와
마이크로소프트의 COM(component object model)/DCOM(distributed component object model),
선 마이크로시스템즈의 EJB(enterprise java beans)가 있다.
EJB는 CORBA를 지원하므로 마이크로소프트 vs anti-마이크로소프트
윈도우 운영체제 기반은 COM+로 발전된 COM/DCOM vs 유닉스나 다중 플랫폼(multi platform)환경에서는 EJB가 CORBA와 함께 주류를 이룬다.
컴포넌트 소프트웨어 조건
1. 서로 다른 언어로 만든 프로그램을 연결시켜야 한다.
결국 표준적인 방법 언어 독립적(language-independent)이어야 한다.
2. 컴포넌트가 설치되고 실행되는 위치에 관계없이 같은 방법으로 컴포넌트를 사용할 수 있어야 한다.
위치 투명성(location transparency) DLL이냐 EXE이냐 로컬이냐 리모트나 상관 없어야 한다.
3. 컴포넌트 버전 관리가 쉬어야 한다.
ORB(object request broker)
객체에 서비스 요청을 중개하는 중개자.
결국 서버와 클라이언트가 서로 커뮤니케이션을 하기 위한 공통적인 서비스를 제공해 주는 역할.
인터페이스 정의 언어(IDL, interface definition language)
COM과 DCOM은 다른 분산 객체 기술과 같이 인터페이스와 IDL을 통해 언어독립적인 특징을 가진다.
COM의 역사
OLE부터..
object linking and embedding의 약자로 객체를 연결하고 포함하는 기술.
결국 엑셀이나 파워포인트 프리젠테이션 자료를 섞어 포함할수 있는 기술.
DDE(dynamic data exchange)를 사용했지만 문제가 많아서 사장
OLE 2.0에서는 DDE 대신 COM이 도입
결국 COM은 OLE 도큐먼트(OLE Document)와 함께 들어온 기술
이것을 OLE 자동화(OLE Automation)라고 하는데 기존의 VBX(visual basic custom control)을 대체하는 OLE 컨트롤(OLE Control)이라는 기술도 OLE 자동화 기술화 함께 시작.
결국 대박.
OLE컨트롤은 비주얼 베이직의 폼과 같은 컨테이너(container)안에 포함되어 시각적인 사용자 인터페이스를 제공. OLE컨트롤에는 자동화 기술의 속성과 메서드외에 이벤트를 발생 시키는 기능도 추가.
하지만 이런 부가적인 기능을 제공하기 위해서는 IDispatch와 같은 인터페이스를 제공해야 하는데
가상 함수 테이블에 직접 접근 가능한 언어들에는 부가적으로 붙는 내용.
그래서 COM컴포넌트가 디스패치 인터페이스와 커스텀 인터페이스를 함께 제공되도록 권고되었다.
이것을 이중 인터페이스(dual interface) 라고 한다.
이후 비주얼 베이직 4.0 엔터프라이즈 에디션(기업판) 에서는 리모트 OLE 자동화(Remote OLE Automation)이란 기술을 선보이며 3계층 클라이언트/서버 시대를 시작.
그후 윈도 NT 4.0에서 분산 COM (DCOM)이 등장하면 본격적인 3계층 클라이언트/서버 시대 개막.
기업 애플리케이션에 필수적인 트랜잭션(transaction)처리등 여러 동시 사용자를 수용하기 위한 컴포넌트 관리등 많은 서비스를 컴포넌트에 직접 구현하기 위해 마이크로 소프트 트랜잭션 서버(MTS, microsoft transaction server)등장. 그리고 MTS가 윈도우 운영체제와 완전히 통합된 형태로 등장하게 된 것이
바로 COM+
이 COM과 MTS 그리고 COM+는 마이크로소프트의 윈도우 DNA 아키텍처의 핵심.
이들을 기반으로 웹 애플리케이션과 클라이언트/서버 애플래케이션을 통합하는 3계층 컴포넌트 기반의
분산 애플리케이션의 개발 모델을 제시하는 것이 윈도우DNA아키텍처.
또한 기업내 퍼져있는 모든 정보에 접근할 수 있도록 하는 마이크로소프트 데이터 접근 전략이 UDA
(universal data access) 아키텍처의 핵심인 OLEDB도 COM기반, 윈도우는 COM+기반..
인터페이스
interface IUnknown {
|
C++ 언어에서 가상 함수를 포함하는 클래스에서 생성된 인스턴스는 가상 함수 테이블 포인터를 갖게 된다.
이 가상 함수 테이블 포인터를 인터페이스 포인터라 하며, 인터페이스 포인터를 갖는 클라이언트는
가상 함수 테이블을 통해 멤버 함수를 호출할 수 있다.
COM은 인터페이스를 통하여 다형성(polymorphism)을 지원하게 되는 것이다.
그러나 COM은 상속성(inheritance)는 지원하지 않는다.
문제점은 구현된 코드를 다시 사용할 수 없기 때문에 이 문제는 포함(containment)과 통합(aggregation)이라는 해결 방법을 제공한다. 11장으로
COM 컴포넌트
COM 컴포넌트를 COM 서버(COM Server)또는 COM 객체 서버(COM object server)라고 부른다.
COM 컴포넌트의 위치에 따라 인-프로세스 서버(in-process server)와 아웃-오브-프로세스 서버(out-of-process server)로 구분된다.
인-프로세스 서버는 클라이언트와 같은 프로세스 안에서 생성되고 실행되는 서버
아웃-오브-프로세스 서버는 클라이언트와 다른 별도의 프로세스가 생성되어 실행되는 서버
따라서
인-프로세스 서버는 DLL로 구성되며
아웃-오브-프로세스 서버는 EXE로 구성.
이 아웃-오브-프로세스는 다시 로컬 서버(local server)와 리모트 서버(remote server)로 구분
로컬 서버는 서버가 클라이언트와 같은 시스템에서 실행되는 것을 뜻하며
리모트 서버는 서로 다른 시스템 즉, 네트워크를 통해 커뮤니케이션을 하는 것을 말한다.
리모트 서버를 대리 프로세스(surrogate process, dllhost.exe)안에서 실행되는 인-프로세스 서버로 구현할 수도 있지만 일반적으로 아웃-오브-프로세스 서버로 구현하는 것이 일반적이다.
GUID
OSF(open software foundation)가 제공하는 GUID(globally unique identifier)을 사용한다.
GUID(globally unique identifier)를 사용한다. GUID는 128비트(16바이트)의 크기를 갖는 구조체로 전세계적으로 시간과 장소에 관계없이 고유하다고 보장할 수 있는 값을 나타내는 식별자이다.
인터페이스 GUID를 IID(interface identifier)라고 하고 COM객체의 GUID를 CLSID(class identifier)라고 한다.
GUID는 UUID(universally unique identifier)라고도 하며 guiddef.h헤더 에 정의 되어 있다.
typedef struct _GUID {
unsigned short Data2; unsigned short Data3; unsigned char Data4[8]; |
윈도우에서는 GUID를 생성하는 두가지 툴을 제공한다.
콘솔 명령어 행에서 사용하는 UUIDGEN.EXE와 GUI를 제공하는 GUIDGEN.EXE이다.
이들 툴은 COM 라이브러리인 CoCreate?GUID함수를 호출하고 Win32 RPC API함수인 UUIDCreate를
호출하여 내부 알고리즘으로 GUID를 생성하여 준다.
GUID구조체에는 서로 다른 GUID를 비교하는 operator == 함수가 정의어 비교할 수 있다.
그리고 같은 기능을 하는 IsEqual?GUID(), IsEqual?IID(), IsEqual?CLSID()등이 있다.
그리고 구조체의 크기가 크기때문에 레퍼런스 형태로 매개변수를 사용한다.
GUID를 문자열로 변환하는 StringFrom?GUID2()도 제공된다.
닷넷으로 넘어가면서 COM을 구현하는 일은 완전히 Visual C++과 ATL의 몫이 되었다.
제2장 COM 컴포넌트 사용
COM 컴포넌트 등록
먼저 사용하기 전에 시스템 레지스트리에 등록이 되어 있어야 한다.
DLL파일은 (in-process server) regsvr32.exe 툴을 사용하면 쉽게 등록할 수 있다.
EXE파일의 경우 (out-of-process server) 파일이름.exe /regserver 옵션을 통해 등록을 한다.
COM 컴포넌트 생성 과정
1. COM 라이브러리를 초기화 한다.
2. COM 객체의 CLSID를 구한다.
3. COM 객체의 인스턴스를 생성한다.
4. COM 객체가 지원하는 인터페이스 포인터를 구하여 해당 인터페이스가 제공하는 메서드를 호출함으로 COM객체를 사용한다.
5. COM 객체의 상요이 끝나면 COM라이브러리 초기화를 해지한다.
COM 라이브러리 초기화
COM라이브러리는 COM객체에 대한 서비스 요청을 중개하는 ORB(object request broker)이며 COM을 사용하는 모든 애플리케이션들이 유용하게 사용할 수 있는 컴포넌트 관리 서비스를 제공한다.
COM라이브러리는 모든 COM 컴포넌트에 필요한 몇가지 필수 API를 제공하며 Co-의 접두어를 가진다.
objbase.h 헤더 파일에 선언되어 있다.
HRESULT CoInitializeEx?(
DWORD dwCoInit? // COINIT 열거형 값. 당분가 COINIT_APARTMENTTHREADED를 지정하면 된다고.... |
이 함수를 사용하기 위해서는 DCOM이 사용가능한 윈도우NT4.0 이상이나 윈98이상
윈95에 DCOM이 깔려진 경우 이다.
선행 매크로는
#define _WIN32_DCOM 이거나 #define _WIN32_WINNT 0x0400 |
DCOM 이전에는 CoInitialize?()함수가 사용되었다.
내부적으로 COINIT_APARTMENTTHREAD를 호출하지만 지금은 사용 안한다.
HRESULT
HRESULT 함수 호출 결과를 나타내는 32비트 값이다.
성공시는
S_OK 0x00000000L 성공
실패시는
에러코드 | 값 | 설명 |
E_NOTIMPL | 0x800040001L | 인터페이스 메서드가 구현되지 않음 |
E_OUTOFMEMORY | 0x8007000EL | 요청한 메모리를 할당하지 못함 |
E_INVALIDARG | 0x80070057L | 인터페이스 메서드에 잘못된 인수가 넘어옴 |
E_NOINTERFACE | 0x80004002L | 요청한 인터페이스를 지원하지 않음 |
E_ACCESSDENIED | 0x80070005L | 요청한 컴포넌트에 접근할 수 없음 |
SUCCESSED와 FAILED매크로로 단순한 성공 실패를 구별할 수도 있다.
COM 객체의 CLSID 구하기
1. OLE/COM 개체 뷰어(OLE/COM object viewer)를 사용
2. 프로그램ID(ProgID) 하지만 중복될 가능성이 존재한다.
참고로 엑셀의 ProgID는 Excel.Application.10이다. 버저 번호를 생략할 수 있으며
버전 독립적 ProgID라고 한다. Excel.Application과 같이
3. 레지스트리에서는 HKEY_CLASSES_ROOT 키 밑에 존재 한다.
CLSIDFromProg?ID함수
HRESULT CLSIDFromProg?ID(
LPCLSID pclsid // 반환된 CLSID값을 저장할 CLSID 포인터 |
COM 에서의 문자열 사용
내부적으로 유니코드(unicode)사용. 유니코드가 아닐경우 변환을 해주어야 한다.
앞의 LPOLESTR와 같은 경우 싹다 유니코드 결국 wchar_t 형이다.
WIN32API와 C 런타임 라이브러리에는 ANSI문자열과 유니코드 문자열의 상호 변환 함수가 존재한다.
MultiByteToWideChar?함수는 ANSI문자열을 UNICODE로 변환
WideCharToMultiByte?함수는 역변환하여주는 Win32API
mbstowcs함수는 ANSI문자열을 UNICODE로 변환
wcstombs함수는 역변환 하여주는 C 런타임 라이브러리
유니코드 사용시
#define UNICODE
COM 객체 인스턴스 생성
STDAPI CoCreateInstance?(
LPUNKNOWN pUnkOuter?, // 외부 COM객체의 IUnknown 포인터. 일반적으로 null, 11장 컴포넌트 재사용에 사용 DWORD dwClsContext?, // 서버 컨텍스트, 일반적으로 CLSCTX_ALL사용시 문제 없음. 레지스트리에서 COM 컴포넌트를 찾는 위치(in-process > out-of-process 순) REFIID riid, // 요청 인터페이스의 IID LPVOID* ppv // 리턴될 인터페이스 포인터 |
CLSID로 식별되는 COM객체의 인스턴스를 생성하고 해당 COM 객체로부터 IUnknown 인터페이스 포인터를 리턴하여줌. 이 함수는 내부적으로 CoGetClassObject?() API를 호출하고 clsid와 dwClsContext? 인수값을 넘겨준다.
STD CoGetClassObject?( ); |
결국
// CoCreateInstance?() 내부에서는 IClassFactory?* pClassFactory? = NULL; pClassFactory?->CreateInstance?(NULL, riid, ppv); |
이 함수 내부에서는 시스템 프로세스에 rpcss.dll로 구현된 SCM(service control manager)에게
COM 컴포넌트를 찾아 로드하고 SCM이 요청한 COM 컴포넌트를 찾아 로드한 후에는 SCM은 물러나고
클라이언트와 COM 컴포넌트는 직접 커뮤니케이션 한다.
구체적으로는 먼저 CLSID에 해당하는 COM 객체 정보를 레지스트리에서 찾고 dwClsContext?에 해당하는
CLSCTX_INPROC_SERVER 레지스트리에서 CLS문자열 서브키 밑에 있는 InprocServer32?서브키에서 COM 컴포넌트의 실제 경로명을 구하고 CLSCTX_LOCAL_SERVER 이라면 LocalServer32? 서브키에서
실제 경로명을 구한다.
위치 투명성이 확실이 제공된다면 CLSCTX_ALL을 사용해 먼저 InprocServer32?에서 찾고 없으면 LocalServer32?에서 찾는다. 또한 내부적으로 CoLoadLibrary?() API 함수를 호출한다.
HINSTANCE CoLoadLibrary?( { } |
// CoGetClassObject?() 내부에서는 HINSTANCE helloDll = ::CoLoadLibrary?(L"helloserver.dll", TRUE); // typedef HRESULT (__stdcall *PFNDLLGETCLASSOBJECT)(REFCLSID clsid, REFIID riid, void **ppv) |
결국 DLL이나 EXE로 존재하는 하는 파일에서 DllGetClassObject?라는 함수 포인터를 얻어와 riid에
해당하는 IClassFactory? 인터페이스를 얻어오는 형식이다.
이렇게 얻어온 IClassFactory? 인터페이스에서 CreateInstance?() 메서드를 호출하여 자신에 해당하는
(클라이언트가 요청한) COM 객체 인스턴스를 넘겨주게 된다.
이렇게 IUnknown 인터페이스 포인터를 구했다면 이제 COM 객체가 제공하는 서비스를 사용할 준비가
된 것이다.
IUnknown 인터페이스
COM인터페이스는 반드시 IUnknown에서 파생된다.
interface IUnknown {
|
CreateInstance?() 함수는 인터페이스로 변환 가능한 클래스를 생성시킨다. 그리고 해당 인터페이스로 업캐스팅하여 반환한다. 이렇게 사용하는 이점은 실제 객체가 바뀐다고 하더라도 일괄적으로 CreateInstance?()함수를 통해 사용자에게 사용법에 해당하는 인터페이스를 반환할 수 있다는 점이다.
여러 인터페이스를 지원하는 클래스가 존재한다면 하나의 인터페이스에서 다른 인터페이스의 호출은 사실상 막혀 있는 셈이다. 상속 받는 객체에서 같이 존재할뿐 사실상 다른 인터페이스는 서로의 존재를 알 수 있는 방법이 없다.
그러므로 다시 객체에 접근을 하여 다른 인터페이스를 호출 할 수 있다.
그게 바로 QueryInterface?()의 역할이다.
그리고 객체는 하나이지만 사용되는 인터페이스의 개수는 여러개가 될 수 있다.
하나의 인터페이스에서 인터페이스를 사용하고 종료를 하면서 객체를 소멸시킨다면..
그외 인터페이스를 통해 접근하고 있는 다른 객체들은 Access Violation과 같은 오류에 빠진다.
그럼으로 객체 카운트가 있는 스마트 포인터를 사용하여 객체 카운터가 0가 되는 시점에 실제 객체를
삭제 시킬수 있도록 한다. 이때 사용되는 함수가 AddRef?()와 Release()인 것이다.
위의 COM 컴포넌트를 만들때는 CLSID를 참조해서 만들면 되지만
Interface를 만들때는 IID가 레지스트리에 존재하지 않는다.
그렇기 때문에 COM 컴포넌트의 인터페이스를 제공할 때는 IID까지 같이 제공해야 한다.
일반적으로 COM컴포넌트가 제공하는 인터페이스는 IDL구문으로 정의하고 MIDL(microsoft IDL) 컴파일러를 사용하여 컴파일한다. MIDL 컴파일러는 xxxh.h 헤더파일에 C++ 클래스로 정의한 코드를 제공하며 xxx.i_c 파일에 인터페이스의 IID를 제공해 준다. 실제로 COM 컴포넌트에 인터페이스를 만들려고 한다면
IUnknown을 통해 다시 QueryInterface?()를 호출할 필요는 없다.
CoCreateInstance?()를 호출할때 실제 IID를 통해 인터페이스를 구하는 편이 낫다.
COM 컴포넌트 사용시 컴포넌트 내부에서 생성된 메모리의 경우 결국 클라이언트가 해지를 해주어야 한다.
같은 프로세스라면 낫지만 아웃-오브-프로세스 서버라면 이런 방법으로 안되고 반드시 COM라이브러리가
제공하는 CoTaskMemFree?()와 같은 함수를 사용하여 해지 하여야 한다.
반대로 컴포넌트 내부에 메모리를 할당하는 CoTaskAlloc?()도 존재한다.
COM 라이브러리 초기화 해제
void CoUninitialize?(); |
제3장 COM 객체 구현
COM 구현은 어렵다.
하지만 맨바닥부터 할 필요는 없으니 그나마 낫다.
인터페이스는 가상 함수 테이블(virtual function table) 형식의 메모리 구조를 정의하는 것이 된다.
COM/DCOM은 IDL과 인터페이스를 통해 컴포넌트가 언어 독립적인 특징을 갖도록 한다.
표준 인터페이스
두가지 종류가 있다.
1. 표준 인터페이스(standard interface)
대표적인 경우로 IUnknown과 IClassFactory?가 있고 이외에 OLE, ActiveX, OLE DB와 같은 COM기반 기술에서 사용되는 IDispatch, IConnectionPointContainer?, IConnectionPoint?, IOleControl?기타 등등이 있다.
이들 표준 인터페이스 IDL 파일은 마소에의해 미리 정의되어 SDK(software development kit)안에서 제공되므로 굳이 IDL을 정의할 필요없다. 예를 들면 IUnknown 인터페이스는 unknwn.idl 파일에 다음과 같이 정의 되어 있다.
[
object, uuid(00000000-0000-0000-C000-000000000046), pointer_default(unique) interface IUnknown |
2. 커스텀 인터페이스(custom interface)
개발자가 자신의 필요로 정의하여 사용하는 인터페이스를 말한다.
COM에서 사용하는 IDL은 OSF의 RPC에서 사용하던 IDL을 확장시킨것으로 C++과 매우 흡사한 구문을 제공한다.
[
uuid(783964A0-A074-11d1-B20C-0060970A3516) interface IHello : IUnknown |
각 인터페이스는 헤더와 본체로 구성된다.
헤더에는 전체 인터페이스에 해당하는 어트리뷰트 정보가 포함된다.
어트리뷰트(attribute)는 인터페이스의 전체 또는 데이터와 메서드가 갖는 특징을 지정하는 키워드로서
대괄호(, ?)안에 정의 된다.
IDL목적 중의 하나는 COM이 클라이언트와 COM 객체 사이에 메서드의 인수값을 전달하는데 필요한 정보를 제공하는데 있으며, 이것의 위의 예제의 in, out, string 어트리뷰트의 목적이다.
정확히 이야기 하자면 in, out, string등의 어트리뷰트는 MIDL 컴파일시 생성되는 프록시(proxy)/스텁(stub) 코드의 최적화에 필요한 특성 정보를 제공한다.
즉, 메서드가 프로세스 사이에 마샬링(marshaling)되는 필요한 충분한 정보를 제공함으로 필요한 데이터만 마샬링되어 최적화가 목적이다. in은 readonly 값으로 사용되고 out은 클라이언트로 반환되며 string은 null문자로 끝나는 문자열이 전달된다는 뜻으로 보면된다.
MIDL 컴파일러
midl.exe FILENAME.idl |
FILENAME.h // c와 c++에서 사용할 수 있는 인터페이스 클래스 코드 FILENAME_i.c // 모든 인터페이스의 GUID FILENAME_p.c // 커스텀 인터페이스의 마샬링 코드가 정의된 프록시/스텁 코드가 저장된다. dlldata.c |
midl /h FILENAME_h.h FILENAME.idl |
아까 보았던 IDL 코드의 컴파일후 인터페이스 클래스의 예이다.
// Hello_h.h MIDL_INTERFACE("783964A0-A074-11d1-B20C-0060970A3516") IHello : public IUnknown {
|
몇가지 매크로가 정의 되엉 있는데
#define MIDL_INTERFACE(x) struct DELCLSPEC_UUID(x) DECLSPEC_NOVTABLE #define STDMETHODCALLTYPE __stdcall #define DECLSPEC_UUID(x) __declspec(uuid(x)) #define DECLSPEC_NOVTABLE __declspec(novtable) |
결국 확장시키면
// Hello_h.h struct __declspec(uuid(783964A0-A074-11d1-B20C-0060970A3516)) __declspec(novtable) IHello : public IUnknown {
|
__declspec 비주얼C++ 확장 키워드가 uuid 애트리뷰트화 함께 지정되면 해당 인터페이스 클래스에 uuid 애트리뷰트의 인수에 지정된 GUID가 부착된다. 따라서 나중에 __uuidof 비주얼C++ 확장 키워드를 사용하여 인터페이스 클래스에 부착된 GUID를 구할 수 있게 되는 것이다.
나머지 확장 키워드는 6장에 계속
novtable 애트리뷰트의 의미는 순수 가상함수만 있는 추상 클래스에서는 가상 함수 테이블이 포함될 필요가 없다는 이야기. 결국 메모리 공간을 가질 필요가 없고 생성자나 소멸자의 역활 또한 막는다.
__stdcall의 경우 Win32 API들에 광범위하게 사용되는데 결국 callee에서 스스로 스택을 제거하는 것을의미한다. 결국 제거할 스택을 계산하는 시간과 용량을 줄일 수 있다. (가변인자를 사용하는 C의 printf문과 같은 경우 __cdecl는 함수 콜하는 쪽에서 인자가 넘어가는 갯수를 계산하여 넘겨주면 된다. unstack하면 되는 것이다.) 결국 다 효율이다.
COM 객체 클래스 구현
인터페이스 포함 방법
간단히 말하자면 인스턴스를 생성할 클래스는 IUnknown을 상속 시키고, 인스턴스 클래스를 다시 상속 받는 이너 클래스를 만들어 멤버로 포함시킨다.
그리고 그 멤버에 friend 설정을 하여 내부 멤버를 참조할 수 있도록 만들어 사용한다.
HRESULT STDMETHODCALLTYPE CHello::CImplIHello::QueryInterface?(REFIID riid, LPVOID *ppv) {
... HRESULT STDMETHODCALLTYPE CHello::CImplIHello::sayHello(...)
|
인터페이스 상속 방법
일반적인 상속과 같다. QueryInterface?()호출시 riid에 따라 this 포인터를 형변환으로 반환하여 주면 된다.
IUnknown 인터페이스를 상속받는 IHello 인터페이스를 다시 상속받는 CHello 클래스가 있다고 하면
인터페이스 |
가상 함수 테이블 영역 |
IUnknown | QueryInterface?(),AddRef?(),Relase()의 영역 |
IHello | 위의 메소드 + sayHello() 결국 모든 영역 |
다중 인터페이스 구현
인터페이스 포함 방법
inner 클래스를 여러개 만들고 각각 상속을 받는다.
인터페이스 상속 방법
위의 인터페이스 상속 방법과 상속 방법은 같은데
결국 형변환을 시킬때 C++ 컴파일러가 오프셋 계산을 훌륭하게 처리하느냐에 달려있다고 한다.
Visual C++ ATL(Active Template Library)도 이와 같은 상속 방법을 사용하여 COM 클래스를 구현한다.
클래스 팩토리의 구현
CoCreateInstacne?() 함수를 호출하면 내부적으로 CoGetClassObject?() 함수를 호출하고 이때 레지스트리에서 해당 COM 객체의 정보를 찾아 CoLoadLibrary?() 함수를 호출하고 해당 COM 객체를 구현한 인-프로세스 서버DLL을 로드한다.
로드된 Dll로 부터 DllGetClassObject?()익스포트 함수를 호출하고 생성하고자 하는 COM 객체의 CLSID를
넘겨준다. DllGetObject?()함수에서 인수로 넘어온 CLSID값에 대응되는 클래스 팩토리 COM 객체의 인스턴스를 생성하고 클래스 팩토리 COM객체의 IClassFactory? 인터페이스 포인터를 구하여 리턴해준다.
CoGetClassObject?()로 넘어오고 다시 리턴하여 CoCreateInstance?() 함수로 돌아가서 IClassFactory?
클래스의 CreateInstance?()를 호출함으로써 COM 객체의 인스턴스를 생성하게 된다.
위의 내용에서 알수 있듯이 결국 클래스 팩토리란 IClassFactory? 인터페이스를 구현하는 COM 객체이다.
MIDL_INTERFACE("00000001-0000-0000-C000-000000000046") IClassFactory? : public IUnknown {
// pUnkOuter?는 반드시 NULL(뒤의 11장에 COM 컴포넌트 재사용에서 나온다.),
// 만약 new 연산자를 통해 인스턴스 생성이 실패하게되면 E_OUTOFMEMORY를 반환하고 |
CreateInstance?() 메서드의 인자에는 CoCreateInstance?() COM 라이브러리 함수와 아주 유사한데
클라이언트가 생성하고자 하는 COM객체의CLSID가 매개변수로 전달되지는 않는다.
이 사실은 결국 클래스 팩토리는 생성되는 COM객체와 1:1 관계이라는 점이다.
제4장 인-프로세스 서버 COM 컴포넌트
DLLMain 함수
BOOL APIENTRY DllMain?(HINSTANCE hModule, DWORD dwReason, LPVOID lpReserved) {
{
return TRUE; |
DLL export 함수
다른 모듈에서 DLL에 정의된 함수를 사용하기 위해서는 DLL이 해당함수를 익스포트(export)해야하며, 이와 같이 DLL 이 제공하는 함수를 익스포트 함수라고 한다. 익스포트함수를 정의하는 방법에는 두가지가 있다.
1. __declspec(dllexport)라고 하는 마이크로 소프트 확장 키워드를 사용하는 방법
2. .def 확장자를 갖는 모듈 정의(module definition) 파일에 익스포트 함수에 대한 정보를 정의하여
링커(linker)에 넘겨 주는 방법이다.
그리고 C++언어를 사용한다면 익스포트 함수는 extern "C"로 선언되어야 한다.
extern "C" STDAPI DllGetClassObject?(REFCLSID rclsid, REFIID riid, LPVOID* ppv); extern "C" STDAPI DllCanUnloadNow?(void); extern "C" STDAPI DllRegisterServer?(void); extern "C" STDAPI DllUnregisterServer?(void); |
Or
LIBRARY HelloServer?.dll EXPORTS |
DllGetClassObject?()
이미 위에서 세번 정도 반복.
CoCreateInstance?()내부에 CoGetClassObject?()내부에 CoLoadLibrary?()내부에 LoadLibaray?()
그리고 DllGetClassObject?호출하고 COM의 CLSID를 넘겨준다. DllGetClassObject?()에서는 클래스 팩토리 COM 객체의 인스턴스를 생성하고 IClassFactory?인터페이스 포인터를 구하여 CoGetClassObject?()에
반환하여 주고 CoCreateInstance?()로 다시 반환되고 CreateInstance?()를 호출함으로 COM객체 인스턴스를 생성하게 된다.
DllCanUnloadNow?()
일반적인 DLL을 사용후 FreeLibrary?() API함수를 사용해 더 이상 사용하지 않는 DLL은 메모리에서 언로드 해야한다. COM 컴포넌트도 마찬가지로 더 이상 사용하지 않을때 CoFreeUnusedLibraries?() 함수를 호출하여 언로드 시켜야 한다. 이때 더 이상 참조 되지 않음을 확인하기 위해서 정해진 함수이다.
원형은 다음과 같고
STDAPI DllCanUnloadNow?(void); |
내부적으로 COM 객체 카운터를 두어 더 이상 사용되지 않은 카운터 0일때 언로드 가능의 의미인 S_OK를
리턴하도록 되어 있다.
DllRegisterServer?()
regsvr32.exe 툴을 이용하여 명령 프롬프트 창으로 입력을 할때 사용된다.
구현 내용은 COM 컴포넌트에 구현되어 있는 COM 객체들에 대한 정보를 시스템 레지스트리에 저장하는
코드를 구현하고 있다.
이때 RegCreateKey?(), RegSetValueEx?(), RegCloseKey?() API함수가 사용된다.
로드되는 DLL의 절대 경로명을 구하기 위해서는 GetModuleFileName?() API함수를 사용한다.
DllUnregisterServer?()
regsvr32.exe 툴의 /u 옵션으로 서버 COM 컴포넌트를 시스템 레지스트리에서 등록 해제할 때 사용된다.
RegDeleteKey?() API함수를 사용하면 된다.
인-프로세스 서버 COM 컴포넌트 선행 작업
#include <objbase.h> // COM 라이브러리 API 선언이 있다.
#pragma comment(lib, "ole32.lib")
미리 컴파일된 헤더 파일 사용 해제 // C 언어 파일의 경우
제5장 아웃-오브-프로세스 서버 COM 컴포넌트
마샬링(marshaling)의 이해
아웃 오브 프로세스 서버 COM 컴포넌트의 경우 클라이언트 프로세스 영역 밖에 인스턴스가 생성되기 때문에 서로 다른 프로세스간의 통신이 필요하다.
표준 마샬링 과정
COM 은 두 프로세스 사이의 커뮤니케이션 (Inter-Process Communication, IPC)수단으로
LPC(Local Procedure Call)와 RPC(Remote Procedure Call)란 방법을 사용한다.
LPC는 같은 시스템 상에서 실행되는 서로 다른 프로세스 사이간의 커뮤니케이션 방법으로 사용되고
RPC는 광범위한 네트워크 전송 매커니즘을 사용하여 서로 다른 시스템 상에서 실행되는 프로세스가 서로
커뮤니케이션 하는 방법을 제공한다.
서로 다른 프로세스 영역 사이에 인터페이스 포인터가 넘겨질 때 윈도우운영체제는 인터페이스를 먼저 마샬링하는데 필요한 프록시(proxy)와 스텁(stub)코드를 로드한다.
클라이언트에서는 프록시가 로드되고 서버에서는 스텁코드가 로드되어 서로 통신할 때 NDR(Network Data Representation)이라고 하는 네트워크 중립적인 형식으로 포장하는 작업을 수행하는 아주작고 가벼운 일종의 COM 컴포넌트이다.
프록시는 클라이언트가 커뮤니케이션하고자 하는 로컬 또는 리모트 서버에 있는 COM 객체와 같은 인터페이스를 노출시킨다. 따라서 클라이언트에서 로컬 또는 리모트 서버의 COM 객체에 인터페이스 포인터를 요청할 때 클라이언트에게 리턴되는 인터페이스 포인터는 실제로 프록시가 노출하는 인터페이스 포인터가 된다. 실제 이 인터페이스 포인터를 사용하여 메서드를 호출할 때 실제로는 프록시 안에 있는 코드가 실행된다.
프록시는 클라이언트로부터 넘겨 받은 매개변수를 전송할 수 있도록 포장하는 작업을 수행한다. COM에서는 데이터 전송의 표준 형식으로 NDR을 사용한다.
이때 호출측의 데이터를 전송할 수 있는 표준 형식으로 변환하는 것을 마샬링이라고 하며, 그 반대의 작업을 언마샬링이라고 한다. 스텁이 클라이언트에서 넘겨준 데이터를 받아 언마샬링작업을 수행하고 COM 객체
내부에 있는 메서드를 호출한다.
커스텀 인터페이스의 프록시/스텁 생성
매우 복잡하고 어려운 일이다.
IDL을 MIDL로 컴파일하면 쉽게 커스텀 인터페이스에 대한 프록시/스텁 코드를 얻을수 있다.
MIDL은 기본적으로 64비트 운영체제를 지원한다. 32비트 윈도우 운영체제에서 실행될 프록시/스텁 DLL을 생성하기 위해서는 /env win32 옵션을 지정해야 한다. (예 midl /env win32 /Oic /h hello_h.h hello.idl)
인터페이스 헤더는 XXX_h.h, 인터페이스 UUID는 XXX_i.c 커스텀 인터페이스의 프록시/스텁 코드는 XXX_p.c 에 저장된다. 이 파일에는 클라이언트 클라이언트와 서버 스텁 파일에 있는 것과 동일한 루틴을 포함하고 있다. 클라이언트와 서버 측의 두개의 대리자(surrogate) 메서드가 포함된다.
클라이언트 대리자 메서드는 XXX_Proxy와 같은 이름을 가지며 서버측 대리자 메서드는 XXX_Stub과 같은 이름을 갖는다. dlldata.c 파일에는 프록시/스텁 DLL을 레지스트리에 등록하는데 필요한 DllMain?, DllRegisterServer?, DllUnregisterServer등?의 함수가 정의되어 있다. 따라서 dlldata.c 파일을 컴파일할 때 이들 함수를 생성하려면 REGISTER_PROXY_DLL 매크로를 지정해야 한다. 이와 함께 프록시/스텁 DLL을 생성할 때 RPC 런타임 임포트 라이브러리 rpcrt4.lib를 함께 링크시켜야 한다.
효율적인 마샬링을 위한 IDL 애트리뷰트
in, out, string의 애트리뷰트는 위에서 살펴 보았다.
포인터 관련 IDL 애트리뷰트 |
설 명 |
ref | 포인터가 단지 레퍼런스이며 메서드 안에서 포인터 값이 변경되지 않는다는 것을 나타낸다. 포인터가 가르키는 주소의 데이터는 변경될 수 있다. 또한 포인터는 공유하지 않는 고유한 메모리를 가르킨다는 것을 나타낸다. 이 애트리뷰트의 포인터에 대한 마샬링 코드는 가장 작고 빠르다. |
unique | 포인터는 다른 포인터와 공유하지 않는 고유한 메모리를 가르킨다는 것을 나타낸다. 그러나 메서드안에서 포인터 값은 변경될 수 있기 때문에 마샬링 코드는 서버에서 포인터가 가르키는 데이터를 클라이언트로 복사해와야 한다. |
ptr | 메서드 안에서 포인터 값이 변경될 수 있으며 공유 메모리 영역을 가르킬 수도 있다는 것을 나타낸다. 이 애트리뷰트의 포인터에 대한 마샬링 코드는 가장 복잡하다. |
인터페이스 애트리뷰트 리스트에 pointer_default 예약어를 사용하여 포인터 매개변수에 다른 애트리뷰트가 지정되지 않은 경우에 어떻게 처리할 것인가에 대한 디폴트 정보를 제공할 수 있다.
예)
[ uuid(@#$@#$#@$), object, pointer_default(unique) ]
배열 관련 IDL 애트리뷰트 |
설 명 |
size_is | 복사해야 하는 배열 요소의 전체 갯수를 지정한다. |
max_is | 0번째 배열 요소에서부터 복사되어야 하는 최대 배열 요소를 지정한다. |
first_is | 복사를 시작해야 하는 첫번째 배열 요소를 지정한다. 일반적으로 last_is 애트리뷰트와 같이 사용된다. |
last_is | 복사해야 하는 마지막 배열 요소를 지정한다. 일반적으로 first_is 애트리뷰트와 같이 지정된다. |
인터페이스 포인터(interface pointer) iid_is 애트리뷰트를 사용한다.
아웃-오브-프로세스 서버 COM 컴포넌트 구현
인프로세스 서버 COM 컴포넌트는 DllCanUnloadNow?(), DllGetClassObject?(), DllRegisterServer?(), DllUnregisterServer?()의 4개의 익스포트 함수를 통해 DLL파일 형식으로 기능을 제공한다.
아웃-오브-프로세스 서버 COM 컴포넌트는 EXE형식으로 외부로 함수를 노출하지 못한다.
WinMain?()함수
3가지 부분으로 나뉜다.
1.COM 라이브러리 초기화
2.메시지 루프
3.COM 라이브러리 해제
WinMain()
{
...
hr = ::CoInitializeEx(NULL, COINIT_APARTMENTTHREADED);
...
while(GetMessage(&msg, NULL, 0, 0)
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
...
::CoUninitalize();
return 0;
}
메시지 루프가 존재 이유는
클라이언트가 아웃-오브-프로세스 서버 COM 컴포넌트에 포함되어 있는 COM 객체의 인스턴스를 생성할때, COM 라이브러리는 이 COM 객체에 대하여 히든 윈도우를 생성한다. 그리고 클라이언트가 이 COM 객체의 인터페이스 메서드를 호출할 때, COM 라이브러리는 이 메서드 호출을 히든 윈도우의 메시지 큐에 저장한다. 따라서 아웃-오브-프로세스 서버는 윈도우 메시지 큐에서 저장된 메시지를 읽기위해서 메시지 루프가
존재하게된다.
이러한 방식 메서드 호출 전달 메커니즘을 STA(single threaded apartment)안에서 실행되는 COM객체에
대한 메서드 호출을 동기화 하는 방법이다.
CoRegisterClassObject?()와 CoRevokeClassObject?()
인프로세스 서버의 경우 CoCreateInstanceEx?()함수를 사용해 COM 객체의 인스턴스를 요청할때,
COM 라이브러리에 으해 DllGetClassObject?()익스포트 함수가 호출되므로 인스턴스 요청에 대해
언제 생성할지 명확하다. 반면, 아웃-오브-프로세스 서버의 경우 클라이언트에서 CoCreateInstanceEx?()
함수 호출을 통해 COM 객체를 요청할때, 레지스트리에 저장된 아웃오브프로세스서버 컴포넌트의 경로명에 "-Embedding" 명령행 매개변수를 추가한다. 그리고 WinMain?()에 넘어온 매개변수의 값이 "-Embedding"
인지 확인하여 클래스 팩토리를 생성시키는 작업을 수행한다.
COM 라이브러리 내부적으로 등록된 클래스 팩토리 COM 객체를 저장하는 ROT(Running Object Table)이 존재한다. 클라이언트가 CoGetClassObject?()를 호출할 때, COM라이브러리는 먼저 이 ROT에 요청한 CLSID가 존재하는지 검사후 등록되어 있지 않다면 위의 방법을 통해 새로운 클래스 팩토리 를 생성하고 ROT에 등록하게된다.
이 ROT에 클래스 팩토리를 등록시키는 역활이 CoRegisterClassObject?()의 역할이다.
STDAPI CoRegisterClassObject(
REFCLSID rclsid, // 등록된 클래스 팩토리 컴포넌트와 관련된 CLSID
IUnknown *pUnk, // 클래스 팩토리 컴포넌트의 IUnknown 인터페이스 포인터
DWORD dwClsContext, // 클래스 팩토리의 실행 컨텍스트. 반드시 CLSCTX_LOCAL_SERVER 이어야 한다.
DWORD flags, // 클래스 팩토리가 생성할 수 있는 컴포넌트의 수. REGCLS_SINGLEUSE나 REGCLS_MULTI_SEPARATE를 선택할 수 있다.
LPDWORD lpdwRegister // 리턴값 포인터. 클래스 팩토리에 대한 매직 쿠키(magic cookie)
)
단, REGCLS_MULTI_SEPARATE를 사용할 경우 COM 컴포넌트내에 존재하는 또 다른 객체를 생성시키고자 할때
또 다시 COM 컴포넌트를 로드하고 등록하게 된다. 이경우 대부분 비효율적으로 돌아가게 되는데 이럴때 dwClsContext?를 CLSCTX_LOCAL_SERVER|CLSCTX_INPROC_SERVER를 사용하여 하나의 컴포넌트만 등록하게 해야 한다.
같은 내용으로 일관되게 사용하는 방법은 CLSCTX_LOCAL_SERVER, REGCLS_MULTIPLEUSE 로
위와 같은 결과를 얻게된다.
등록된 ROT에서 제거하는 방법은 CoRevokeClassObject?()를 사용하는 방법이다.
HRESULT CoRevokeClassObject(
DWORD dwRegister // 클래스 팩토리 쿠키값. 위의 CoRegisterClassObject()의 리턴값 포인터가 쓰인다.
);
쿠키(cookie)란 용어는 어떤 것을 식별할 수 있는 데이터 구조체를 가르킨다.
프로세스 종료
인프로세스 서버에서는 수동적으로 메모리가 언로드 된다. COM 라이브러리가 CoFreeUnusedLibraries?()를 호출하면, 인프로세스의 DllCanUnloadNow?()가 호출되고 사용중인 COM 객체가 없을 때, CoFreeunusedLibraries?()가 메모리에서 언로드 하게 된다.
아웃-오브-프로세스 서버에서는 COM 객체 카운터 외에도 락(lock)카운터를 관리해야 한다.
아웃-오브-프로세스 서버 COM 컴포넌트가 종료되는 두가지 경우가 있다.
1.COM 객체 카운터가 현재 0이고, 클라이언트가 IClassFactory?::LockServer?(FALSE)를 호출함으로써
마지막 락 카운터가 0이 될 때
2.락 카운터가 현재 0이고, 클라이언트가 IUnknown::Release를 호출하여 마지막 COM 객체 카운터가
0이 될 때
레지스트리 등록 및 해제
인프로세스 서버에 DllRegisterServer?(), DllUnregisterServer?()에 대응하는 아웃-오브-프로세스 서버의 대체할 수 있는 기능이 있다. 위의 "-Embedding" 과 같은 방식으로 매개변수 "-RegServer?"와 "-UnregServer?"를 사용한다. 하는 역할을 거의 같다.
제6장 Visual C++ COM 컴파일러
COM 지원 C++ 컴파일러 개요
Visual C++ 컴파일러는 형식 라이브러리(type library)를 직접 읽어, 그것을 컴파일 가능한 C++소스코드로 변환하는 기능을 제공한다.
Visual C++ 컴파일러는 COM을 지원하기 위한 C++언어 확장 예약어와 클래스를 제공한다.
Visual C++ 컴파일러 COM을 위해 추가된 항목 |
#import!! 선행 처리기 지시어 |
_declspec 확장 속성 : uuid 및 property |
_uuidof |
_com_ptr_t 클래스 |
_com_error 클래스 |
_bstr_t 클래스 |
_variant_t 클래스 |
전역함수 |
사용예
CLSID clsid;
hr = ::CLSIDFromProgID(L"HelloServer.Hello.1", &clsid);
위의 코드와 같은 의미이다.
#import!! "progid:HelloServer.Hello.1" no_namespace // 프로그램 아이디로 COM 객체의 CLSID를 구할 수 있다.
...
CLSID clsid = _uuidof(Hello); // CLSID형식의 CLSID를 반환한다.
// 에러 처리도 이전에 hr을 통해 결과값을 확인했지만 try-catch문을 사용한다.
try
{
...
}
catch(_com_error &e)
{
cout << e.ErrorMessage() << endl;
}
형식 라이브러리
COM 객체가 소스 형태로 제공하지 않는다면 COM 객체를 사용할 수 없다.
COM 컴포넌트가 형식 라이브러리를 제공하기 위해서는 IDL 파일에 형식 라이브러리 정보를 제공해야 한다.
형식 라이브러리란? COM 컴포넌트가 노출하는 COM객체에 대한 정보를 포함하는 .TLB확장자를 같은 일종의 복합 다큐먼트(compound document)이다. 따로 존재할 수도 있고 인식 가능한 파일 형식 안에 포함될 수 있다.
형식 라이브러리 자체도 ITypeLib?, ITypeLib2?, ITypeInfo?, ITypeInfo2? 그리고 ITypeComp? 인터페이스를
노출하는 하나의 COM 객체이므로 Visual C++ 컴파일러나 OLE/COM 객체 뷰어와 같은 툴은 이들 인터페이스를 통하여 형식 라이브러리가 제공하는 정보에 접근할 수 있다. 또한 MIDL 컴파일러는 ICreateTypeLib?, ICreateTypeLib2?, ICreateTypeInfo2?, ICreateTypeInfo? 인터페이스를 사용하여 형식 라이브러리를
생성하게 된다. 형식 라이브러리 정보는 레지스트리의 HKEY_CLASSES_ROOT\TypeLib? 서브키에
저장된다.
#import!! 선행 처리기 지시어
형식 라이브러리에 정보를 읽어 COM 객체와 인터페이스가 기술된 C++ 헤더 파일로 변환하는 작업을
수행한다.
#import!! "파일명" 속성리스트 #import!! <파일명> 속성리스트 |
파일명에는 전형적인 .TLB 형식 라이브러리뿐 아니라 DLL,EXE등의 형식 라이브러리를 저장할 수 있는
모든 파일이 포함된다.
C++ 컴파일러의 파일을 찾는 순서는 다음과 같다.
1.현재 디렉토리
2.PATH 환경 변수에 지정된 경로명
3.LIB 환경 변수에 지정된 경로명
4./I 컴파일러 옵션에 지정된 경로명
좀 더 편한 방법은 프로그램ID나 타입라이브러리ID를 지정할 수도 있다.
#import!! "progid:HelloServer?.Hello.1"
#import!! "libid:$%$#%#$-#$%#$-$%#$%$%$#%$#%#$%#$"
어느 방법을 사용하든 Visual C++ 컴파일러는 형식 라이브러리 정보를 읽어 C++ 소스 코드로 된 2개의 형식 라이브러리 헤더 파일을 생성한다.
.TLH와 .TLI을 생성하는데 각각 헤더와 구현내용에 해당한다.
속성리스트 |
설명 |
auto_rename | |
auto_search | |
embedded_idl | |
exclude | |
high_method_prefix | |
high_property_prefixes | |
implementation_only | |
include(...) | |
inject_statement | |
named_guids | VisualC++컴파일러가 .TLH 파일을 생성할 때, GUID를 정의하는 코드를 추가한다. 즉 변수로 GUID가 존재한다. |
no_auto_exclude | |
no_dual_interfaces | |
no_implementation | |
no_namespace | 라이브러리명이 HelloServerLib?이면 디폴트 네임스페이스명은 HelloServerLib?가 된다. 설정된 네임스페이스를 생략한다. |
no_search_namespace | |
no_smart_pointers | |
raw_dispinterfaces | |
raw_interfaces_only | |
raw_method_prefix | |
raw_native_types | |
raw_property_prefixes | |
rename | |
rename_namespace | |
rename_search_namespace | |
tlbid |
C++ 언어 확장
_declspec(uuid())와 _uuidof()
_declspec(uuid(GUID))으로 GUID를 선언하고
_uuidof(표현식)으로 GUID를 꺼내 온다.
struct _declspec(uuid("$#^%#$%^#$-$#%$#")) /* CLSID */ Hello;
_uuidof(Hello);
_declspec(property)
확장속성을 사용한 변수에 포인터 멤버 선택 연산자('->')를 사용해서 참조할 때 컴파일러는 이들에 대하여 대응되는 함수 호출로 변경한다.
_declspec(property(get=Getname, put=Putname)) _bstr_t name;
wchar_t *name;
name = (wchar_t*)pIHello->name; // 이문장은 name = (wchar_t*)pIHello->Getname(); 으로 번역이 된다.
put 형식의 함수의 반환값은 void다.
COM 지원 확장
_com_ptr_t 스마트 포인터 클래스
스마트 포인트 기능을 가지고 있는 템플릿 클래스.
CoCreateInstance?(), IUnknown인터페이스,AddRef?(), Release(), QueryInterface?()기능을 클래스 안에 감춤으로 편리하게 COM객체의 새로운 인스턴스를 생성할 수 있다. 구현 코드는 COMIP.H에서 볼 수 있다.
스마트 포인터 클래스를 사용하여 COM 객체의 새로운 인스턴스를 생성하는 방법은 2가지가 있다.
_COM_SMARTPTR_TYPEDEF(IHello, _uuidof(IHello));
//typedef _com_prt_t<_com_IID<IHello, _uuidof(IHello)>> IHelloPtr; 과 같은 코드
struct _declspec(uuid("#$%#$%#$%$%#$%#$%#$")) Hello;
IHelloPtr pIHello(_uuidof(Hello)/*, CLSCTX_ALL*/);
//extern "C" const GUID _declspec(selectany) CLSID_Hello = { 0x29ebf$%#$%#$%$%#$%$%#$%$% };
//IHelloPtr pIHello(_uuidof(CLSID_Hello); 와 같고
//IHelloPtr pIHello("HelloServer.Hello.1"); 와도 같다.
_COM_SMARTPTR_TYPEDEF(IHello, _uuidof(IHello));
IHelloPtr pIHello; // default생성자와 CreateInstance()호출로 새로운 인스턴스 생성하는 방법
hr = pIHello.CreateInstance(_uuidof(Hello));
if(SUCCEEDED(hr))
{
...
}
_com_ptr_t 클래스의 멤버함수에 접근할때는 . 멤버접근연산자를 사용하고, 생성된 COM객체에 속성이나
메서드에 접근할 때는 -> 멤버접근연산자를 사용한다는 점을 유의할 것!!
하나의 COM 객체에서 2가지 인터페이스를 지원한다면 각각 COM객체를 생성하는게 아니라 하나를 만든
다음에 대입하여 준다.
IGoodbyePtr pIGoodbye = pIHello;
//IGoodbyePtr pIGoodbye; PIHello.QueryInterface(_uuidof(IGoodbye), &pIGoodbye); 와 같다.
사용이 완료되었다면
pIHello = 0; pIGoodbye = 0;
// pIHello.Release(); pIGoodbye.Release(); 와 같다.
_com_error 클래스와 HRESULT 에러 처리
#import!! 지시어가 생성하는 .TLI 파일의 메서드 구현코드는 HRESULT 검사에 실패하게 되면 _com_issue_error()또는 _com_issue_errorex()를 호출한다.
_com_issue_error()는 단순히 HRESULT 값을 매개변수로 취하여 IErrorInfo?객체 없이
_com_error 데이터 형의 예외를 던짐.
_com_issue_errorex()는 HRESULT값, 인터페이스 포인터, IID를 취하여 해당 인터페이스가
IErrorInfo? 인터페이스를 지원하면 IErrorInfo? 객체를 구하여 _com_error데이터형의 예외를 던진다.
_com_error 클래스 | |
ErrorMessage?() | const TCHAR *형의 에러 설명하는 문자열을 반환 |
Error() | HRESULT 값. 에러코드를 리턴 |
Source() | IErrorInfo?객체에 대하여 IErrorInfo?::GetSource?()멤버 함수의 호출결과(_bstr_t) 즉 에러소스를 리턴 |
Description() | IErrorInfo?::GetDescription?()멤버 함수 호출결과(_bstr_t)를 리턴 |
COM 데이터형 클래스 : _bstr_t와 variant_t
Visual Basic의 BSTR과 VARIANT 데이터형을 지원하기 위한 방법
일단 생략
COM 지원 전역 함수
기본적으로 BSTR이나 _bstr_t 타입의 문자열이 저장되지만 char* 데이터 타입과의 변환은 번거롭다.
comutil.h에 두 데이터 타입의 변환 함수를 제공한다.
namespace _com_util { BSTR _stdcall ConvertStringToBSTR(const char* pSrc); char * _stdcall ConvertBSTRToString(BSTR pSrc); }
제7장 Active Template Library 개요
ATL의 특징
첫번째 장점. 작고 빠르고 확장성 갖는 COM 컴포넌트를 만들어준다. ATL은 STL의 전통을 이어받아
C++ 템플릿을 기반으로 한다.
두번째 장점. COM 컴포넌트를 쉽게 구현할 수 있다. IUnknown, IClassFactory등? 중복되는 코드를 넣을
필요가 없다.
ATL 프로젝트 생성
Visual C++에서 지원하는 옵션만으로 쉽게 생성가능
IDL, rgs(레지스트리 등록 파일), def(익스포트 함수 정의 파일) 등 기본적인 골격을 다 만들어준다.
ATL COM 객체 생성
옵션 제공
ATL COM 객체 구현
위자드 제공
ATL 기반 클래스
ATL 모듈 클래스
모듈 클래스 |
설명 |
CAtlDllModule?T | 서버 유형이 동적 연결 라이브러리인 경우. DllMain?()외 인-프로세스가 노출해야 하는 4개의 익스포트 함수를 정의한다. |
CAtlExeModule?T | 서버 유형이 실행 파일일 경우 WinMain?외에도 아웃-오브-프로세스 서버 COM 컴포넌트가 구현해야 하는 ROT에 클래스 팩토리를 등록및 해제하는 코드와 메시지 루프를 정의한다. |
CAtlServiceModule?T | CAtlExeModule?T를 상속받은 서비스 모듈. 윈도우 운영체제 서비스로서 실행하는데 필요한 여러 함수를 정의하고 있다. |
// CAtlDllModuleT의 예
class CHelloServerModule : public CAtlDllModuleT <CHelloServerModule>
{
public:
DECLARE_LIBID(LIBID_HelloServerLib)
DECLARE_REGISTRY_APPID_RESOURCEID(IDR_HELLOSERVER, "{$#$@%@#$%$@%$%$#%$#%#$%#$%}")
};
CHelloServerModule _AtlModule;
}
ATL COM 객체 기초 클래스
// CHello
class ATL_NO_VTABLE CHello:
public CComObjectRootEx<CComSingleThreadModel>,
public CComCoClass<CHello, &CLSID_Hello>
public IHello
{
public:
CHello() {}
HRESULT FinalConstruct() { return S_OK; }
void FinalRelease() {}
STDMETHOD(sayHello)(wchar_t* name, wchar_t **message);
DECLARE_REGISTRY_RESOURCEID(IDR_HELLO)
BEGIN_COM_MAP(CHello)
COM_INTERFACE_ENTRY(IHello)
END_COM_MAP()
DECLARE_PROTECT_FINAL_CONSTRUCT()
};
OBJECT_ENTRY_AUTO(__uuidof(Hello), CHello)
CComObjectRootEx? 템플릿 클래스는 ATL COM 객체의 IUnknown 인터페이스 내부 구현 코드
(InternalAddRef?(), InternalRelease?(), InternalQueryInterface?())를 제공!
CComCoClass? 템플릿 클래스는 COM 객체의 디폴트 클래스 팩토리 COM 객체를 정의한다.
주의할 점은 아직 ATL의 마법사가 생성하는 COM 객체 클래스는 추상 클래스이다.
아직 IUnknown 인터페이스의 3개의 메서드.
즉 AddRef?(), Release(), QueryInterface?()가 재정의 구현되어 있지 않다는 점이다.
ATL의 IUnknown 인터페이스 구현
class CComObjectRootBase
{
public:
union
{
long m_dwRef;
IUnknown* m_pOuterUnknown;
}
CComObjectRootBase() { m_dwRef = 0L; }
HRESULT FinalConstruct() { return S_OK; }
void FinalRelease() {}
static HRESULT WINAPI InternalQueryInterface(void *pThis, const _ATL_INTMAP_ENTRY* pEntries, REFIID iid, void **ppvObject)
{
HRESULT hRes = AtlInternalQueryInterface(pThis, pEntries, iid, ppvObject); // COM 맵에 정의된 정보에서 찾고자 하는 인터페이스를 조회하는 코드를 만들어 낸다.
return _ATLDUMPIID(iid, pszClassName, hRes);
}
ULONG OuterAddRef() { return m_pOuterUnknown->AddRef(); }
ULONG OuterRelease() { return m_pOuterUnknown->Release(); }
HRESULT OuterQueryInterface(REFIID iid, void **ppvObject) { return m_pOuterUnknown->QueryInterface(iid, ppvObject); }
void SetVoid(void *) {}
void InternalFinalConstructAddRef() {}
void InternalFinalConstructRelease() {}
}
template <class ThreadModel> // ThreadModel은 CComSingleThreadModel or CComMultiThreadModel 클래스가 지정됨
class CComObjectRootEx : public CComObjectRootBase
{
private:
_CritSec m_critsec;
public:
typedef ThreadModel _ThreadModel;
typedef _ThreadModel::AutoCriticalSection _CritSec;
ULONG InternalAddRef() { return _ThreadModel::Increment(&m_dwRef); }
ULONG InternalRelease() { return _ThreadModel::Decrement(&m_dwRef); }
void Lock() { m_critsec.Lock(); }
void Unlock() { m_cirtsec.Unlock(); }
}
Internal QueryInterface?(), AddRef?(), Release()는 COM 객체의 레퍼런스 카운트와 인터페이스 조회를
담당하고, Outer QueryInterface?(), AddRef?(), Release()는 통합(aggregation)의 COM 객체의 레퍼런스
카운트와 인터페이스 조회를 위임하는 코드를 만들어낸다.
하지만, CComObjectRootEx? 템플릿 클래스를 파생하는 것으로도 IUnknown 인터페이스의 AddRef?(), Release(), QueryInterface?()메서드가 재정의되어 구현되지 않았다. 때문에 CComObject? 템플릿 클래스는
이들 메서드를 구현한다.
template<class Base>
class CComObject : public Base
{
public:
typedef Base _BaseClass;
CComObject(void *=NULL) { _Module.Lock(); }
~CComobject() { m_dwRef = 1L; FinalRelease(); _Module.Unlock(); }
STDMETHOD_(ULONG, AddRef)() { return InternalAddRef(); }
STDMETHOD_(ULONG, Release)() { ULONG l = InternalRelease(); if(l==0) delete this; return l; }
STDMETHOD(QueryInterface)(REFIID iid, void **ppvObject) { return _InternalQueryInterface(iid, ppvObject); }
static HRESULT WINAPI CreateInstance(CComObject<Base> **pp);
};
ATL의 IClassFactory? 인터페이스 구현
CComCoClass? 템플릿 클래스는 COM 객체의 디폴트 클래스 팩토리 COM 객체를 정의하며, 사용자 COM 객체 클래스는 반드시 CComCoClass? 템플릿 클래스에서 파생되어야 한다.
실제로 하는 역할은 COM 객체의 CLSID 포인터를 템플릿 매개변수로 받아 DECLARE_CLASSFACTORY 매크로를 사용해 CComClassFactory? 클래스가 해당 COM 객체의 클래스 팩토리라는 것을 지정하며, DECLARE_AGGREGATABLE 매크로로 해당 COM 객체가 통합(Aggregation)될 수 있다는 것을 지정하는 역할 뿐이다.
#define DECLARE_CLASSFACTORY_EX(cf) typedef CComCreator<CComObject<cf> > _ClassFactoryCreatorClass; // 클래스팩토리 COM객체의 인스턴스를 생성하는 클래스는 CComCreator클래스이다.
#define DECLARE)CLASSFACTORY() DECLARE_CLASSFACTORY_EX(CComClassFactory) // COM객체의 클래스 팩토리는 CComCoClass클래스이다.
#define DECLARE_AGGREGATABLE(x) public:\ typedef CComCreator2<CComCreator<CComObject<x> >, CComCreator<CComAggObject<x> > > _CreatorClass; // AGGREGATION 여부는 OBJECT_ENTRY 매크로에 의해 객체맵에 저장된다.
#define DECLARE_NOT_AGGREGATABLE(x) public:\ typedef CComCreator2<CComCreator<CComObject<x> >, CComFailCreator<CLASS_E_NOAGGREGATION> > _CreatorClass;
template <class T, const CLSID* pclsid>
class CComCoClass
{
public:
DECLARE_CLASSFACTORY() // ①
// typedef CComCreator<CComObject<CComClassFactory> > _ClassFactoryCreatorClass
DECLARE_AGGREGATABLE(T) // ②
// public:\ typedef CComCreator2<CComCreator<CComObject<x> >, CComCreator<CComAggObject<x> > > _CreatorClass;
...
}
①
class CComClassFactory : public IClassFactory, public CComObjectRootEx<CComGlobalsThreadModel> // IClassFactory를 구현
{
public:
BEGIN_COM_MAP(CComClassFactory)
COM_INTERFACE_ENTRY(IClassFactory)
END_COM_MAP()
STDMETHOD(CreateInstance)(LPUNKNOWN pUnkOuter, REFIID riid, void** ppvObj);
STDMETHOD(LockServer)(BOOL fLock);
void SetVoid(void *pv) { m_pfnCreateInstance = (_ATL_CREATORFUNC*)pv; }
_ATL_CREATORFUNC* m_pfnCreateInstance;
};
template <class T1>
class CComCreator
{
public:
static HRESULT WINAPI CreateInstance(void* pv, REFIID riid, LPVOID* ppv) // T1으로 지정된 클래스의 인스턴스를 생성한다.
{
ATLASSERT(ppv != NULL);
if (ppv == NULL)
return E_POINTER;
*ppv = NULL;
ATLTRY(p = new T1(pv))
if(p!=NULL)
{
p->SetVoid(pv);
p->InternalFinalConstructAddRef();
hRes = p->FinalConstruct();
p->InternalFinalConstructRelease();
if(hRes == S_OK) hRes = p->QueryInterface(riid, ppv);
if(hRes != S_OK) delete p;
}
return hRes;
}
};
클래스팩토리도 COM 객체이도 DECLARE_CLASSFACTORY 매크로에 의해 CComClassFactory? 클래스의
COM 객체 생성자 클래스가 CComCreator? 클래스로 지정되었으므로 CreateInstance?()에 의하여 클래스팩토리 COM 객체가 생성된다.
②
template <class T1, class T2> class CComCreator2 // Aggregation 여부에 따라 적절한 CComCreator 클래스의 CreateInstance()호출하여 COM 객체 인스턴스를 생성하게 된다.
{
public:
static HRESULT WINAPI CreateInstance(void *pv, REFIID riid, LPVOID *ppv)
{
HRESULT hRes = E_OUTOFMEMORY;
if(pv == NULL) { hRes = T1::CreateInstance(NULL, riid, ppv);
else { hRes = T2::CreateInstance(pv, riid, ppv); }
return hRes;
}
};
template <HRESULT hr>
class CComFailCreator
{
public:
static HRESULT WINAPI CreateInstance(void*, REFIID, LPVOID* ppv)
{
return hr;
}
};
사용자 COM 객체 생성 과정 클라이언트에서 CoCreateInstance?()호출하면 내부적으로
- COM 라이브러리의 CoGetClassObject?()는 레지스트리에서 CLSID 정보를 읽어 인-프로세스 서버 COM 컴포넌트를 메모리에 로드
- DllGetClassObject?()를 호출
ATLINLINE ATLAPI AtlComModuleGetClassObject(_ATL_COM_MODULE *pComModule, REFCLSID rclsid, REFIID riid, LPVOID *ppv);
struct _ATL_OBJMAP_ENTRY30
{
const CLSID* pclsid;
HRESULT (WINAPI *pfnUpdateRegistry)(BOOL bRegister); // 시스템 레지스트리에 COM 객체를 등록하는 작업을 수행하는 함수 포인터
_ATL_CREATORFUNC* pfnGetClassObject; // 클래스 팩토리 COM 객체를 생성하는 함수 포인터
_ATL_CREATORFUNC* pfnCreateInstance; // 사용자 COM 객체를 등록하는 함수 포인터
IUnknown* pCF; // 클래스팩토리 COM 객체의 IUnknown 인터페이스 포인터
DWORD dwRegister; // 아웃-오브-프로세스 서버에서 CoRegisterClassObject()호출할 때 리턴되는 쿠키 정보가 저장
_ATL_DESCRIPTIONFUNC* pfnGetObjectDescription; // COM 객체를 설명하는 텍스트를 반환하는 함수 포인터
_ATL_CATMAPFUNC* pfnGetCategoryMap;
...
};
typedef _ATL_OBJMAP_ENTRY30 _ATL_OBJMAP_ENTRY;
OBJECT_ENTRY_AUTO(__uuidof(Hello), CHello) // 다음과 같이 확장 __declspec(selectany) ATL::_ATL_OBJMAP_ENTRY __objMap_CHello = { &__uuidof(Hello), CHello::UpdateRegistry, CHello::_ClassFactoryCreatorClass::CreateInstance, // CComCreator::CreateInstance CHello::_CreatorClass::CreateInstance, // CComCreator2::CreateInstance 0, 0, CHello::GetObjectDescription, CHello::GetCategoryMap, CHello::ObjectMain };
ATL_NO_VTABLE 매크로
ifdef _ATL_DISABLE_NO_VTABLE
#define ATL_NO_VTABLE
#else
#define ATL_NO_VTABLE __declspec(novtable)
#endif
추상 클래스의 생성자에서 초기화된 가상 함수 테이블 포인터는 파생클래스(CComObject?)의 생성자에 의해 다시 한번 덮어 씌여지게 되므로 필요없게 된다. 따라서 이 매크로가 지정되면 가상함수테이블과 가상함수를 없애고 가상함수테이블포인터를 적절하게 초기화 하게 된다.
클래스의 생성자에서 가상함수를 호출하지 않도록 해야 하는데 호출해야 할 필요가 있다면 FinalConstruct?()를 사용하면 된다. 이 멤버함수가 호출되는 때는 이미 COM 객체 클래스의 인스턴스가 생성되어 초기화된
상태이므로 안전하게 가상함수를 호출할 수 있게 된다.
사용여부를 알 수 없다면 ATL_NO_VTABLE을 삭제하던지 stdafx.h헤더파일에서 _ATL_DISABLE_NO_VTABLE 매크로를 사용해 이 속성을 사용하지 않도록 한다.
스마트 포인터 클래스와 데이터 타입 클래스
ATL 스마트 포인터 클래스
template < class T> class CComPtr
template < class T, const IID * piid> class CComQIPtr
두 클래스는 모두 인터페이스에 대하여 자동적으로 AddRef?(), Release()를 호출하여 레퍼런스 카운터를
관리한다. 연산자 멤버함수를 제공하여 포인터를 조작할 수 있으므로 인터페이스 포인터 변수와 이들 클래스를 구별없이 사용할 수 있도록 한다. _com_ptr_t 와 다른 점은 COM 객체의 인스턴스를 생성하는 일은
하지 못한다.
분류 |
멤버명 |
설명 |
데이터멤버 | p | 관리되고 있는 T* 데이터형의 인터페이스 포인터 |
메서드 | Release | 멤버 포인터 p가 가르키는 인터페이스 포인터의 레퍼런스 카운터를 감소 |
연산자 | operator T* | CComPtr?이나 CComQIPtr 객체를 T* 데이터형으로 변환한다. |
연산자 | operator * | 멤버 포인터 p의 값을 리턴한다. |
연산자 | operator & | 멤버 포인터 p의 주소를 리턴한다. |
연산자 | operator -> | 멤버 포인터 p를 리턴한다. |
연산자 | operator = | 다른 포인터 값을 멤버 포인터 p에 지정한다. |
연산자 | operator ! | 멤버 포인터 p의 값이 NULL이면 TRUE를 리턴한다. |
CComPtr? 클래스 사용시 주의점
&연산자를 사용할 때는 해당 CComPtr?클래스 객체의 멤버 포인터인 p가 반드시 NULL이어야 한다.
->연산자를 사용할 때는 해당 CComPtr?클래스 객체의 멤버 포인터인 p가 반드시 NULL이 아니어야 한다.
이유는 &연산자를 이용할 때는 QueryInterface?()의 매개변수로 포인터를 채우게 되고 ->연산자를 이용할 때는 참조된 객체를 사용하기 때문이다.
->연산자를 사용할 때 또 하나의 유의점은 Release()메서들 호출하는 경우이다.
pUnk->Release()호출은 실제로는 pUnk.p->Release()와 같다. (연산자 오버라이딩에 의해서)
그러므로 CComPtr?의 멤버 메서드인 Release()를 호출하기 위해서는 직접 .연산자를 통해 사용해야 한다.
pUnk.Release();
CComPtr? 클래스 생성자와 대입 연산자
CComPtr::CComPtr();
CComPtr::CComPtr(T* lp);
CComPtr::CComPtr(const CComPtr<T> &lp);
T* CComPtr::operator=(T*lp);
T* CComPtr::operator=(const CComPtr<T> &lp);
CComPtr? 클래스는 서로 다른 인터페이스 포인터나 인터페이스 포인터를 캡슐화하는
CComPtr? 클래스 객체를 수용하지 못한다.
CComPtr<IUnknown> pUnk;
CComPtr<IHello> pIHello;
CComPtr<IGoodbye> pIGoodBye(pUnk); // CComPtr형을 받는 생성자 없음. 컴파일 에러
pHello = pUnk; // CComPtr 형을 받는 대입 연산자가 없음. 컴파일 에러.
이러한 문제점을 보완하기 위해서 CComQIPtr 클래스를 제공한다.
CComQIPtr 클래스는 IUnknown 인터페이스 포인터를 매개변수로 받아들이는 생성자와 대입 연산자를
제공한다. 템플릿 매개변수에 GUID를 포함함으로써 다른 인터페이스 포인터로 부터 접근한 인스턴스에서 인터페이스를 생성할 수 있게 된다.
그렇다면 CComPtr?을 사용하는 이점은? 이전 ATL버전과 호환성과 CComQIPtr클래스는 IUnknown인터페이스를 수용할 수 없다는 점이다.
ATL 데이터 타입 클래스
BSTR과 VARIANT 데이터형은 9장으로
애트리뷰트 기반 프로그래밍
애트리뷰트의 목적
컴파일러가 코드에 특정 정보를 담아 컴파일시 골격을 재구성하여 위자드의 의존성을 벗어나 보다 핵심적인 내용만을 담을 수 있다. 애트리뷰트는 전통적은 C++ 언어 구조에 부착되어 C++ 언어를 확장한다.
애트리뷰트 기본 매커니즘
애트리뷰트가 포함된 소스코드를 컴파일할 때, 컴파일러는 소스 코드에서 애트리뷰트를 인식하여 동적으로
파싱한다. 애트리뷰트를 파싱하고 구문 분석후 애트리뷰트 공급자(attribute provider)를 호출하여 컴파일시에 코드를 삽입하거나 수정하여 결국 일반 C++ 컴파일과 같이 .obj의 오브젝트 파일을 생성한다.
애트리뷰트 공급자의 구현은 애트리뷰트의 타입에 따라 다르다.
ATL과 관련된 애트리뷰트는 atlprov.dll에서 구현된다.
COM 관련 애트리뷰트
COM 인터페이스를 정의하는 IDL과 관련된 애트리뷰트의 대부분은 MIDL 애트리뷰트와 거의 동일한 구문과 의미를 갖는다. 따라서 여기에서는 COM객체와 컴파일러에 관련된 애트리뷰트에 대해서 알아본다.
애트리뷰트 |
설명 |
aggregatable | COM객체가 다른 COM객체에 의해 통합될 수 있음을 표시한다. |
aggregates | COM객체가 대상 COM객체를 통합하고 있음을 표시한다. |
coclass | COM 인터페이스를 구현하는 COM객체를 생성한다. |
com_interface_entry | COM 맵에 인터페이스 항목을 추가한다. |
implements_category | COM 객체에 대하여 구현된 컴포넌트 카테고리를 지정한다. |
perf_counter | 클래스 멤버 변수에 성능 모니터 카운터 지원을 추가한다. |
perfmon | 클래스에 성능 모니터 지원을 추가한다. |
progid | COM 객체의 프로그램 ID를 정의한다. |
rdx | 레지스트리 키를 생성하거나 수정한다. |
registration_script | 지정된 등록 스크립트를 실행한다. |
requires_category | COM 객체에 대하여 요구되는 컴포넌트 카테고리를 지정한다. |
synchronize | 메서드 접근을 동기화 한다. |
threading | COM 객체의 스레딩 모델을 지정한다. |
vi_progid | COM 객체의 버전 독립적인 프로그램 ID를 정의한다. |
컴파일러와 관련된 애트리뷰트는 다음과 같다.
emitidl | IDL 애트리뷰트를 처리하여 IDL 파일을 생성할지 여부를 결정한다. |
event_receiver | 이벤트 수신자를 생성한다. |
event_source | 이벤트 소스를 생성한다. |
export | 데이터 구조를 생성되는 IDL 파일에 저장하게 한다. |
implements | IDL coclass 멤버가 되어야 하는 디스패치 인터페이스를 지정한다. |
import!!idl | 지정된 IDL 파일을 생성되는 IDL 파일에 추가한다. |
import!!lib | 이미 다른 형식 라이브러리에 저장된 타입을 생성되는 형식 라이브러리에서 사용할 수 있게 한다. |
includelib | IDL또는 헤더 파일을 생성되는 IDL 파일에 포함시킨다. |
library_block | IDL 파일의 library 블럭 안에 추가한다. |
no_injected_text | 컴파일러가 애트리뷰트 사용의 결과로서 코드를 생성하지 못하게 한다. |
satype | SAFEARRAY 데이터 타입을 지정한다. |
version | 인터페이스나 클래스의 여러 버전중에서 특정한 버전을 찾는다. |
ATL 마법사에서의 애트리뷰트 사용
ATL 마법사에 의존하지 않고도 ATL을 사용하여 COM 컴포넌트를 구현할 수 있는 방법을 제공하지만,
ATL 마법사의 도움을 받으면 더욱 용이하게 개발 할 수 있다.
ATL 프로젝트 마법사에서 특성 사용을 체크해 둔다.
제8장 ATL 윈도우 클래스와 WTL
WTL(windows template library)
윈도우 클래스
윈도우 클래스 개요
ATL::CWindow | |||
ATL::CContainerWindow?T | |||
ATL::CWindowImplRoot? | |||
ATL::CWindowImplBase?T | |||
ATL::CWindowImpl? | |||
ATL::CDialogImplBase?T | |||
ATL::CDialogImpl? | |||
ATL::CAxDialogImpl? | |||
ATL::CAxWindow?T | |||
ATL::CAxWindow2?T |
void MoveFocus(HWND hwnd)
{
CWindow wnd;
wnd.Attach(hwnd);
// 윈도우 사용
wnd.SetFocus();
}
위는 간단히 CWindow 클래스를 이용하여 윈도우를 조작한 예제이다.
이렇게 CWindow 클래스를 직접 사용할 수도 있지만 CWindowImpl?과 CDialogImpl? 클래스로부터 파생되는 클래스를 정의함으로써 프레임이나 다이얼로그와 같은 윈도우를 구현할 수도 있다.
윈도우 애플리케이션
기본적인 윈도우 애플리케이션의 모습
WinMain?()
윈도우 생성 메시지 루프 |
윈도우 등록 //윈도우 생성 CWindow wnd; wnd.Create(szWindowClass, 0, CWindow::rcDefault, _T("윈도우 애플리케이션"), WS_OVERLAPPEDWINDOW, 0); wnd.ShowWindow(nCmdShow); wnd.UpdateWindow(); 메시지 루프 { ... case WM_PAINT: CWidow wnd(hWnd); HDC hdc = wnd.BeginPaint(&ps); RECT rect; wnd.GetClientRect(&rect); ... wnd.EndPaint(&ps); ... }
CWindowImpl? 클래스
CWindowImpl?클래스는 윈도우를 생성하는 것을 포함하는 CWindow 클래스의 기능 외에도 윈도우 클래스 등록과 메시지 처리 기능을 추가로 제공한다.
template
<
class T, // CWindowImpl 클래스에서 파생되는 윈도우 이름
class TBase /*=CWindow*/, // 기초 클래스, default는 CWindow
class TWinTraits /*=CControlWinTraits*/ // 윈도우 스타일을 정의하는 특징(trait)클래스가 지정. default는 CControlWinTraits. 추가 밑에
>
class ATL_NO_VTABLE CWindowImpl : public CWindowImplBaseT< TBase, TWinTraits >
{
public:
DECLARE_WND_CLASS(NULL) // 매개변수는 클래스명이 들어가고, NULL일 경우 ATL이 자동적으로 윈도우 클래스 이름을 생성한다.
// DECLARE_WND_CLASS_EX(WndClassName, style, bkgnd) // 윈도우클래스이름, 윈도우스타일, 배경색
HWND Create(HWND hWndParent, _U_RECT rect = NULL, LPCTSTR szWindowName = NULL, DWORD dwStyle = 0, DWORD dwExStyle = 0, _U_MENUorID MenuOrID = 0U, LPVOID lpCreateParam = NULL); // 윈도우 클래스 등록하고 생성하는 작업
};
class CMainWindow : public CWindowImpl < CMainWindow, CWindow, CFrameWinTraits>
{
};
int APIENTRY WinMain()
{
CMainWindow wnd;
wnd.Create(0, CWindow::rcDefault, _T("ATL 윈도우 애플리케이션"), 0, 0, (UINT)NULL);
if(!wnd) return -1;
wnd.ShowWindow(nCmdShow);
wnd.UpdateWindow();
}
// 메시지 루프 생략
ATL은 두가지 윈도우 특징(trait)클래스를 제공한다.
CWinTraits? 클래스와 CWinTraits?OR 클래스이다.
// CWinTraits
template
<
DWORD t_dwStyle = 0,
DWORD t_dwExStyle = 0
>
class CWinTraits
...
typedef CWinTraits<WS_CHILD| WS_VISIBLE| WS_CLIPCHILDREN| WS_CLIPSIBLINGS, 0> CControlWinTraits;
typedef CWinTraits<WS_OVERLAPPEDWINDOW| WS_CLIPCHILDREN| WS_CLIPSIBLINGS| WS_EX_APPWINDOW| WS_EX_WINDOWEDGE> CFrameWinTraits;
typedef CWinTraits<WS_OVERLAPPEDWINDOW| WS_CHILD| WS_VISIBLE| WS_CLIPCHILDREN| WS_CLIPSIBLINGS| WS_EX_MDICHILD> CMDIChildWinTraits;
typedef CWinTraits<0,0> CNullTraits;
// CWinTraitsOR
template
<
DWORD t_dwStyle = 0,
DWORD t_dwExStyle = 0,
class TWinTraits = CControlWinTraits
>
class CWinTraitsOR
...
// CFrameWinTraits스타일에 WS_EX_CLIENTEDGE 스타일을 추가하고 싶다면
typedef CWinTraitsOR<0,WS_EX_CLIENTEDGE, CFrameWinTraits> CMainWinTraits;
윈도우 프로시저와 메시지 맵
메시지 루프에서 전달된 메시지는 궁극적으로CWindowImplBase?T 클래스의 WindowProc?()로 전달된다.
WindowProc?()는 메시지 처리를 위해 CWindowImpl?클래스에서 파생된 클래스의 ProcessWindowMessage?()를 호출한다.
template <class TBase, class TWinTraits>
LRESULT CALLBACK CWindowImplBaseT< TBase, TWinTraits > :: WindowProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
CWindowImplBaseT<TBase, TWinTraits> *pThis = (CWindowImplBaseT<TBase, TWinTraits> *)hWnd;
...
BOOL bRet = pThis->ProcessWindowMessage(pThis->m_hWnd, uMsg, wParam, lParam, lResult, 0);
if(uMsg == WM_NCDESTROY)
{
HWND hWnd = pThis->m_hWnd;
pThis->OnFinalMessage(hWnd); // PostQuitMessage API를 호출하여 윈도우를 종료하게 만든다.
}
...
}
그리고 궁극적으로 CWindowImplBase?T클래스가 파생하는 CMessageMap? 클래스에서는 ProcessWindowMessage?()는 가상함수 이다.
class ATL_NO_VTABLE CMessageMap
{
public:
virtual BOOL ProcessWindowMessage(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam, LRESULT &lResult, DWORD dwMsgMapID) = 0;
};
결국 메시지를 처리하기 위해서는 ProcessWindowMessage?()를 오버라이딩해야 되지만
ATL에서는 구현코드를 제공하는 메시지 맵을 제공한다. BEGIN_MSG_MAP과 END_MSG_MAP 매크로이다.
//BEGIN_MSG_MAP 부분
BOOL ProcessWindowMessage(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam, LRESULT &lResult, DWORD dwMsgMapID=0)
{
BOOL bHandled = TRUE;
switch(dwMsgMapID)
{
case 0:
break;
// END_MSG_MAP 부분
default:
break;
}
return FALSE;
}
BEGIN_MSG_MAP과 END_MSG_MAP 사이에 들어가는 매크로 |
설명 |
MESSAGE_HANDLER(msg, func) | 윈도우 메시지를 핸들러 함수에 맵핑 시킨다. |
MESSAGE_RANGE_HANDLER(msgFirst, msgLast, func) | 윈도우 메시지의 연속적인 범위를 핸들러 함수에 맵핑시킨다. |
COMMAND_HANDLER(id, code, func) | 메뉴 항목, 컨트롤, 엑셀러레이터에서 발생하는 WM_COMMAND 메시지를 리소스 ID와 알림 코드(notification code)를 기반으로 핸들러 함수를 맵핑시킨다. |
COMMAND_CODE_HANDLER(code, func) | 메뉴항목, 컨트롤, 엑셀러레이터에서 발생하는 WM_COMMAND 메시지를 알림코드를 기반으로 핸들러 함수에 맵핑시킨다. |
COMMAND_RANGE_HANDLER(idFirst, idLast, func) | 메뉴항목, 컨트롤, 엑셀러레이터에서 발생하는 WM_COMMAND 메시지의 연속적인 범위를 리소스 ID를 기반으로 핸들러 함수에 맵핑시킨다. |
NOTIFY_HANDLER(id, code, func) | 컨트롤에서 발생하는 WM_NOTIFY 메시지를 리소스 ID와 알림코드를 기반으로 핸들러 함수에 맵핑시킨다. |
NOTIFY_ID_HANDLER(id, func) | 컨트롤에서 발생하는 WM_NOTIFY 메시지를 리소스 ID를 기반으로 핸들러 함수에 맵핑시킨다. |
NOTIFY_CODE_HANDLER(code, func) | 컨트롤에서 발생하는 WM_NOTIFY 메시지를 알림코드를 기반으로 핸들러 함수에 맵핑시킨다. |
NOTIFY_RANGE_HANDLER(idFirst, idLast, func) | 컨트롤에서 발생하는 WM_NOTIFY 메시지의 연속적인 범위를 리소스 ID를 기반으로 핸들러 함수에 맵핑시킨다. |
이외에도 부모 윈도우가 자식 윈도우에게 메시지를 반사(reflection)하는 메시지와 관련된 REFLECTED_로 시작하는 메시지 맵 매크로 항목도 있다.
매크로가 확장되어 작성되는 핸들러 함수의 원형이다.
// 윈도우 메시지
LRESULT MessageHandler(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL &bHandled);
// WM_COMMAND 메시지
LRESULT CommandHandler(WORD wNotifyCode, WORD wID, HWND hWndCtl, BOOL &bHandled);
// WM_NOTIFY 메시지
LRESULT NotifyHandler(int idCtrl, LPNMHDR pnmh, BOOL &bHandled);
마지막 매개변수인 bHandled는 메시지를 처리했는지 여부를 나타낸다. default는 TRUE이다.
다른 메시지 맵으로 연결시켜주는 체인(chaining) 기능도 제공한다.
매크로 |
설명 |
CHAIN_MSG_MAP(theChainClass?) |
기초 클래스의 디폴트 메시지 맵으로 연결시킨다. |
CHAIN_MSG_MAP_MEMBER(theChainMember?) |
클래스 멤버의 디폴트 메시지 맵으로 연결시킨다. |
CHAIN_MSG_MAP_ALT(theChainClass?, msgMapID) |
기초 크래스의 대체 메시지 맵으로 연결시킨다. |
CHAIN_MSG_MAP_ALT_MEMBER(theChainMember?, msgMapID) |
클래스 멤버의 대체 메시지 맵으로 연결시킨다. |
CHAIN_MSG_MAP_DYNAMIC(dynaChainID) | 실행시에 다른 클래스의 디폴트 메시지 맵으로 연결시킨다. |
CDialogImpl? 클래스
윈도우는 CWindowImpl? 파생 클래스로 정의되는 것처럼 대화상자는 CDialogImpl? 파생 클래스로 정의된다.
대화상자위의 컨트롤 또한 윈도우이다. CDialogImpl?의 상속받은 사용자대화상자클래스에서 CWindow 형태
의 멤버들을 생성하고 대화상자 리소스ID 와 연결을 해주어야 한다.
연결방법은
CWindow m_name;
...
m_name.Attach(GetDlgItem(IDC_NAME));
...
TCHAR name[128];
...
m_name.SetWindowText(name);
m_name.GetWindowText(name, 127);
ATL 대화상자 마법사가 생성한 대화상자 클래스는 CAxDialogImpl? 클래스에서 파생된다.
CAxDialogImpl? 클래스는 CDialogImplBase?T 클래스에서 파생되며 ActiveX 컨트롤을 포함할 수 있도록
구현되어 있다는 것을 제외하고는 CDialogImpl? 클래스와 동일하다.
Windows Template Library
ATL이 윈도우를 효율적으로 구현할 수 있는 기능을 제공하지만 부족하다.
윈도우 어플리케이션에는 윈도우, 대화상자 뿐아니라 메뉴, 도구모음, 상태표시줄과 같은 사용자 인터페이스와 화면에 표시하는데 필요한 GDI 객체와 디바이스 컨텍스트(DC)등에 대한 래퍼가 필요하다.
MFC42.DLL 은 983KB로 무겁다.
WTL은 2000년 1월부터 템플릿 기반 윈도우 라이브러리로 제공하기 시작했다.
WTL의 특징 |
ATL/WTL 응용 프로그램 마법사 지원 |
풍부한 UI 및 컨트롤 클래스 지원 |
GDI 및 DC 클래스 제공 |
DDX(Dynamic Data Exchange) 매커니즘 제공 |
비주얼 스튜디오 닷넷에서는 기본적으로 WTL을 제공하지 않는다.
따라서 WTL을 사용하기 위해서는 WTL을 설치해야 한다. include 디렉터리 포함해 줄 것.
ATL/WTL 응용 프로그램 마법사
등록되어있는 마법사를 동작
프레임 윈도우 클래스
WTL 애플리케이션에서 프레임 윈도우는 CFrameWindowImpl? 클래스에서 파생된 클래스로 정의한다.
SDI 애플리케이션 / MDI 애플리케이션
명령도구모음줄(command bar) : 윈도우 메뉴와 비슷하지만 비트맵 메뉴항목을 갖는 도구모음줄
도구모음줄 / 리바 / 상태표시줄
다중 SDI 애플리케이션
익스플로어 같이 하나의 프로세스 안에서 실행되는 여러 상위 레벨의 SDI 애플리케이션을 생성한다.
1.각 상위 레벨 윈도우(그리고 차일드 윈도우)는 각각의 쓰레드(thread)에서 실행된다.
2.각 쓰레드는 메시지 펌프를 가지며 자신이 소유한 윈도우에 메시지를 가져와 처리할 수 있는 UI 쓰레드이다. 이것은 모든 쓰레드가 프로세스 범위의 데이터를 공유할 수 있을 뿐만 아니라 하나의 쓰레드가 어떤 작업을 수행하느라고 바쁠 때도 다른 쓰레드가 충분히 반응할 수 있다는 것을 의미한다.
3.따라서 각 상위 레벨 윈도우는 사용자에게 독립적인 애플리케이션으로 인식된다.
뷰(view)
프레임 윈도우의 클라이언트 영역을 뷰라고 하는 별도의 차일드윈도우로 표현하는 것이 일반적이다.
CHelloAppView m_view; m_hWndClient = m_view.Create(m_hWnd, rcDefault, NULL, WS_CHILD|WS_VISIBLE|WS_CLIPSIBLINGS|WS_CLIPCHILDREN,WS_EX_CLIENTEDGE);
수퍼클래싱 부분
GDI 래퍼 클래스
Win32 GDI | 래퍼 클래스 |
HDC | CDCT<> |
HPEN | CPenT<> |
HBRUSH | CBrushT<> |
HFONT | CFontT<> |
HBITMAP | CBitmapT<> |
HRGN | CRgnT<> |
HPALETTE | CPalletT<> |
WTL은 각 GDI 객체에 대해서는 두 가지 버전의 래퍼 클래스를 제공한다.
관리버전은 자신이 갖고 있는 객체를 소멸시키지만 비관리버전은 GDI객체의 소멸을 해당 GDI 객체의
클라이언트에게 넘겨준다. 구별방법은 bool 형의 템플릿 매개변수를 취하여 true일때는 관리버전,
false일때는 비관리버전객체를 생성한다.
HDC의 래퍼 클래스인 CDCT 클래스는 무려 240개가 넘는 메서드를 제공한다.
CDC클래스로부터 CPaintDC, CWindowDC, CClientDC, CEnhMetaFile?DC등의 클래스가 파생한다.
컨트롤 클래스와 DDX(Dynamic Data eXchange)
WTL은 모든 표준 컨트롤과 공통 컨트롤에 대하여 래퍼 클래스를 제공하며 이들 래퍼 클래스들은
해당 컨트롤에 보내느 메시지를 캡슐화하는 헬퍼 메서드를 제공한다.
예를 들어 ListBox? 컨틀로의 경우
문자열 추가하는 LB_ADDSTRING 메시지 호출을 구현하는 AddString?()이 존재하며
현재 선택된 인덱스를 구하기 위해 LB_GETCURSEL 메시지 호출을 구현하는 GetCurSel?()을 제공한다.
하지만 보통 DDX를 사용함으로써 동적으로 데이터를 손쉽게 교환할 수 있게 한다.
CWinDataExchange?클래스와 이 클래스의 DoDataExchange?()를 구현하고 DDX_로 시작하는 데이터 교환 메서드를 호출하는 여러 DDX 매크로를 통해 DDX기능을 제공한다.
DDX를 사용하고자 하는 클래스는 CWinDataExchange?클래스에서 파생하고 DoDataExchange?메서드의
구현코드는 BEGIN_DDX_MAP과 END_DDX_MAP매크로를 통해 손쉽게 구현할 수 있다.
매크로 |
목적 |
DDX_TEXT(nID, var) | 컨트롤의 텍스트 내용을 CString, CComBSTR 또는 LPTSTR 데이터 멤버와 연결시킨다. |
DDX_TEXT_LEN(nID, var, len) | DDX_TEXT와 같지만 길이를 체크하여 유효 검사를 실시한다. |
DDX_INT(nID, var) | 컨트롤의 숫자 값을 정수 클래스 멤버와 연결시킨다. |
DDX_INT_RANGE(nID, var, min, max) | 범위지정 DDX_INT |
DDX_UINT(nID, var) | unsigned int 타입용 DDX_INT |
DDX_UINT_RANGE(nID, var, min, max) | 범위지정 DDX_UINT |
DDX_FLOAT(nID, var, precision) | float 타입용 DDX_INT |
DDX_FLOAT_RANGE(nID, var, min, max) | 범위지정 DDX_FLOAT |
DDX_FLOAT_P(nID, var, precision) | 정밀도 체크하는 DDX_FLOAT |
DDX_FLOAT_P_RANGE(nID, var, min, max, precision) | 범위지정과 정밀도 체크하는 DDX_FLOAT |
DDX_CONTROL(nID, obj) | obj 매개변수에 지정된 컨트롤을 서브클래싱한다. obj 매개변수에 지정된 데이터 멤버는 반드시 CWindowImpl?클래스에서 파생해야 한다. |
DDX_CONTROL_HANDLE(nID, obj) | obj 매개변수에 지정된 컨트롤에 윈도우 핸들을 부착시킨다. |
DDX_CHECK(nID, var) | 단추의 체크 상태를 var 매개변수에 저장한다. |
DDX_RADIO(nID, var) | Radio 컨트롤 그룹과 int 데이터 멤버 사이의 정수 데이터 교환을 관리한다. |
데이터 교환을 위해서는 DoDataExchange?()를 직접 호출해야 한다.
컨트롤 <- 멤버 | DoDataExchange?(FALSE) |
멤버 <- 컨트롤 | DoDataExchange?(TRUE) |
제9장 자동화와 이중 인터페이스
자동화 개요
자동화란 무엇인가
자동화란?
어플리케이션은 자산의 기능을 노출시켜서 다른 프로그램에서 사용할 수 있도록 하는 것이다.
자동화를 제공하는 측의 어플리케이션을 자동화 컴포넌트(Automation Component)나 자동화 서버(Automation Server) 자동화 서비스를 사용하는 측의 어플리케이션을 자동화 컨트롤러(Automation Controller)나 자동화 클라이언트(Automation Client)라고 한다.
자동화 서비스를 제공하는 객체를 자동화 객체(Automation Object)
자동화의 이점과 단점
스크립트 언어를 사용하는 클라이언트 어플리케이션에서도 사용할 수 있다.
마샬링 코드를 운영체제에서 제공해 주기 때문에 프록시/스텁 DLL을 생성할 필요가 없다.
아웃-오브-프로세스 서버 컴포넌트로 구현되는 COM 객체는 마샬링 작업을 수행하는 프록시/스텁 DLL이
필요하게 된다. 하지만 자동화 기술을 사용할 때는 운영체제의 OleAuto32?.DLL 에서 자동화 컴포넌트의 표준 마샬링 코드를 제공하기 때문에 복잡한 마샬링을 작성할 필요도 없고 클라이언트마다 재분배할 필요도 없다.
단점
C++와 같이 COM 인터페이스에 직접 접근할 수 있는 언어를 사용하는 클라이언트어플리케이션에서는 속도상 불이익을 받게 된다. 이럴 때를 위해서 디스패치인터페이스(dispatch interface)를 제공해야 하며, 직접 COM 객체의 메서드를 호출하는 대신에 자동화 객체가 제공하는 IDispatch 인터페이스 Invoke()를 통하여 간접적으로 자동화 객체가 지원하는 메서드에 접근하게 된다.
단점 해결법
이중 인터페이스(dual interface)를 지원할 것을 권고하고 있다. 이것은 자동화 객체가 디스패치 인터페이스 외에도 IDispatch인터페이스에서 파생되는 커스텀 인터페이스를 함께 노출함으로써 C++와 같이 직접 COM 인터페이스에 접근할 수 있는 언어에서는 커스텀 인터페이스를 통하여 메서드를 호출함으로써 실행속도를
향상시킬 수 있다.
또다른 단점
자동화 객체가 사용할 수 있는 데이터 형이 제한된다는 점이다.
대부분의 언어에서 제공하는 표준 데이터형과 문자열, 배열등으로 사용할 수 있는 데이터형을 제한하며,
구조체와 같은 사용자 정의 데이터형을 직접지원하지 않는다. 따라서 구현하기 위해서는 복잡해진다.
COM 컴포넌트 사용자는 반드시 직접 COM 인터페이스를 접근할 수 있는 언어만은 아니라는 점이다. 이것이 자동화 기술을 사용해야 하는 궁극적인 이유이다.
IDispatch 인터페이스
IDispatch 인터페이스 개요
interface IDispatch : public IUnknown
{
public:
virtual HRESULT STDMETHODCALLTYPE GetTypeInfoCount(/*[out]*/ UINT *pctinfo) = 0;
virtual HRESULT STDMETHODCALLTYPE GetTypeInfo(
/*[in]*/ UINT iTinfo,
/*[in]*/LCID lcid,
/*[out]*/ITypeInfo **ppTinfo
) = 0;
virtual HRESULT STDMETHODCALLTYPE GetIDsOfNames(
/*[in]*/ REFIID riid,
/*[size_is][in]*/ LPOLESTR *rgszNames,
/*[in]*/ UINT cNames,
/*[in]*/ UINT lcid,
/*[size_is][out]*/ DISPID *rgDispId
) = 0;
virtual HRESULT STDMETHODCALLTYPE Invoke(
/*[in]*/ DISPID dispIdMember,
/*[in]*/ REFIID riid,
/*[in]*/ LCID lcid,
/*[in]*/ WORD wFlags,
/*[out][in]*/ DISPPARAMS *pDispParams,
/*[out]*/ VARIANT *pVarResult,
/*[out]*/ pExceptInfo,
/*[out]*/ UINT *puArgErr
) = 0;
};
// Visual Basic 코드
dim obj As Object
set obj = CreateObject("AddBack.AddBack.1") // CLSIDFromProgID()호출하여 해당하는 CLSID를 구한후 CoCreateInstance()호출하여 자동화 객체 인스턴스를 생성하고 IDispatch 인터페이스 포인터를 obj에 저장한다.
obj.Prop = propValue // GetIDsOfNames()를 이용하여 Prop이란 속성이름에 대응되는 DISPID(디스패치식별자)를 구한다.
obj.Method // 역시나 Method란 이름에 대응되는 DISPID를 구한다. 해당 컴포넌트가 지정된 속성이나 메서드를 제공하지 않는 경우 실행 에러를 발생시킨다. 해당 DISPID를 매개변수로 Invoke()를 호출한다.
DISPID는 LONG 데이터 형의 값을 갖는다. late binding에서 사용된다.
디스패치 인터페이스(dispatch interface, dispinterface)
Dispatch인터페이스 | dispinterface | ||||
IDispatch *pDisp | pVtbl->&QueryInterface? | ||||
&AddRef? | |||||
&GetTypeInfoCount? | |||||
&Release | |||||
&GetTypeInfo? | |||||
&GetIDsNames? | -> | DISPID | 이름 | ||
1 | "Prop1" | ||||
2 | "Prop2" | ||||
3 | "Method" | ||||
&Invoke | -> | DISPID | 함수 포인터 | ||
1 | &get_Prop1 | ||||
2 | &put_Prop2 | ||||
3 | &Method |
일반적으로 Invoke 함수는 COM 인터페이스와 유사한 함수 포인터 배열을 생성하고, 매개변수로 넘어온 DISPID를 이 함수 포인터 배열의 인덱스로 사용하여 매개변수로 넘어온 DISPID를 함수 포인터 배열의 인덱스로 사용하여 DISPID에 대응되는 함수를 호출하는 방식으로 구현된다. 이 때 IDispatch::Invoke 함수에 의하여 구현되는 함수 포인터 배열을 디스패치 인터페이스라고 한다. 디스패치 인터페이스가 COM 인터페이스와 유사하기는 하지만 COM 인터페이스는 아니다.
가상함수테이블의 처음 세개의 요소에는 QueryInterface?, AddRef?, Release 함수 포인터가 저장되지만
디스패치 인터페이스는 그렇지 않기 때문이다.
IDispatch::GetIDsOfNames? 메서드
virtual HRESULT STDMETHODCALLTYPE GetIDsOfNames(
/*[in]*/ REFIID riid, // 항상 ID_NULL로 예약
/*[size_is][in]*/ LPOLESTR *rgszNames, // 속성 또는 메서드 명
/*[in]*/ UINT cNames, // 개수. 한번에 하나의 속성또는 메서드에 대한 DISPID를 구하기 때문에 cNames는 항상 1이 된다.
/*[in]*/ UINT lcid, // 로케일 정보
/*[size_is][out]*/ DISPID *rgDispId // 리턴된 DISPID값. 성공적으로 호출되면 rgsznames 매개변수에 해당되는 DISPID값이 배열형태로 저장된다.
) = 0;
IDispatch::Invoke 메서드
virtual HRESULT STDMETHODCALLTYPE Invoke( /*[in]*/ DISPID dispIdMember, // DISPID /*[in]*/ REFIID riid, // 미리 예약 되어있으므로 ID_NULL /*[in]*/ LCID lcid, // 로케일 정보. GetUserDefaultCLID()를 호출하여 리턴된값을 지정하면됨. /*[in]*/ WORD wFlags, // 플래그 /*[out][in]*/ DISPPARAMS *pDispParams, // 매개변수 정보 /*[out]*/ VARIANT *pVarResult, // 리턴된 결과 /*[out]*/ pExceptInfo, // 예외 사항 정보 /*[out]*/ UINT *puArgErr // 에러 매개변수 인덱스 ) = 0;
네번째 매개변수
Visual Basic에서는 GET/SET 으로 시작되는 함수인 속성(Property)가 존재한다.
// Visual Basic에서 접근 가능한 속성 함수의 예 interface IDispatch {
HRESULT Prop(in? VARIANT_BOOL bValue); propget? ... |
이런 속성을 지원하기 위해서 함수의 종류를 몇가지로 나누어 놓았다.
wFlags 값 |
의미 |
DISPATCH_METHOD | 일반 함수. 즉, 메서드 |
DISPATCH_PROPERTYGET | 속성을 읽는 함수 |
DISPATCH_PROPERTYPUT | 속성을 저장하는 함수 |
DISPATCH_PROPERTYPUTREF | 레퍼런스로 속성을 저장하는 함수 |
다섯번째 매개변수
// OAIDL.H typedef struct tagDISPPARAMS {
DISPID *rgdispidNamedArgs?; // 이름 갖는 매개변수의 DISPID 배열 UINT cArgs; // 매개변수의 수 UINT cNamedArgs?; // 이름 갖는 매개변수의 수 |
여섯번째 매개변수
메서드나 propget속성의 결과를 저장하는 VARIANT 포인터
포인터를 리턴하지 않는 메서드나 propput또는 propputref 속성에 대해서는 NULL이 된다.
* 꼭 알아둘 것은 자동화 객체의 매개변수나 리턴되는 결과값은 언제나 VARIANT 데이터 형이라는 점이다.
일곱번째 매개변수
typedef struct tagEXCEPINFO {
WORD wReserved; BSTR bstrSource; // 예외 발생 소스 BSTR bstrDescription; // 예외 설명 BSTR bstrHelpFile?; // 도움말 경로명 DWORD dwHelpContext?; // 도움말 컨텍스트 ULONG pvReserved; ULONG pfnDeferredFillIn?; // 구조체를 채우는 함수 SCODE scode; // 리턴값 |
예외 매개변수 사용예
EXCEPINFO excepinfo; HRESULT hr = pIDisp->Invoke(..., &excepinfo); if(FAILED(hr) {
{
|
* 리턴값이 DISP_E_PARAMNOTFOUND나 DISP_E_TYPEMISMATCH 인 경우
Invoke 함수의 마지막 매개변수 puArgErr?에는 에러에 대응되는 매개변수의 인덱스가 리턴된다.
IDispatch 인터페이스 사용
생략
이중 인터페이스
이중 인터페이스란?
디스패치 인터페이스는 VB에서 사용가능하지만 결국 느리다.
IDispatch::Invoke 함수를 통하여 모든 기능을 제공하는데.
많은 매개변수를 스택에 저장하고
DISPPARAMS와 VARIANTARG 구조체를 생성하고 이 들 매개변수를 채운후에야 Invoke()를 호출한다.
데이터를 받을 때에도 마찬가지로 데이터 형 변환과 검사를 해야하므로 많은 오버헤드가 있다.
결국 이중 인터페이스란? IDispatch::Invoke()를 통하여 사용할 수 있는 모든 함수를 인터페이스 테이블, 즉 가상 함수 테이블(vtable)을 통해서도 직접 사용할 수 있게 하도록 구현된 디스패치인터페이스를 말한다.
후기 바인딩과 초기 바인딩
후기 바인딩은 IDispatch::GetIDsOfNames?()를 호출하여 실행 시간에 속성이나 메소드에 대응되는
DISPID를 구하는 것이다. 속도가 느리며 자동화 컨트롤에서 잘못된 속성명이나 메서드명을 사용한 경우 실행 에러를 발생시킨다. ID 바인딩은 각 속성이나 메소드에 대응되는 DISPID는 항상 고정되며 이 값에 대한 정보는 형식 라이브러리(tyep library)에 저장된다.
자동화 컨트롤이 GetIDsOfNamed?()를 호출할 필요없이 형식 라이브러리에서 ID를 읽어서 호출한다.
이 방법은 후기 바인딩에 비해서 속도가 약 두배는 빠르다. 하지만 IDispatch인터페이스와 형식 라이브러리를
제공해야 한다. 초기바인딩은 ID 바인딩과 같이 형식 라이브러리에서 DISPID를 구한다.
하지만 IDispatch 인터페이스를 지원하지 않아도 된다.
대신 가상 함수 테이블로 구현된 인터페이스를 사용하게 된다.
가상 함수 테이블을 사용하므로 vtable 바인딩이라고도 한다.
COM을 지원하는 C++ 컴파일러는 초기바인딩을 지원한다.
VB에서는 특정 클래스 형을 사용하여 객체 변수를 선언한 경우 초기 바인딩을 사용한다.
자동화와 형식 라이브러리
6.2에서 이미 살펴본바 있는 형식 라이브러리는 COM 객체에 대한 정보를 저장하고 있는 이진 파일로,
ODL(Object Description Language)라고 하는 스크립트 언어를 사용하여 자동화 객체를 설명하는 .ODL 파일
을 작성하고 MkTypLib?.exe 유틸로 ODL을 컴파일하여 .TLB 확장자를 갖는 형식 라이브러리를 생성한다.
중요!!
책 426에 ODL 파일과 IDL 파일, IDL을 midl.exe로 컴파일한 header파일의 비교
ODL과 IDL은 유사한 문법과 약간의 차이를 가지고 있다.
자동화 데이터 타입
VARIANT 데이터 형
typedef struct tagVARIANT {
WORD wReserved1; WORD wReserved2; WORD wReserved3; union {
unsigned char bVal; // VT_Ul1 short iVal; // VT_I2 float fltVal; // VT_R4 double dblVal; // VT_R8 VARIANT_BOOL boolVal; // VT_BOOL SCODE scode; // VT_ERROR CY cyVal; // VT_CY DATE date; // VT_DATE BSTR bstrVal; // VT_BSTR IUnknown *punkVal; // VT_UNKNOWN LPDISPATCH pdispVal; // VT_DISPATCH SAFEARRAY *parray; // VT_ARRAY unsigned char *pbVal; // VT_BYREF | VT_Ul1 short *piVal; // 나머지 생략 long *plVal; float *pfltVal; double *pdblVal; VARIANT_BOOL *pboolVal; SCODE *pscode; CY *pcyVal; DATE *pdate; BSTR *pbstrVal; IUnknown **ppunkVal; LPDISPATCH *ppdispVal; SAFEARRAY **pparray; struct tagVARIANT *pvarVal; void *byref; CHAR cVal; USHORT uiVal; ULONG ulVal; INT intVal; UINT uintVal; DECIMAL *pdecVal; CHAR *pcVal; USHORT *puiVal; ULONG *pulVal; INT *pintVal; UINT *puintVal; struct { } __VARIANT_NAME_4; DECIMAL decVal; typedef VARIANT VARIANTARG; |
// VARIANT 구조체 인스턴스의 데이터 형을 다른 데이터 형으로 변환하고자 할때 사용 예 int ret = vDest.intVal; |
VARIANT 구조체는 VB 디폴트 데이터 형인 Variant 데이터 형과 동일하다.
C++과 같은 인터페이스 직접 접근 할VARIANT 구조체를 사용하면 사용법이 불편하고 속도상의 불이익을
가지지만 VB와 같은 언어에서는 필수적이다.
BSTR 데이터 형
와이드 문자열 포인터로 사용된다.
typedef OLECHAR *BSTR; |
BSTR 데이터형의 구조
문자개수 | 문자 |
한마디로 BSTR bstr = L"this is wrong"; 는 사용할 수 없다.
BSTR의 사용은 SysAllocString? Win32 API 함수를 호출함으로서 사용할 수 있다.
wchar_t szStr[] = L"This is right!"; BSTR bstr; bstr = SysAllocString?(szStr); ... SysFreeString?(bstr); |
SAFEARRAY 데이터 형
배열의 범위데 대한 정보를 포함하는 배열을 나타낸다.
typedef struct tagSAFEARRAY {
USHORT fFeatures; // 플래그 ULONG cbElements; // 배열의 요소의 개수 ULONG cLocks; // 락 카운터 PVOID pvData; // 배열의 데이터 포인터 SAFEARRAYBOUND rgsabound[1]; // 각 배열 차원 바운드 typedef struct tagSAFEARRAYBOUND {
LONG lLbound; // 배열 하위 바운드 |
자동화 라이브러리 OLEAUT32.DLL에는 SAFEARRAY 데이터 형을 조작할 수 있는 SafeArray?로 시작하는
함수들이 있다.
IDispatchEx? 인터페이스
IDispatch 인터페이스 외에도 실행 시에 메서드와 속성을 동적으로 추가할 수 있는 확장 객체(expand object)를 생성할 수 있는 7개의 새로운 메서드를 제공한다. 이와 함께 사용되지 않는 매개변수를 사라졌다.
자세한 내용은 일단 생략
ATL 자동화 컴포넌트 구현
또 생략
제10장 커넥션 포인트와 이벤트
자동화 객체는 클라이언트인 자동화 컨트롤러에게 이벤트를 발생시킬 수 있다.
커넥션 포인트 매커니즘
이벤트(event)란?
자동화 객체가 자신에게 어떠한 사건이 발생했다는 사실을 자동화 컨트롤에 알려주는 기능이다.
자동화 컨트롤러가 자동화 객체에게 어떤 작업을 요청하기 위해 메서드를 호출한다면 역으로 자동화 객체가 자동화 컨트롤에게 어떤 작업을 요청하기 위해서는 이벤트를 발생시켜야 한다.
이벤트는 커넥션 포인트(connection point)라는 메커니즘으로 구현된다.
일반적으로 자동화 객체는 하나 이상의 인터페이스를 구현하여 기능을 노출시키지만 커넥션 포인트는
그 반대 메커니즘이다.
클라이언트가 인터페이스를 구현하고, 구현한 인터페이스 포인터를 자동화 객체에게 넘겨주어 호출하게 하는 것이다. 커넥션 포인트는 두개의 부분으로 구성된다.
그 하나는 이벤트를 발생시키는 소스 인터페이스(source interface)이고 다른 하나는 이벤트를 받아들이는
싱크 인터페이스(sink interface)이다. 소스 인터페이스를 구현한 객체를 소스 객체(source object)라고 하며,
싱크 인터페이스를 구현한 객체를 싱크 객체라고 한다.
일반적으로 자동화 객체는 소스 인터페이스의 구현 코드를 포함하고 있으므로 소스 객체는 자동화 객체가
되며 싱크 객체는 자동화 컨트롤러 안에서 별도로 생성된다. 사실, 이벤틀르 발생시키기 위해 자동화 객체에서 호출하는 메서드는 자동화 컨트롤러에서 제공하는 싱크 인터페이스의 메서드이다.
정리하면
특정한 인터페이스를 통해 메서드를 호출하는 객체를 소스 객체 또는 커넥션 포인트라고 하며,
인터페이스를 구현하고 호출을 받는 객체를 싱크 객체라고 한다.
클라이언트는 싱크로서 싱크 인터페이스를 구현하는 싱크 객체를 생성하고, 자동화 객체는 소스로서
싱크 인터페이스 메서드를 호출한다.
구현 방법은 또 생략
제11장 COM 컴포넌트 재사용
포함과 통합
COM 에서의 재사용성
포함과 통합은 하나의 COM 객체가 다른 COM 객체를 재사용하는 기법이다.
이들 두 COM 객체를 외부 COM 객체(outer COM object 또는 outer COM component)와 내부 COM 객체(inner COM object 또는 inner COM component)로 구별하며 외부 COM 객체는 내부COM 객체를 포함하거나 통합하여 COM 객체를 재사용한다.
포함(containment)
외부 COM객체에서 내부 COM객체의 인스턴스를 생성시켜 단순히 내부COM객체에 그 실행을 위임하는 방식이다. QueryInterface?()호출시 그 외부 COM객체의 인터페이스를 노출시킨다.
그리고 내부 COM객체의 메서드를 외부 COM 객체에 생성시켜 내부 COM 객체의 호출로 연결시켜 준다.
// 예 class public IAddBack?, IAddEnd? { } ... HRESULT CAddBack?::Init()
hr = CoCreateInstacne?(CLSID_AddEnd?, NULL, CLSCTX_ALL, IID_IAddEnd?, (LPVOID*)&m_pAddEnd?); ... STDMETHODIMP CAddBack?::get_AddEnd?(short *pVal) |
통합(aggregation)
외부 COM객체외 내부 COM객체를 통합하여 내부 COM객체의 인터페이스를 직접 클라이언트에게 노출시킨다. 즉 내부COM 객체의 인터페이스를 재구현하는 포함과는 달리, 클라이언트에서 내부 COM객체의 인터페이스를 요청할 때 외부 COM 객체는 내부 COM 객체의 인터페이스 포인터를 클라이언트에 직접 전달한다.
그래서 내부 COM 객체의 인터페이스 메서드 호출시 실제 내부 COM 객체에 구현된 메서드가 실행된다.
// 예 class public IAddBack? {
... HRESULT CAddBack?::Init()
hr = CoCreateInstacne?(CLSID_AddEnd?, (IUnknown*)this /* 외부 Unknown */, CLSCTX_ALL, IID_IUnknown, (LPVOID*)&m_pUnk); ... |
하지만 AddEnd? 클래스 팩토리에서 CreateInstane?() 호출시 통합 지원 여부를 허락 해주어야 한다.
HRESULT __stdcall CFAddEnd?::CreateInstance?(LPUNKNOWN pUnkOuter?, REFIID riid, LPVOID *ppv) { ppv = NULL; if(pUnkOuter?!= NULL) // 현재는 통합을 지원하지 않는다. 이부분을 pUnkOuter? !=NULL&& riid !=IID_IUnknown 으로 고치면 통합을 지원하게 된다. {
if(pAddEnd?!=null) {
hr=pAddEnd?->QueryInterface?(riid, ppv); if(FAILED(hr))
else
return hr; } |
ATL 에서의 포함 구현
쭉쭉 실습 생략
제12장 컬렉션 구현
컬렉션 개요
반복적인 작업을 수행할 수 있는 유사한 객체 그룹을 포함하고 있는 자동화 객체를 일컫는다.