1.      COM개요

1.1.   COM 의 이점

1.1.1.   왜 COM 이 필요한가?

컴포넌트 개체 모델(Component Object Model, COM)은 자신의 고유한 기능을 제공하는 단위 어플리케이션 즉, 컴포넌트의 통합 및 커뮤니케이션 방법에 대한 표준을 정의한 사양이다.

1.1.2.   컴포넌트 소프트웨어의 조건

1.       COM 컴포넌트는 언어 독립적이어야 한다.

2.       COM 컴포넌트는 이진(binary) 형태로 제공되어야 한다.

3.       COM 컴포넌트는 버전 호환성을 제공해야 한다.

4.       COM 컴포넌트는 위치 투명성(location transparency)을 제공해야 한다.

1.2.   인터페이스 개요

1.2.1.   인터페이스란?

COM 개체가 자신의 기능을 클라이언트에 노출시키는 기본적인 방법이다.

프로그램 상에서 인터페이스는 C++ 와 같은 OOP 언어에서의 순수한 가상 함수(pure virtual function) 만을 멤버로 포함하는 추상적인 클래스(abstract class)로 표현된다. 인터페이스는 가상 함수 테이블(virtual function table, VTBL) 형태의 메모리 구조를 정의한다.

인터페이스를 직접 클래스로 표현하는 대신에 C 언어와 같이 클래스를 지원하지 않는 언어와 함께 사용할 수 있도록 MS Win32 SDK 에서는 OBJBASE.H 헤더 파일에는 다음과 같은 매크로를 제공한다.

#define interface struct

#define STDMETHOD(method)       virtual HRESULT STDMETHODCALLTYPE method

#define STDMETHOD_(type,method) virtual type STDMETHODCALLTYPE method

#define PURE                    = 0

#define THIS_

#define THIS                    void

#define DECLARE_INTERFACE(iface)    interface iface

#define DECLARE_INTERFACE_(iface, baseiface)    interface iface : public baseiface

#define STDMETHODIMP            HRESULT STDMETHODCALLTYPE

#define STDMETHODIMP_(type)     type STDMETHODCALLTYPE

// C++에서 인터페이스는 다음과 같이 클래스로 정의할 수 있다.

class IFoo

{

public:

HRESULT __stdcall Method1() = 0;

HRESULT __stdcall Method2() = 0;

HRESULT __stdcall Method3() = 0;

};

// OBJBASE.H에 정의된 매크로를 사용하여 다음과 같이 인터페이스를 표현.

#include <objbase.h>

DECLARE_INTERFACE(IFoo)

{

STDMETHOD(Method1) (THIS_ short) PURE;

STDMETHOD(Method2) (THIS_ short) PURE;

STDMETHOD(Method3) (THIS_ short) PURE;

};

__stdcall 이란 MS 컴파일러 확장 예약어로, __stdcall로 표시된 함수는 Pascal 호출규약(calling convention)을 사용한다. Pascal 호출 규약을 사용하는 함수는 호출한 함수로 리턴 하기 전에 스택에서 매개변수를 삭제한다.

C/C++ 호출 규약을 사용하는 함수는 호출한 함수에서 스택에 있는 매개변수를 삭제하게 된다. Visual Basic 과 같은 대부분의 다른 언어에서는 기본적으로 Pascal 호출 규약을 사용한다. 또한, 모든 Win32 API 함수는 Pascal 호출 규약을 사용하므로, Pascal 호출 규약은 표준 호출 규약이 된다.

1.2.2.   인터페이스 메모리 구조

인터페이스에서 파생되는 클래스

// 인터페이스 파생 클래스의 메서드 정의.

class CImplIfoo : public IFoo

{

public:

HRESULT __stdcall Method1() {...};

HRESULT __stdcall Method2() {...};

HRESULT __stdcall Method3() {...};

};

// OBJBASE.H에 정의된 매크로를 사용하여 다음과 같이 인터페이스 정의.

#include <objbase.h>

class CImplIFoo : public IFoo

{

public:

STDMETHODIMP Method1() {...};

STDMETHODIMP Method2() {...};

STDMETHODIMP Method3() {...};

};

C++ 문법에서 가상함수를 포함하는 클래스에서 생성된 개체는 가상 함수 테이블 포인터를 갖게 된다.

이 가상 함수 테이블 포인터를 ‘인터페이스 포인터’ 라고 하며, 일단 인터페이스 포인터를 갖는 클라이언트에서는 가상 함수 테이블을 통하여 멤버함수 즉, 메서드를 호출할 수 있게 된다.

1.2.3.   인터페이스 상속과 구현 상속

COM 은 인터페이스를 통하여 다형성 (polymorphism)을 지원하게 된다. 그러나 COM 은 상속성(inheritance)을 지원하지 않는다.

구현상속(implementation inheritance)

1.       일반적인 상속방법

2.       언어종속적

3.       기초 클래스가 손상되기 쉽다

인터페이스상속(interface inheritance)

1.       순수한 가상함수만을 포함하는 추상적인 클래스 즉, 인터페이스를 기초클래스로 파생클래스를 구현한다.

2.       구현상속의 단점이 해결됨.

3.       COM 에서 사용

4.       문제점은 이미 구현된 코드를 전혀 재사용할 수 없다는 것이다. 이 문제는 COM 의 포함(Containment) 과 통합(Aggregation) 이라는 방법으로 해결된다.

1.3.   IUnknown인터페이스

1.3.1.   IUnknown 인터페이스가 필요한 이유

1.       인터페이스는 서로 독립적이다. 따라서 COM 개체는 자신이 제공하는 다른 인터페이스의 포인터를 클라이언트가 구할 수 있는 방법을 제공해야 한다.

2.       여러 클라이언트에서 COM 개체를 동시에 사용할 수도 있다는 사실

3.       이러한 이유로 모든 COM 개체는 반드시 IUnknown 인터페이스에서 파생되어야만 한다. IUnknown 인터페이스는 모든 COM 개체들이 반드시 갖추어야 할 기본적인 서비스에 대한 기준을 제공한다.

4.       UNKNWN.H

virtual HRESULT __stdcall QueryInterface(

/* [in] */ REFIID riid,

/* [iid_is][out] */ void** ppv) = 0;

virtual ULONG __stdcall AddRef(void) = 0;

virtual ULONG __stdcall Release(void) = 0;

// IUnknown 인터페이스 적용 예.

class IFoo : public IUnknown

{

public:

HRESULT __stdcall Method1() = 0;

HRESULT __stdcall Method2() = 0;

HRESULT __stdcall Method3() = 0;

};

// IUnknown 인터페이스를 매크로를 사용한 예.

#include <objbase.h>

DECLARE_INTERFACE_(IFoo, IUnknown)

{

STDMETHOD(Method1) (THIS_ short) PURE;

STDMETHOD(Method2) (THIS_ short) PURE;

STDMETHOD(Method3) (THIS_ short) PURE;

};

// IFoo 인터페이스를 구현한 클래스.

class CImplIFoo : public IFoo

{

public:

// IUnknown 메서드

HRESULT __stdcall QueryInterface(REFIID riid, void** ppv) {...};

ULONG __stdcall AddRef(void) {...};

ULONG __stdcall Release(void) {...};

// IFoo 메서드

HRESULT __stdcall Method1() {...};

HRESULT __stdcall Method2() {...};

HRESULT __stdcall Method3() {...};

};

// objbase.h에 정의된 매크로를 사용한 예.

#include <objbase.h>

class CImplIFoo : public IFoo

{

public:

// IUnknown 메서드

STDMETHODIMP QueryInterface(REFIID riid, void** ppv) {...};

STDMETHODIMP_(ULONG) AddRef(void) {...};

STDMETHODIMP_(ULONG) Release(void) {...};

// IFoo 메서드

STDMETHODIMP Method1() {...};

STDMETHODIMP Method2() {...};

STDMETHODIMP Method3() {...};

};

1.3.2.   QueryInterface

IUnknown 인터페이스는 클라이언트가 COM 개체의 다른 인터페이스를 요청할 때 해당 인터페이스의 포인터를 리턴 하는 QueryInterface 멤버를 포함한다.

HRESULT __stdcall QueryInterface(REFIID riid, void** ppv);

<TYPES.H>

#define REFIID const IID &

1.       riid 매개변수는 IID (인터페이스 식별자 구조체)의 레퍼런스(reference)로, 클라이언트가 요청하는 인터페이스에 대한 정보를 포함하는 입력 매개변수이다.

2.       ppv 에는 클라이언트가 riid 매개변수를 통해 요청한 인터페이스에 대한 QueryInterface 함수의 실행 결과인 인터페이스 포인터가 저장되는 출력 매개변수이다. COM 컴포넌트에 클라이언트가 요청한 인터페이스가 없다면 ppv에는 NULL이 저장된다.

// 클라이언트에서 QueryInterface 함수 사용 예

void foo(IUnknown* pI)

{

// 요청할 인터페이스의 포인터를 저장할 변수 선언

IFoo* pIFoo = NULL;

// IFoo 인터페이스 요청

HRESULT hr = pI->QueryInterface(IID_Foo, (void**) &pIFoo);

// 성공 여부를 검사하여

if (SUCCEEDED(hr))

{

// 성공했다면 인터페이스를 사용한다.

pIFoo->Method1();

}

}

1.3.3.   AddRef, Release

여러 개의 클라이언트에서 COM 개체를 동시에 사용할 수도 있으며 따라서, 어떤 하나의 클라이언트에서 다른 클라이언트에서는 아직도 사용중인 COM 개체를 소멸시키는 경우에는 당연히 문제가 발생한다. 이 문제를 해결하기 위해 COM 개체 스스로가 자신이 몇 번 참조되고 있는가 하는 횟수를 관리하고, 이 레퍼런스 카운터가 0일 때 스스로를 소멸시키는 방법을 사용한다.

ULONG AddRef(void);

ULONG Release(void);

위 두 함수는 모두 레퍼런스 카운터를 나타내는 ULONG 값을 리턴 하지만, 디버깅을 위해 사용되는 것일 뿐, 이 리턴 값에 의존하지 말아야 한다.

레퍼런스 카운터가 0일 때 COM 개체 스스로가 자신을 소멸시키게 할 수 있다. 그러나 이들 함수를 사용할 때는 지켜야 할 몇 가지 규칙이 있다.

1.       인터페이스를 리턴 하는 함수에서는 리턴 하기 전에 해당 인터페이스 포인터에 대하여 항상 AddRef 함수를 호출해야 한다.

2.       인터페이스 사용이 끝나면 해당 인터페이스 포인터에 대하여 항상 Release 함수를 호출해야 한다.

3.       인터페이스 포인터를 다른 인터페이스 포인터에 대입한 경우에도 해당 인터페이스 포인터(LValue)에 대하여 AddRef 함수를 호출해야 한다.

// AddRef와 Release 함수 사용 예

void foo(IUnknown* pI)

{

// 요청할 인터페이스의 포인터를 저장할 변수 선언

IFoo* pIFoo = NULL;

IFoo* pIAnotherFoo = NULL;

// IFoo 인터페이스 요청

HRESULT hr = pI->QueryInterface(IID_Foo, (void**) &pIFoo); // RefCount == 1

// 성공 여부를 검사하여

if (SUCCEEDED(hr))

{

// 성공했다면 인터페이스를 사용한다.

pIFoo->Shift(1);

}

// pIFoo 포인터를 통하여 IFoo 인터페이스를 사용한다.

pIFoo->Method1();

// 인터페이스 포인터를 대입한다.

pIAnotherFoo = pIFoo;

pIAnotherFoo->AddRef(); // RefCount == 2

// pIFoo 포인터를 통하여 IFoo 인터페이스 사용이 끝나면

pIFoo->Release(); // RefCount == 1

// pIAnotherFoo 포인터를 통하여 IFoo 인터페이스를 사용한다

pIAnotherFoo->Method1();

// pIAnotherFoo 포인터를 통하여 IFoo 인터페이스 사용이 끝나면

pIAnotherFoo->Release(); // RefCount == 0

}

1.3.4.   IDL

인터페이스는 C++의 클래스로 정의하거나, 또는 SDK에서 제공하는 매크로를 사용하여 정의할 수 있다.

COM 에서 인터페이스를 정의할 수 있는 표준 개발 도구가 IDL(Interface Definition Language) 이다. COM 에서 사용하는 IDL은 OSF(Open Software Foundation)에서 빌려와 사용하였던 RPC(Remote Procedure Call)의 IDL을 확장한 것이다. 이와 함께 MS는 IDL 정의 파일을 컴파일 할 수 있는 MIDL(Microsoft IDL) 컴파일러를 제공한다.

// IFoo 인터페이스를 IDL을 사용하여 정의한 예

[

object

uuid(C6F96D90-9FCF-11d1-B20A-0060970A3516),

]

interface IFoo : IUnknown

{

import "unknwn.idl";

HRESULT Method1();

HRESULT Method2();

HRESULT Method3();

};

// 위의 IDL 파일을 MIDL 컴파일러를 사용하여 컴파일 하면 MIDL 컴파일러는 다음과 같이 IFoo 인터페이스를 정의하는 코드를 생성해 준다.

interface DECLSPEC_UUID(“C6F96D90-9FCF-11d1-B20A-0060970A3516”)

IFoo : public IUnknown

{

public:

virtual HRESULT STDMETHODCALLTYPE Method1() = 0;

virtual HRESULT STDMETHODCALLTYPE Method2() = 0;

virtual HRESULT STDMETHODCALLTYPE Method3() = 0;

};

IDL과 MIDL 컴파일러를 사용하여 다음과 같은 작업을 할 수 있다.

1.       인터페이스를 정의한 코드를 포함하는 헤더 파일을 생성한다.

2.       로컬서버(local server) 또는 리모트서버(remote server) 컴포넌트의 커스텀 인터페이스에 대한 마샬링(marshaling) 기능을 제공하는 프록시(proxy)와 스텁(stub) 코드를 생성한다.

3.       자동화(automation)에서 사용되는 형식 라이브러리(type library)를 생성한다.

1.3.5.   표준 COM 인터페이스와 커스텀 COM 인터페이스

COM 인터페이스는 COM 에 의하여 정의된 표준 COM 인터페이스(standard COM interface)와 개발자에 의해 정의된 커스텀 COM 인터페이스(custom COM interface)로 나뉘어진다.

1.       커스텀 COM 인터페이스: 개발자가 자신의 목적에 따라 명확하게 정의된 인터페이스. 커스텀 COM 인터페이스를 노출하는 COM 컴포넌트가 로컬 서버로 구현될 때 해당 COM 컴포넌트는 마샬링(marshaling) 코드를 제공해야 한다.

2.       표준 COM 인터페이스: 인터페이스가 COM 에 의해 제공되므로 여러분이 명확하게 인터페이스를 정의할 필요가 없다. IUnknown, IClassFactory 등의 인터페이스가 표준 COM 인터페이스에 속한다. 여기에는 OLE 나 ActiveX 기술에 포함되는 OLE Document, ActiveX Document, ActiveX Control, Automation 등에 사용되는 인터페이스가 포함되며, MS에 의하여 윈도우 운영체제 확장 컴포넌트로 제공되는 OLE DB나 Active Directory 등에 사용되는 인터페이스도 넓은 의미에서 표준 COM 인터페이스로 간주할 수 있다. 이들 표준 COM 인터페이스를 노출하는 COM 컴포넌트가 로컬 서버로 구현되는 경우에 해당 COM 컴포넌트는 마샬링코드가 ole32.dll과 oleauto32.dll을 통하여 운영체제 레벨에서 자동적으로 제공되기 때문에 마샬링 코드를 제공할 필요가 없다.

1.4.   GUID와 HRESULT

1.4.1.   GUID

GUID(Globally Unique Identifier) 란 128비트(16바이트)의 크기를 갖는 구조체로, 전세계적으로 시간과 장소에 관계없이 고유하다고 보장할 수 있는 값을 나타내는 식별자로 사용된다. GUID를 UUID(Universally Unique Identifier) 라고도 하며, WTYPES.H 헤더 파일에 다음과 같이 정의되어 있다.

typedef struct _GUID

{

DWORD Data1;

WORD Data2;

WORD Data3;

BYTE Data4[8];

} GUID

인터페이스ID 즉, IID (Interface Identifier)는 인터페이스를 식별하는데 사용되는 GUID로 다음과 같이 정의된다.

typedef GUID IID;

결국 IID와 GUID는 같다.

COM 개체도 자신을 식별하는데 사용되는 GUID를 갖는다. 이것을 클래스 ID(Class Identifier, CLSID)라고 하며 다음과 같이 정의된다.

typedef GUID CLSID;

Visual C++ 에서는 GUID를 생성하는 두 가지 툴을 제공한다. 도스 창에서 사용할 수 있는UUIDGEN.exe 와 Visual C++ 예제 프로그램으로도 제공되는 GUIDGEN.exe 이다.

이들 툴은 COM 라이브러리의 CoCreateGuid 함수를 호출하고, 다시 이 함수는 Win32 RPC API 함수인 UUIDCreate 를 호출하여 내부 알고리즘에 의하여 GUID를 생성한다. 이들 툴은 Visual Studio 설치 디렉토리의 Common 폴더의 Tools 디렉토리에 있다.

일반적으로 인터페이스의 GUID 식별자명은 ‘IID_’로 시작하며,

COM 개체의 GUID식별자명은 ‘CLSID_’로 시작한다.

DEFINE_GUID 매크로는 GUID 구조체 형식에 일치하도록 매크로 확장을 한다.

이 매크로는 INITGUID.H 헤더 파일에 정의되어 있으며, 이 매크로를 사용하기 위해서는 반드시 다음과 같은 순서로 헤더 파일을 #include 해야 한다.

#include <objbase.h>

#include <initguid.h>

OBJBASE.H 헤더 파일에는 GUID를 비교하는 operator== 함수가 정의되어 있으므로 2개의 GUID를 비교하는데 사용할 수 있다. 이 함수와 같은 기능을 하는 함수에는 IsEqualGUID, IsEqualIID, IsEqualCLSID 등이 있다.

GUID는 16바이트 크기를 갖기 때문에 레퍼런스(reference)로 넘겨주는 것이 효율적이다. 이것이 QueryInterface 함수의 첫 번째 매개변수가 REFIID 즉, const IID & 데이터 형을 갖는 이유이다.

COM 라이브러리에서는 GUID를 문자열로 변환하는 StringFromGUID2 함수를 제공한다.

// StringFromGUID2 함수의 사용방법

TCHAR szIID[129];

wchar_t wszIID[129];

int r = ::StringFromGUID2(IID_Foo, wszIID, 128);

wcstombs(szIID, wszIID, 128);

IUnknown 인터페이스의 IID는 UNKNWN.H에 다음과 같이 선언되어 있으며, IUnknown 인터페이스를 비롯한 다른 표준 인터페이스의 IID는 UUID.LIB에 정의된다. 따라서, COM 컴포넌트를 어플리케이션에 UUID.LIB 파일을 링크시켜야 한다.

EXTERN_C const IID IID_IUnknown;

1.4.2.   HRESULT

S_OK

함수가 성공함. 함수는 참(true)를 리턴한다. S_OK는 0으로 정의되어 있다.

NOERROR

S_OK와 동일

S_FALSE

함수가 성공함. 함수는 거짓(false)를 리턴한다. S_FALSE는 1로 정의되어 있다.

E_UNEXPECTED

예기치 못한 실패

E_NOTIMPL

멤버함수에 구현 코드가 포함되어 있지 않음

E_NOINTERFACE

요청한 인터페이스를 컴포넌트가 지원하지 않음

E_OUTOFMEMORY

요청한 메모리를 컴포넌트가 할당하지 못함

E_FAIL

지정되지 않은 실패

SUCCEEDED(), FAILED() 매크로를 사용한다.

void foo(IUnknown* pI)

{

IFoo* pIFoo = NULL;

HRESULT hr = pI->QueryInterface(IID_IFoo, (void**) &pIFoo);

if (hr == S_OK) // 이렇게 비교 불가

{

pIFoo->Method1();

}

pIFoo->Release();

}

void foo(IUnknown* pI)

{

IFoo* pIFoo = NULL;

HRESULT hr = pI->QueryInterface(IID_IFoo, (void**) &pIFoo);

if (SUCCEEDED(hr))

{

pIFoo->Method1();

}

pIFoo->Release();

}

2.      COM 사용

2.1.   COM 클라이언트 어플리케이션

2.1.1.   COM 클라이언트, COM서버 어플리케이션

COM 은 클라이언트/서버(Client/Server) 모델을 사용한다.

l        COM 컴포넌트는 어떤 프로세스에서 생성되는가에 따라 [In-Process Server] 와 [Out-of-Process Server]로 나뉜다.

l        [Out-of-Process Server]는 어떤 위치에서 생성되는가에 따라 [Local Server] 와 [Remote Server]로 분류된다.

2.1.2.   COM 컴포넌트 등록

COM 컴포넌트는 반드시 시스템 레지스트리에 등록되어 있어야 한다. 따라서, COM 컴포넌트의 설치 프로그램에서 시스템 레지스트리에 등록하는 기능을 제공하든가, 아니면 다음과 같은 방법으로 시스템 레지스트리에 직접 등록할 수 있다.

l        [In-Process]인 경우, Visual C++에서 제공하는 regsvr32.exe를 사용하여 도스 창에서 다음과 같은 명령을 입력한다.
regsvr32 ServerName.dll
regsvr32 ControlName.ocx

l        [Out-of-Process]인 경우, 직접 실행될 수 있기 때문에 regsvr32.exe를 사용하지 않고 다음과 같이 도스 창에서 ‘/regserver’ 또는 ‘-regserver’ 옵션으로 서버 어플리케이션을 실행한다.
ServerName.exe /RegServer
또는
ServerName.exe -RegServer

2.1.3.   COM 컴포넌트 해제

l        [in-process]: regsvr32 /u ServerName.dll

l        [Out-of-Process]:
ServerName.exe /unregserver
또는
ServerName.exe -unregserver

2.1.4.   COM 클라이언트 어플리케이션 생성 과정 개요

1.       COM 라이브러리(COM Library)를 초기화한다.

2.       COM 컴포넌트가 제공하는 COM 개체의 CLSID를 구한다.

3.       COM 개체의 인스턴스를 생성한다.

4.       COM 개체에서 제공하는 인터페이스의 포인터를 구하여 해당 인터페이스가 제공하는 메서드를 호출함으로써 COM 개체가 제공하는 서비스를 사용한다.

5.       COM 컴포넌트의 사용이 끝나면 COM 라이브러리의 초기화를 해제한다.

2.2.   COM 라이브러리 초기화와 해제

2.2.1.   COM 라이브러리

COM 은 컴포넌트의 통합 및 커뮤니케이션 방법에 대한 표준을 정의한 사양(specification)이다. 이와 함께 COM 은 COM 라이브러리를 통하여 다음과 같은 기능을 포함하는 구현 코드를 제공한다.

1.       COM 클라이언트와 서버 어플리케이션의 생성기능을 제공하는 몇 가지 기본적인 API 함수를 제공한다. 클라이언트 어플리케이션에 대해서는 COM 은 COM 개체를 생성하는 기본적인 함수를 제공하며, 서버 어플리케이션에 대해서는 COM 개체를 노출시키는 기능을 제공한다.

2.       클래스ID(CLSID)를 통하여 어떤 서버가 해당 클래스를 구현하고 있고, 그 서버가 어디에 위치하고 있는지를 시스템 레지스트리에서 찾아내는 기능을 제공한다.

3.       COM 개체가 로컬 또는 리모트 서버에서 실행되고 있을 때 클라이언트를 대신하여 투명하게 RPC 호출을 해 준다.

4.       어플리케이션이 자신의 프로세스 영역 안에서 메모리를 할당을 제어하는 표준 메커니즘을 제공한다.

즉 COM 라이브러리는 COM 을 사용하는 모든 어플리케이션에서 유용하게 사용될 수 있는 컴포넌트 관리 서비스를 제공한다. COM 라이브러리는 COMPOBJ.DLL 이란 이름으로 윈도우 운영체제에 의하여 제공되며, 윈도우 System 폴더에서 찾을 수 있다.

2.2.2.   COM 라이브러리 초기화

CoGetMalloc 함수를 포함하는 메모리 할당 함수를 제외한 COM 라이브러리의 모든 함수는 반드시 CoInitialize 함수를 호출하여 COM 라이브러리를 초기화하여야 사용할 수 있다.

HRESULT CoInitialize(

LPVOID pvReserver    // 예약됨. 반드시 NULL이어야 함.

);

// CoInitialize 사용 예

BOOL CAddFrontApp::InitInstance()

{

// COM Initialization

HRESULT hr;

hr = ::CoInitialize(NULL);

if (FAILED(hr))

{

AfxMessageBox("COM 라이브러리를 초기화할 수 없습니다!");

return FALSE;

}

...

}

2.2.3.   COM 라이브러리 초기화 해제

CoUninitialize 함수는 CoInitialize 함수와 짝을 이루어 호출되어야 한다. CoInitialize 함수가 여러 번 호출되었다면 설사 CoInitialize 함수가 S_FALSE 를 리턴 했다고 해도 그만큼의 CoUninitialize 함수가 호출되어야 한다.

void CoUninitialize();

BOOL CAddFrontApp::ExitInstance()

{

CoUninitialize();

return CWinApp:ExitInstance();

}

2.3.   COM 개체 인스턴스 생성

2.3.1.   COM 개체의 CLSID 구하기

COM 개체의 인스턴스를 생성하기 위해서는 먼저 해당 COM 개체의 클래스ID(CLSID)를 알아야 한다.

COM 클라이언트 어플리케이션에서 COM 개체의 CLSID 를 구하기 위해서는 다음과 같은 방법을 사용할 수 있다.

1.       COM 컴포넌트가 소스 코드 형태로 CLSID 정의 파일을 제공한다면 해당파일을 COM 클라이언트 어플리케이션의 프로젝트에 포함시켜 사용할 수 있다. 그러나 일반적으로 COM 컴포넌트는 이진(binary) 파일 형태로만 제공하기 때문에 자신이 COM 컴포넌트를 생성하거나 SDK형태로 제공하는 경우를 제외하고는 이 방법을 사용하기는 어렵다.

2.       시스템 레지스트리에 등록되어 있는 COM개체에 대해 레지스트리 편집기 또는 Visual C++의 OLE/COM Object Viewer를 사용하여 해당 COM개체의 CLSID를 복사한 후 그 값을 GUID 구조체 형식으로 변환하여 정의하는 소스 코드를 작성하여 COM 클라이언트 어플리케이션에서 사용하는 방법이 있다. 그러나 이 방법도 몇 단계의 수작업 과정을 거쳐야 하며, 이 과정에서 문제점이 발생할 가능성이 많다.

3.       마지막으로 가장 좋은 방법은 CLSID에 대응되는 프로그램ID(ProgID)를 사용하는 것이다. COM 개체의 CLSID는 ProgID라고 하는 좀 더 읽기 쉬운 문자열 식별자와 대응되는 값을 갖는다.

그러나 CLSID가 전세계적으로 시간과 장소에 관계없이 고유하다고 보장할 수 잇는 값을 나타내는 식별자인 반면에, ProgID는 그것을 보장하지 않는다.

일반적으로 ProgID는 다음과 같은 형식을 갖는다.

<컴포넌트 또는 라이브러리명>.<개체명>.<버전>

예) Excel.Sheet.8

ProgID에서 버전 번호가 생략될 때 버전 독립적인 ProgID라고 한다. ProgID는 시스템 레지스트리에 HKEY_CLASSES_ROOT 밑에 저장되며, ProgID 키 밑에는 대응되는 CLSID를 저장하는 CLSID서브 키가 있다. 다시 CLSID 서브 키에 저장된 값은 HKEY_CLASSES_ROOT 키 밑에 있는 CLSID 밑에 서브키로 저장되며, 이 서브 키 밑에는 대응되는 ProgID 값이 저장하는 ProgID 서브키가 있다.

시스템 레지스트리에서 CLSID와 ProgID는 반드시 대칭적으로 일치해야 하며, COM 라이브러리는 CLSID와 ProgID를 상호 변환할 수 있는 CLSIDFromProgID와 ProgIDFromCLSID 함수를 제공한다.

HRESULT CLSIDFromProgID(

LPCOLESTR lpszProgID,               // 구하고자 하는 CLSID의 ProgID 포인터

LPCLSID pclsid                             // 리턴된 CLSID값을 저장할 CLSID 포인터

);

WINOLEAPI ProgIDFromCLSID(

REFCLSID clsid,                            // 구하고자 하는 ProgID의 CLSID

LPOLESTR FAR* lplpszProgID       // 리턴된 ProgID 포인터를 저장할 변수의 포인터

);

// CLSIDFromProgID 사용 예

HRESULT hr;

CLSID clsid;

hr = ::CLSIDFromProgID(OLESTR("AddBack.AddBack.1"), &clsid);

if (FAILED(hr))

{

AfxMessageBox("클래스ID를 구할 수 없습니다!");

return;

}

COM 은 각국 언어를 지원하기 위해 내부적으로는 유니코드를 사용한다. 이것은 8비트 코드 체계로 한글과 같이 한 문자를 표현하는데 2바이트 또는 다중바이트를 사용하는 DBCS(Double Byte Character Set) 또는 MBCS(Multi Byte Character Set)와 다르다. CLSIDFromProgID 함수와 같이 COM 라이브러리의 문자열을 매개변수로 받아들이는 모든 API는 유니코드 문자열을 요구하며, 많은 표준 인터페이스에서 리턴 되는 문자열도 유니코드를 사용한다.

LPCOLESTR 데이터 형은 WTYPES.H 헤더파일에 다음과 같이 정의되어 있다.

typedef wchar_t WCHAR;

typedef WCHAR OLECHAR;

typedef OLECHAR *LPOLESTR;

typedef const OLECHAR *LPCOLESTR;

wchar_t는 유니코드 문자를 저장할 수 있는 16비트 wide char 데이터 형이며, OLESTR 매크로는 문자열을 LPCOLESTR 데이터 형으로 변환시켜준다.

일반문자열과 유니코드 문자열을 상호 변환하는 함수

// Win32API 함수

MultiByteToWideChar(

UINT CodePage, DWORD dwFlags,

LPCSTR lpMultiByteStr, int cchMultiByte,

LPWSTR lpWideCharStr, int cchWideChar);

WideCharToMultiByte(UINT CodePage, DWORD dwFlags,

LPCWSTR lpWideCharStr, int cchWideChar,

LPSTR lpMultiByteStr, int cchMultiByte,

LPCSTR lpDefaultChar, LPBOOL lpUsedDefaultChar);

// C런타임 함수

mbstowcs(wchar_t *, const char *, size_t);

wcstombs(char *, const wchar_t *, size_t);

TCHAR szID[129];

wchar_t wszCLSID[129];

StringFromGUID2(CLSID_AddBack, wszCLSID, 128);

wcstombs(szID, wszCLSID, 128);

2.3.2.   CoCreateInstance()

COM 개체의 인스턴스를 생성하는 함수

CoCreateInstance(

REFCLSID rclsid,              // COM 개체의 클래스 식별자(CLSID)

LPUNKNOWN pUnkOuter, // 외부 COM 개체의 IUnknown 포인터

DWORD dwClsContext,                  // 서버 컨텍스트

REFIID riid,                                     // 요청할 인터페이스 식별자(IID)

LPVOID FAR* ppv                          // 리턴된 인터페이스 포인터

);

rclsid: 인스턴스를 생성할 COM개체의 CLSID를 지정. CLSIDFromProgID()를 사용하여 구함.

pUnkOuter: 내부 COM개체에서 사용하게 될 IUnknown인터페이스 포인터를 지정한다. 이 매개변수는 통합(Aggregation)으로 COM개체를 재사용하는 경우에만 사용되며, 사용하지 않을 경우에는 NULL을 지정한다. 사용될 경우에 riid는 반드시 IID_IUnknown이어야 한다.

dwClsContext: COM 개체를 포함하는 COM컴포넌트 서버가

클라이언트와 같은 프로세스 영역에서 실행될 지(CLSCTX_INPROC_SERVER),

다른 프로세스 영역에서 실행될 지(CLSCTX_LOCAL_SERVER),

다른 시스템에서 실행될 지(CLSCTX_REMOTE_SERVER)

여부를 제어하기 위해 사용된다.

l        CLSCTX_INPROC_SERVER: 클라이언트는 같은 프로세스에서 실행하는 COM컴포넌트를 사용한다. COM 컴포넌트는 클라이언트와 같은 프로세스에서 실행될 수 있도록 DLL로 구현되어 있어야 한다.

l        CLSCTX_INPROC_HANDLER: 클라이언트는 인-프로세스 핸들러를 사용한다. 인-프로세스 핸들러는 COM 개체의 기능 일부분 만을 구현한 인-프로세스 서버이다. 컴포넌트의 다른 부분은 로컬 또는 리모트 서버에서 실행되는 아웃-오브-프로세스 서버로 구현된다.

l        CLSCTX_LOCAL_SERVER: 클라이언트는 같은 시스템의 다른 프로세스에서 실행되는 COM컴포넌트를 사용한다. 이러한 COM 컴포넌트를 아웃-오브-프로세스 서버 또는 로컬 서버(Local Server)라고 한다.

l        CLSCTX_REMOTE_SERVER: 클라이언트는 다른 시스템에서 실행되는 COM 컴포넌트를 사용한다. 이러한 COM컴포넌트를 리모트 서버(Remote Server)라고 한다.

objbase.h

CLSCTX_INPROC

CLSCTX_INPROC_SERVER | CLSCTX_INPROC_HANDLER

CLSCTX_SERVER

CLSCTX_INPROC_SERVER |

CLSCTX_LOCAL_SERVER |

CLSCTX_REMOTE_SERVER

CLSCTX_ALL

CLSCTX_INPROC_SERVER | CLSCTX_INPROC_HANDLER |

CLSCTX_LOCAL_SERVER |

CLSCTX_REMOTE_SERVER

리모트 서버 사용

CLSCTX_REMOTE_SERVER가 포함된 값을 사용하기 위해서는 OBJBASE.H 헤더 파일을 #include 하기 전에 _WIN32_WINNT >= 0x0400 또는 _WIN32_DCOM 이 정의되어 있어야 한다.

즉, 윈도우NT4.0과 DCOM 확장 모듈을 설치한 윈도우95에서만 사용할 수 있다.

riid: 생성된 COM 개체에서 사용하고자 하는 인터페이스 식별자(IID)를 지정한다.

일반적으로 표준 인터페이스에 대한 IID는 Win32 SDK에서 제공되며,

자동화(Automation) 컴포넌트인 경우에는 형식 라이브러리(Type Library)에서 읽어온다.

커스텀 인터페이스만 COM컴포넌트가 포함하는 경우에는 COM컴포넌트에서 IID와 인터페이스 정의 코드를 소스 코드 형태로 제공해야 한다.

ppv: COM 개체가 리턴 하는 인터페이스 포인터가 저장된다.

CoCreateInstance 함수가 성공적으로 clsid 매개변수에 지정된 COM 개체를 생성하고,

COM 개체가 riid 매개변수에 지정된 인터페이스를 지원한다면,

ppv 매개변수에는 COM 개체가 리턴 하는 인터페이스가 저장된다.

만약 COM개체가 riid 매개변수에 지정된 인터페이스를 지원하지 않는다면 이 매개변수에는 NULL이 저장된다.

HRESULT hr;

CLSID clsid;

IAddEnd* pIAddEbd = NULL;

hr = ::CLSIDFromProgID(OLESTR("AddBack.AddBack.1"), &clsid);

if (FAILED(hr))

{

AfxMessageBox("클래스 ID를 구할 수 없습니다!");

return;

}

hr = ::CoCreateInstance(

clsid,

NULL,

CLSCTX_ALL,

IID_IAddEnd,

(void**) &pIAddEnd);

if (FAILED(hr))

{

AfxMessageBox("AddBack 컴포넌트를 생성할 수 없습니다!");

return;

}

2.4.   COM 개체 사용

CoCreateInstance 함수를 사용하여 COM 개체의 인스턴스를 성공적으로 생성하고

사용하고자 하는 인터페이스의 포인터를 얻었다면

이 인터페이스 포인터를 통하여 인터페이스가 제공하는 메서드를 호출하여

COM 개체가 지원하는 서비스를 사용할 수 있게 된다.

또한, 해당 COM 개체가 다른 인터페이스를 지원한다면

CoCreateInstance 함수에서 리턴된 인터페이스 포인터를 통하여

QueryInterface 메서드를 호출함으로써 지원하는 인터페이스를 요청한 후

다시 해당 인터페이스 포인터를 통하여 메서드를 호출할 수 있다.

인터페이스의 사용이 끝났을 때 Release 메서드를 호출하는 것을 잊지 않도록 한다.

// ProgID가 “AddBack.AddBack.1” 인 COM 개체가 지원하는 인터페이스

EXTERN_C const IID IID_IAddEnd;

interface DECLSPEC_UUID("C6F96D90-9FCF-11d1-B20A-0060970A3516")

IAddEnd : public IUnknown

{

public:

virtual HRESULT STDMETHODCALLTYPE GetAddEnd(

/* [out] */ short __RPC_FAR *result) = 0;

virtual HRESULT STDMETHODCALLTYPE SetAddEnd(

/* [in] */ short addend) = 0;

virtual HRESULT STDMETHODCALLTYPE GetSum(

/* [out] */ short __RPC_FAR *result) = 0;

virtual HRESULT STDMETHODCALLTYPE Clear(void) = 0;

};

EXTERN_C const IID IID_IAdd;

interface DECLSPEC_UUID("C6F96D90-9FCF-11d1-B20A-0060970A3516")

IAdd : public IUnknown

{

public:

virtual HRESULT STDMETHODCALLLTYPE Add(void) = 0;

virtual HRESULT STDMETHODCALLLTYPE AddTen(void) = 0;

};

// COM 개체 사용의 예

HRESULT hr;

CLSID clsid;

IAddEnd* pIAddEnd = NULL;

IAdd* pIAdd = NULL;

short nAddEnd;

short nSum;

// ProgID에서 CLSID를 구한다.

hr = ::CLSIDFromProgID(OLESTR("AddBack.AddBack.1"), &clsid);

if (FAILED(hr))

{

AfxMessageBox("클래스 ID를 구할 수 없습니다!");

return;

}

// AddBack COM 컴포넌트를 생성하고 IAddEnd 인터페이스를 구한다.

hr = ::CoCreateInstance(

clsid,

NULL,

CLSCTX_ALL,

IID_IAddEnd,

(void**) &pIAddEnd);

if (FAILED(hr))

{

AfxMessageBox("AddBack 컴포넌트를 생성할 수 없습니다!");

return;

}

// IAddEnd 인터페이스 포인터를 통하여 IAdd 인터페이스를 요청한다.

hr = pIAddEnd->QueryInterface(IID_IAdd, (void**) &pIAdd);

if (FAILED(hr))

{

AfxMessageBox("IAdd 인터페이스를 지원하지 않습니다!");

return;

}

// AddBack COM 컴포넌트가 제공하는 서비스를 사용한다.

pIAddEnd->GetAddEnd(&nAddEnd);

pIAddEnd->GetSum(&nSum);

nAddEnd = 20;

pIAddEnd->SetAddEnd(nAddEnd);

pIAdd->Add();

pIAddEnd->GetSum(&nSum);

pIAdd->AddTen();

pIAddEnd->GetSum(&nSum);

// 인터페이스의 사용이 끝나면 Release 메서드를 호출한다.

pIAdd->Release();

pIAddEnd->Release();

3.      COM 개체 구현

3.1.   COM 인터페이스 정의

3.1.1.   IDL(Interface Definition Language)와 MIDL 컴파일러

인터페이스란 순수한 가상함수만을 포함하는 C++ 클래스로 인터페이스를 정의할 수 있다고 배웠다. 그러나 Win32 환경에서 인터페이스를 정의하는 가장 좋은 방법은 IDL(Interface Definition Language)를 사용하는 것이다. IDL은 C++와 유사한 구문으로 인터페이스를 가장 정확하게 정의할 수 있도록 해준다.

MIDL 컴파일러는 IDL파일을 컴파일하여 C와 C++에서 사용할 수 있는 인터페이스를 정의한 코드를 포함하는 헤더파일을 생성한다. 사실 MIDL 컴파일러는 이것 외에도 두 가지 중요한 기능을 제공한다.

1.       로컬서버 또는 리모트 서버 컴포넌트의 커스텀 인터페이스에 대한 프록시와 스텁 코드를 생성하는 기능을 제공

2.       자동화(Automation)에서 사용되는 형식 라이브러리(type library)를 생성하는 기능 제공

// IDL 구문을 사용하여 인터페이스를 정의한 예

// AddBack.idl 파일

[

uuid(C6F96D90-9FCF-11d1-B20A-0060970A3516),

object

]

interface IAddEnd : IUnknown

{

import "unknwn.idl";

HRESULT GetAddEnd([out] short* result);

HRESULT SetAddEnd([in] short addend);

HRESULT GetSum([out] short* result);

HRESULT Clear(void);

};

[

uuid(C6F96D91-9FCF-11d1-B20A-0060970A3516),

object

]

interface IAdd : IUnknown

{

import "unknwn.idl";

HRESULT Add(void);

HRESULT AddTen(void);

};

midl AddBack.idl

MIDL로 컴파일러는 4개의 파일을 생성한다.

1.       AddBack.h: C와 C++ 에서 사용할 수 있는 IDL 파일에 지정된 모든 인터페이스를 정의한 코드가 저장된다.

2.       AddBack_i.c: IDL파일에서 사용되는 모든 GUID, 즉 IID를 정의하는 코드가 포함된다.

3.       AddBack_p.c와 DllData.c: 로컬 서버 또는 리모트 서버에 필요한 커스텀 인터페이스에 대한 마샬링 기능을 제공하는 프록시와 스텁 코드를 포함하게 된다.

// AddBack.h C++로 작성된 인터페이스 정의 부분

EXTERN_C const IID IID_IAddEnd;

interface DECLSPEC_UUID("C6F96D90-9FCF-11d1-B20A-0060970A3516")

IAddEnd : public IUnknown

{

public:

virtual HRESULT STDMETHODCALLTYPE GetAddEnd(

/* [out] */ short __RPC_FAR *result) = 0;

virtual HRESULT STDMETHODCALLTYPE SetAddEnd(

/* [in] */ short addend) = 0;

virtual HRESULT STDMETHODCALLTYPE GetSum(

/* [out] */ short __RPC_FAR *result) = 0;

virtual HRESULT STDMETHODCALLTYPE Clear( void) = 0;

};

EXTERN_C const IID IID_IAdd;

interface DECLSPEC_UUID("C6F96D91-9FCF-11d1-B20A-0060970A3516")

IAdd : public IUnknown

{

public:

virtual HRESULT STDMETHODCALLTYPE Add( void) = 0;

virtual HRESULT STDMETHODCALLTYPE AddTen( void) = 0;

};

// AddBack_i.c 예

typedef struct _IID

{

unsigned long x;

unsigned short s1;

unsigned short s2;

unsigned char  c[8];

} IID;

const IID IID_IAddEnd =

{0xC6F96D90,0x9FCF,0x11d1,{0xB2,0x0A,0x00,0x60,0x97,0x0A,0x35,0x16}};

const IID IID_IAdd =

{0xC6F96D91,0x9FCF,0x11d1,{0xB2,0x0A,0x00,0x60,0x97,0x0A,0x35,0x16}};

3.1.2.   IDL기본 구문

1.       대괄호([]) 안에 인터페이스에 대한 속성 리스트가 놓이게 된다.

2.       인터페이스 속성 리스트 다음에는 인터페이스 본문이 온다.

3.       import 예약어는 C언어의 #include 지시어와 동일한 역할을 한다. 콤마(‘,’)를 사용하여 여러 개의 파일을 포함할 수 있으며 세미콜론(‘;’)으로 끝나야 한다. UNKNWN.IDL 파일에는 IUnknown 인터페이스와 IClassFactory 인터페이스의 IDL문이 저장되어 있다.

IDL의 목적 중의 하나는 메서드의 매개변수가 마샬링(marshaling) 될 수 있는 충분한 정보를 제공하며, 필요한 데이터만 프로세스 사이에 넘겨주기 위해 마샬링에 사용되는 프록시(proxy)와 스텁(stub) 코드를 최적화하는 것이다. 이것을 위해 IDL은 정확하게 데이터 형을 설명하기 위한 많은 속성을 제공하게 되며, 그 중의 하나가 in과 out속성이다.

out속성을 갖는 매개변수는 항상 포인터이어야 한다.

in이나 out속성으로 지정된 매개변수가 사용하는 메모리는 호출 측에서 할당하고 해제한다. 그러나 in과 out 속성을 모두 갖는 매개변수가 사용하는 메모리는 초기에는 호출 측에서 할당하지만, 호출되는 측에서 해제하고 다시 할당할 수 있게 된다.

MIDL컴파일러는 object 예약어가 사용된 인터페이스의 모든 메서드는 HRESULT를 리턴 하도록 제한하고 있다. 만약 메서드가 HRESULT 가 아닌 다른 값을 리턴 해야 한다면 out 매개변수를 사용해야 한다. 비 동기 메서드(asynchronous method)에 대해서는 예외로 void 리턴 데이터 형을 허용한다.

3.2.   COM 개체 구현

3.2.1.   COM 개체 구현 방법 비교

일반적으로 COM 개체를 구현하는 방법에는 두 가지가 있다.

1.       인터페이스 포함 방법: COM개체 클래스 안에 인터페이스 구현 클래스를 포함시키는 방법, 구문은 복잡하지만 디버깅이 쉽다는 장점, MFC라이브러리가 사용하는 방식

2.       인터페이스 상속 방법: COM개체 클래스를 직접 인터페이스에서 파생시키는 방법, 인터페이스 포함방법에 비해 간단하다. ATL이 사용하는 방식

// 인터페이스 포함방법 예

class CAddBack : public IUnknown

{

public:

CAddBack();

~CAddBack();

// IUnknown 메서드

HRESULT __stdcall QueryInterface(REFIID riid, LPVOID* ppv);

ULONG __stdcall AddRef();

ULONG __stdcall Release();

private:

class CImplIAddEnd : public IAddEnd

{

…

}

class CImplIAdd : public IAdd

{

…

}

};

3.2.2.   인터페이스 상속 방법을 사용한 COM 개체 클래스 정의

// 인터페이스 상속방법 예

class CAddBack : public IAddEnd, public IAdd

{

public:

CAddBack();

~CAddBack();

// IUnknown 메서드

HRESULT __stdcall QueryInterface(REFIID riid, LPVOID* ppv);

ULONG __stdcall AddRef();

ULONG __stdcall Release();

…

};

3.2.3.   IUnknown 인터페이스 메서드 구현

// QueryInterface 예

HRESULT __stdcall CAddBack::QueryInterface(REFIID riid, LPVOID* ppv)

{

HRESULT hr = E_NOINTERFACE;

*ppv = NULL;

if (riid == IID_IUnknown || riid == IID_IAddEnd)

*ppv = (IAddEnd*) this;

else if (riid == IID_IAdd)

*ppv = (IAdd*) this;

if (*ppv != NULL)

{

AddRef();

hr = S_OK;

}

return hr;

}

ULONG __stdcall CAddBack::AddRef()

{

return ++m_cRef;

}

ULONG __stdcall CAddBack::Release()

{

if (--m_cRef == 0)

{

delete this;

}

return m_cRef;

}

다중 스레드에서 동시에 같은 변수를 사용하는 것을 막기 위해서 다음과 같은 함수를 사용하여 레퍼런스 카운터를 증가 또는 감소 코드를 작성할 수 있다.

ULONG __stdcall CAddBack::AddRef()

{

return InterlockedIncrement(&m_cRef);

}

ULONG __stdcall CAddBack::Release()

{

if (InterlockedIncrement(&m_cRef) == 0)

{

delete this;

}

return m_cRef;

}

3.3.   클래스 팩토리 구현

3.3.1.   COM 개체 생성 과정

STDAPI CoCreateInstance(

REFCLSID rclsid,

LPUNKNOWN pUnkOuter,

DWORD dwClsContext,

REFIID riid,

LPVOID FAR* ppv)

{

*ppv = NULL;

IClassFactory* pIFactory = NULL;

// 클래스 팩토리를 생성하고 IClassFactory 인터페이스 포인터를 구한다.

HRESULT hr = CoGetClassObject(

rclsid,

dwClsContext,

NULL,

IID_IClassFactory,

(LPVOID*)&pIFactory);

if (SUCCEEDED(hr))

{

// 컴포넌트를 생성한다.

hr = pIFactory->CreateInstance(pUnkOuter, riid, ppv);

// 클래스 팩토리를 해제한다.

pIFactory->Release();

}

return (hr);

}

CoCreateInstance (rclsid, pUnkOuter, dwClsContext, riid, ppv)

CoGetClassObject (rclsid, dwClsContext, NULL, riid, ppv)

DllGetClassObject (In-Process Server일 경우)

IClassFactory::CreateInstance (pUnkOuter, riid, ppv)

IClassFactory::Release ()

1.       CoGetClassObject(): 레지스트리의 HKEY_CLASSES_ROOT\CLSID\에서 rclsid와 같은 서브 키를 찾아, 해당 서브키 밑에 있는 InprocServer32(인-프로세스 서버인 경우), LocalServer32(로컬 서버인 경우) 서브키 값에 지정된 COM 개체를 포함하고 있는 COM 컴포넌트 서버를 메모리에 로드 한다.

HKEY_CLASSES_ROOT

AddBack.AddBack.1 : AddBack Component

CLSID : {6521E660-9FE0-11D1-B20A-0060970A3516}

HKEY_CLASSES_ROOT

CLSID

{0x                           } : AddBack Component

InprocServer32 : D:\AddBack\InProc\Debug\AddBack.dll

ProgID : AddBack.AddBack.1

2.       DllGetClassObject(): IClassFactory 인터페이스를 구현한 클래스 팩토리 COM 개체의 인스턴스를 생성하고, CoGetClassObject()에서 요청한 IClassFactory 인터페이스 포인터를 리턴 하게 된다.

3.       IClassFactory::CreateInstance(): 자신과 관련을 맺고 있는 COM 개체의 인스턴스를 생성하고 QueryInterface()를 호출하여 CreateInstance의 두 번째 매개변수 riid에 지정된 인터페이스 포인터를 구한다음. 해당 인터페이스 포인터를 세 번째 매개변수 ppv에 리턴 하게 된다.

CoGetClassObject 함수는 일반적으로 CoCreateInstance 함수에 의해 호출되지만, CoCreateInstance 함수는 IClassFactory 인터페이스를 지원하는 클래스 팩토리 COM 개체 만을 생성하기 때문에 여러분이 직접 CoGetClassObject 함수를 사용해야 할 경우가 있다. 예를 들어 IClassFactory 인터페이스에 라이센스 기능이 추가된 IClassFactory2 인터페이스를 지원하는 클래스 팩토리 COM 개체를 생성해야 하는 경우에는 여러분은 직접 CoGetClassObject 함수를 사용해야 한다.

3.3.2.   클래스 팩토리와 IClassFactory 인터페이스

COM 개체의 인스턴스를 생성하는 역할은 해당 COM 개체와 관련된 클래스 팩토리(Class Factory)라고 하는 또 다른 COM 개체가 수행하며, 클래스 팩토리 COM 개체는 반드시 IClassFactory 인터페이스를 지원해야 한다.

interface IClassFactory : public IUnknown

{

virtual HRESULT __stdcall CreateInstance(

LPUNKNOWN pUnkOuter,

REFIID riid,

LPVOID* ppv) = 0;

virtual HRESULT __stdcall LockServer(BOOL bLock) = 0;

}

CreateInstance가 CLSID를 받아들이지 매개변수가 없다. 이것은 CoGetClassObject 함수에 매개변수로 넘겨진 CLSID에 대응되는 COM개체의 인스턴스만을 생성한다는 것을 의미한다. 즉, 하나의 클래스 팩토리 COM개체는 하나의 CLSID에 대응되는 COM 개체의 인스턴스만 생성할 수 있다.

3.3.3.   클래스 팩토리 COM 클래스 구현

class CFAddBack : public IClassFactory

{

public:

CFAddBack();

~CFAddBack();

// IUnknown 메서드

HRESULT __stdcall QueryInterface(REFIID riid, LPVOID *ppv);

ULONG __stdcall AddRef(void);

ULONG __stdcall Release(void);

// IClassFactory 메서드

HRESULT __stdcall CreateInstacne(LPUNKNOWN pUnkOuter, REFIID riid, LPVOID* ppv);

HRESULT __stdcall LockServer(BOOL bLock);

private:

DWORD m_cRef: // Reference Counter

};

HRESULT __stdcall CFAddBack::CreateInstacne(

LPUNKNOWN pUnkOuter,

REFIID riid,

LPVOID* ppv)

{

HRESULT hr = E_FAIL;

CFAddBack* pAddBack = NULL;

*ppv = NULL;

// CAddBack COM 개체는 통합(aggregation)을 지원하지 않는다.

if (pUnkOuter != NULL)

{

hr = CLASS_E_NOAGGREGATION;

}

else

{

// 새로운 CAddBack COM 개체를 생성한다.

pAddBack = new CFAddBack;

if (pAddBack != NULL)

{

// 클라이언트에서 요청한 인터페이스 포인터를

// CAddBack COM 개체에게 요구한다.

hr = pAddBack->QueryInterface(riid, ppv);

if ((FAILED(hr)))

{

delete pAddBack;

}

}

else

hr = E_OUTOFMEMORY

}

return hr;

}

3.4.   인 – 프로세스 서버 구현

3.4.1.   Win32에서의 DLL

Win32 운영체제의 프로세스(process)는 4GB의 자기자신의 고유의 주소 영역 안에서 실행된다. 이것은 각 프로세스는 자신이 소유한 메모리와 Win32 운영체제가 공유하도록 지정한 메모리 영역에만 접근할 수 있다는 것을 의미한다.

WinMain 함수가 윈도우 어플리케이션의 시작 함수인 것과 마찬가지로, DLL의 시작함수는 DllMain이다. 그러나 DllMain 함수는 종료 함수이기도 하다. 즉, DllMain 함수는 DLL 모듈이 시작할 때도 호출되지만, 반면에 종료할 때도 Win32 운영체제에 의하여 호출된다.

DllMain 함수의 구조

static HMODULE g_hModule = 0;

BOOL WINAPI DllMain(HINSTANCE hInstDll,

DWORD dwReason,

LPVOID lpvReserved)

{

switch(dwReason)

{

case DLL_PROCESS_ATTACH:

// DLL이 프로세스의 주소영역에 맵핑됨.

// DLL 초기화 코드

g_hModule = hModule;

break;

case DLL_THREAD_ATTACH:

// 쓰레드가 생성됨

break;

case DLL_THREAD_DETACH:

// 스레드가 종료됨

break;

case DLL_PROCESS_DETACH:

// DLL이 프로세스의 주소 영역에서 맵핑이 해제됨

// DLL 종료 처리 코드

break;

}

return TRUE;

}

우리가 DLL을 생성할 때 EXE파일이나 다른 DLL에서 호출할 수 있는 함수를 생성하게 된다. 이 DLL 함수가 EXE파일이나 다른 DLL에서 호출할 수 있게 하기 위해서는 해당 함수는 반드시 익스포트(exported) 되어야 한다. C++ 언어를 사용하는 경우에 DLL 함수를 익스포트 하기 위해서는 해당 함수는 extern “C”로 선언되어야 한다. 다음은 익스포트 함수를 선언한 예이다.

extern "C"

STDAPI DllGetClassObject(REFCLSID rclsid, REFIID riid, LPVOID* ppv);

이와 함께, DEF 파일을 사용하여 익스포트 함수를 선언한다.

LIBRARY "DllServe.DLL"

EXPORTS

DllCanUnloadNow                          @1 PRIVATE

DllGetClassObject                         @2 PRIVATE

DllRegisterServer                          @3 PRIVATE

DllUnregisterServer                       @4 PRIVATE

3.4.2.   인-프로세스 서버 구현 개요

1.       클라이언트는 생성하고자 하는 COM 개체의 CLSID와 IID를 매개변수로 COM 라이브러리의 CoCreateInstance 함수를 호출한다.

2.       COM 라이브러리의 CoCreateInstance 함수는 내부적으로 CoGetClassObject 함수를 호출한다.

3.       CoGetClassObject 함수는 시스템 레지스트리에서 클라이언트가 요청한 CLSID에 대응되는 COM 컴포넌트가 저장되어 있는 인-프로세스 서버 정보를 찾는다.

4.       CoGetClassObject 함수는 CoLoadLibrary 함수를 호출하여 해당 인-프로세스 서버 DLL을 메모리에 로드하고, 해당 DLL의 DllGetClassObject 익스포트 함수를 호출한다.

5.       DllGetClassObject 함수에서는 클라이언트가 요청한 COM 개체의 클래스 팩토리 COM개체를 생성한 후, 해당 클래스 팩토리 COM 개체의 IClassFactory 인터페이스 포인터를 리턴 한다.

6.       CoGetClassObject 함수에서는 IClassFactory 인터페이스 포인터를 통하여 구하고자 하는 인터페이스를 매개변수로 CreateInstance 멤버를 호출한다.

7.       IClassFactory::CreateInstance 함수는 새로운 COM 개체를 생성하고 클라이언트에서 요청한 인터페이스 포인터를 리턴 한다.

8.       CoGetClassObject 함수는 IClassFactory::CreateInstance 함수에서 리턴한 인터페이스 포인터를 다시 CoCreateInstance 함수에 리턴 한다.

9.       CoCreateInstance 함수는 CoGetClassObject 함수가 리턴한 인터페이스 포인터를 다시 클라이언트에 리턴 한다.

10.   클라이언트에서는 리턴된 인터페이스 포인터를 통하여 해당 인터페이스에 구현된 메서드를 사용한다.

CoCreateInstance

CoGetClassObject

CoLoadLibrary

DllGetClassObject

IClassFactory::CreateInstance

COM 개체를 포함하는 인-프로세스 서버를 생성하고자 할 때 해당 DLL에는 DllMain 함수 외에도, 클라이언트에서 COM 개체를 사용할 때 호출되는 다음과 같은 4개의 익스포트 함수가 포함되어 있어야 한다.

1.       DllGetClassObject: COM 라이브러리가 COM 개체의 클래스 팩토리 COM 개체를 생성할 때 호출.

2.       DllRegisterServer / DllUnregisterServer: COM 개체를 시스템레지스트리에 등록할 때 REGSVR32.EXE 파일에 의하여 호출된다.

3.       DllCanUnloadNow: COM 라이브러리가 DLL을 언로드할 수 있는지 여부를 요청할 때 COM 라이브러리에 의해 호출된다.

3.4.3.   DllGetClassObject 함수 구현

COM 개체를 생성하기 위해서는 먼저, 해당 COM 개체와 대응되는 클래스 팩토리 COM 개체를 생성해야 한다. 인-프로세스 서버인 경우에는 COM 라이브러리의 CoGetClassObject 함수에서 DLL의 DllGetClassObject 함수를 호출하고, 이 함수에서 실제로 클래스 팩토리 COM 개체를 생성하게 된다. 따라서, 우리는 인-프로세스 서버에 DllGetClassObject 함수를 구현해야만 한다.

extern "C"

STDAPI DllGetClassObject(REFCLSID rclsid, REFIID riid, LPVOID* ppv)

{

HRESULT hr = CLASS_E_CLASSNOTAVAILABLE;

IUnknown* pUnk = NULL;

if (rclsid == CLSID_AddBack)

{

hr = E_OUTOFMEMORY;

pUnk = new CFAddBack;

}

if (pUnk != NULL)

{

hr = pUnk->QueryInterface(riid, ppv);

if (FAILED(hr))

{

delete pUnk;

}

}

return hr;

}

3.4.4.   DllCanUnloadNow 함수 구현

COM 라이브러리는 사용되지 않는 DLL을 메모리에서 언로드하여 해제시키는 역할을 하는 CoFreeUnusedLibraries 라고 하는 함수를 제공한다.

CoFreeUnusedLibraries 함수는 DLL에 구현된 DllCanUnloadNow 함수를 호출하여 DLL을 해제시켜도 좋은지 여부를 묻는다.

LONG g_cObjects = 0;     // COM 개체 카운터

extern "C"

STDAPI DllCanUnloadNow(void)

{

if (g_cObjects == 0)

{

return S_OK;

}

return S_FALSE;

}

따라서 IClassFactory::CreateInstance 에서는 g_cObjects 전역 변수의 값을 증가시키고, 각 COM 개체 클래스의 Release 함수에서는 감소시키는 코드를 작성함으로써, 전역변수 g_cObjects 즉 컴포넌트 카운터를 관리할 수 있다.

그러나 COM 개체 카운터를 관리하는 것만으로는 충분하지 않을 수도 있다. 이때 IClassFactory::LockServer 멤버는 이것을 위한 장치를 제공한다.

IClassFactory::LockServer구현은 COM 개체 카운터 즉, g_cObjects 값을 증가시키고 감소시키면 된다. 그러나, 이것과는 별도로 로크 카운터(lock counter)를 사용하는 것이 일반적이다.

LONG g_cLocks = 0;        // 로크 카운터

HRESULT __stdcall CFAddBack::LockServer(BOOL bLock)

{

if (bLock)

++g_cLocks;

else

--g_cLocks;

return S_OK;

}

STDAPI DllCanUnloadNow()

{

if (g_cObjects == 0 && g_cLocks == 0)

return S_OK;

return S_FALSE;

}

3.4.5.   DllRegisterServer/DllUnregisterServer 함수 구현

일반적으로 인-프로세스 서버를 레지스트리에 등록할 때 REGSVR32.EXE를 사용한다. 이 프로그램은 명령 행에서 인-프로세스 서버 파일명을 매개변수로 받아, 해당 DLL의 DllRegisterServer 익스포트 함수를 호출한다.

regsvr32 MyServer.dll

또한, REGSVR32.exe 프로그램에 /u 옵션을 함께 사용될 경우에는 해당 DLL의 DllUnregisterServer 익스포트 함수를 호출한다.

regsvr32 /u MyServer.dll

따라서 우리는 레지스트리에 COM 컴포넌트를 등록 해제하는 DllRegisterServer 와 DllUnregisterServer 함수를 구현해야 할 필요가 있다. 이때 레지스트리를 조사하는 Win32 SDK 함수를 사용한다. Win32 SDK 는 레지스트리의 정보를 조작할 수 있는 다음과 같은 6개의 함수를 제공한다.

1.       RegOpenKeyEx

2.       RegCreateKeyEx

3.       RegSetValueEx

4.       RegEnumKeyEx

5.       RegDeleteKey

6.       RegCloseKey

참고로 이들 함수를 사용하기 위해서는 WINREG.H 또는 WINDOWS.H 헤더 파일을 추가하고 ADVAPI32.LIB 파일을 함께 링크해야 한다.

BOOL SetRegKeyValue(LPTSTR pszKey,

LPTSTR pszSubkey,

LPTSTR pszValue);

extern "C"

STDAPI DllRegisterServer (void)

{

HRESULT hr = NOERROR;

TCHAR szID[129];

TCHAR szCLSID[129];

TCHAR szModulePath[MAX_PATH];

wchar_t wszCLSID[129];

GetModuleFileName(g_hModule, szModulePath,

sizeof(szModulePath)/sizeof(TCHAR));

StringFromGUID2(CLSID_AddBack, wszCLSID, 128);

wcstombs(szID, wszCLSID, 128) ;

lstrcpy(szCLSID, TEXT("CLSID\\"));

lstrcat(szCLSID, szID);

SetRegKeyValue(

TEXT("AddBack.AddBack.1"),

NULL,

TEXT("AddBack Component"));

SetRegKeyValue(

TEXT("AddBack.AddBack.1"),

TEXT("CLSID"),

szID);

SetRegKeyValue(

szCLSID,

NULL,

TEXT("AddBack Component"));

SetRegKeyValue(

szCLSID,

TEXT("ProgID"),

TEXT("AddBack.AddBack.1"));

SetRegKeyValue(

szCLSID,

TEXT("InprocServer32"),

szModulePath);

return (hr);

}

extern "C"

STDAPI DllUnregisterServer (void)

{

HRESULT hr = NOERROR;

TCHAR szID[129];

TCHAR szCLSID[129];

TCHAR szTemp[129];

wchar_t wszCLSID[129];

StringFromGUID2(CLSID_AddBack, wszCLSID, 128);

wcstombs(szID, wszCLSID, 128) ;

lstrcpy(szCLSID, TEXT("CLSID\\"));

lstrcat(szCLSID, szID);

RegDeleteKey(HKEY_CLASSES_ROOT,

TEXT("AddBack.AddBack.1\\CLSID"));

RegDeleteKey(HKEY_CLASSES_ROOT,

TEXT("AddBack.AddBack.1"));

wsprintf(szTemp, TEXT("%s\\%s"), szCLSID,

TEXT("InprocServer32"));

RegDeleteKey(HKEY_CLASSES_ROOT, szTemp);

wsprintf(szTemp, TEXT("%s\\%s"), szCLSID,

TEXT("ProgID"));

RegDeleteKey(HKEY_CLASSES_ROOT, szTemp);

RegDeleteKey(HKEY_CLASSES_ROOT, szCLSID);

return (hr);

}

BOOL SetRegKeyValue(LPTSTR pszKey,

LPTSTR pszSubkey,

LPTSTR pszValue)

{

BOOL bOk = FALSE;

LONG ec;

HKEY hKey;

TCHAR szKey[256];

lstrcpy(szKey, pszKey);

if(NULL != pszSubkey) {

lstrcat(szKey, TEXT("\\"));

lstrcat(szKey, pszSubkey);

}

ec = RegCreateKeyEx(

HKEY_CLASSES_ROOT,

szKey,

0,

NULL,

REG_OPTION_NON_VOLATILE,

KEY_ALL_ACCESS,

NULL,

&hKey,

NULL);

if(ERROR_SUCCESS == ec) {

if(NULL != pszValue) {

ec = RegSetValueEx(

hKey,

NULL,

0,

REG_SZ,

(BYTE *)pszValue,

(lstrlen(pszValue)+1)*sizeof(TCHAR));

}

if(ERROR_SUCCESS == ec)

bOk = TRUE;

RegCloseKey(hKey);

}

return bOk;

}

3.5.   로컬 서버 구현

3.5.1.   로컬 서버 구현시 고려 사항

인-프로세스 서버는 DLL 로 구현되기 때문에 자신을 로드한 클라이언트 어플리케이션의 프로세스 영역 안에서 실행된다. 따라서, COM 개체가 리턴한 인터페이스 포인터는 클라이언트와 같은 프로세스 영역 안에 있게 된다. 그러나 별도의 .exe 실행파일로 구현되어, 같은 시스템의 다른 프로세스 영역에서 실행되는 로컬 서버나, 네트워크 상의 전혀 다른 시스템의 프로세스 영역에서 실행되는 리모트 서버의 경우는 자기 자신의 고유한 주소 영역을 가지며, 따라서 설사 같은 주소를 가리킨다고 해도 서로 다른 프로세스 영역에서는 각기 다른 물리적인 메모리를 가리키게 되기 때문이다.

따라서 로컬 서버나 리모트 서버는 인-프로세스 서버와는 전혀 다른 방법으로 구현될 필요가 있으며, 서버와 클라이언트를 연결시켜 줄 수 있는 매개체를 필요로 하게 된다. 그러나, 여기에서 중요한 한 가지 사실은 서비스를 제공하는 COM 컴포넌트가 인-프로세스 서버로 구현되든 로컬 서버나 리모트 서버로 구현되든 클라이언트에서는 같은 방법으로 서비스를 제공받을 수 있어야 한다는 것이다.

3.5.2.   표준 마샬링(standard marshaling)

COM 은 두 프로세스 사이의 커뮤니케이션(Inter-Process Communication, IPC) 수단으로 LPC(Local Procedure Call)와 RPC(Remote Procedure Call)란 방법을 사용한다. LPC는 같은 시스템 상에서 실행되는 서로 다른 프로세스 사이의 커뮤니케이션 방법이며, RPC 는 광범위한 네트워크 전송 메커니즘을 사용하여 서로 다른 시스템 상에서 실행되는 프로세스가 서로 커뮤니케이션 하는 방법을 제공한다.

서로 다른 프로세스 영역 사이에 인터페이스 포인터가 넘겨질 때 Ole32.dll은 인터페이스를 마샬링 하는데 필요한 프록시와 스텁코드를 로드 한다. 먼저 ole32.dll은 로컬 또는 리모트 COM 개체가 생성되는 프로세스 영역에 스텁을 생성한다. 다음에는 스텁과 연결되는 IPC채널을 생성하고, 마지막으로 클라이언트 프로세스 영역에 프록시를 생성한 후 IPC 채널에 연결시킨다.

IPC 채널은 서로 다른 프로세스 영역에 있는 클라이언트와 COM 개체 사이에 메시지를 전달하는 역할을 수행하며, 프록시와 스텁은 데이터를 네트워크 중립적인 형식(Network Data Representation, NDR)으로 포장하는 작업을 수행하는 일종의 COM 개체이다. 이 NDR 패킷은 IPC채널을 통하여 프록시와 스텁 사이에 전송된다. 프록시는 클라이언트 프로세스 영역에 로드 되며, 스텁은 로컬 또는 리모트 서버의 프로세스 영역에 로드 된다.

프록시는 클라이언트가 커뮤니케이션 하고자 하는 로컬 또는 리모트 서버에 있는 COM 개체와 같은 인터페이스를 노출시킨다. 따라서, 클라이언트에서 로컬 또는 리모트 서버의 COM 개체에 인터페이스 포인터를 요청할 때 클라이언트에게 리턴 되는 인터페이스 포인터는 실제로는 프록시가 노출하는 인터페이스 포인터가 된다. 또한, 이 인터페이스 포인터를 사용하여 메서드를 호출할 때도 실제로는 프록시 안에 있는 코드가 실행된다. 이와 함께, 프록시는 클라이언트로부터 넘겨 받은 매개변수를 전송할 수 있도록 포장하는 작업을 수행한다.

매개변수와 리턴 값을 포장할 때, 로컬 서버의 경우에도 두 프로세스가 모두 이해할 수 있는 표준 형식이 필요하지만, 리모트 서버의 경우에는 좀더 복잡하다. 그것은 일반적으로 같은 정보를 서로 다른 형식으로 나타내는 경우가 많기 때문이다. 특별히 문자나 정수 그리고 부동소수점을 표현하는 방식이 서로 다르다. 두 경우 모두 매개변수와 결과를 전송하는 표준적인 형식이 필요하며, 적절한 변환작업을 수행하는 코드가 클라이언트와 서버에 모두 있어야 한다.

이때 호출 측의 데이터를 전송할 수 있는 표준 형식으로 변환하는 것을 마샬링(marshaling)이라고 하며, 그 반대의 작업 즉, 표준 형식의 데이터를 받는 측의 프로세스에 적절한 형식으로 변환하는 것을 언마샬링(unmarshaling) 이라고 한다.

3.5.3.   커스텀 COM 인터페이스의 프록시/스텁 DLL 생성

표준 COM 인터페이스는 COM 에 의하여 정의된 인터페이스이다. 여기에는 자동화(Automation)에서 사용되는 디스패치 인터페이스(dispinterface)와 이중 인터페이스(dual interface)도 포함된다. 표준 COM 인터페이스에 대한 마샬링 코드는 ole32.dll과 oleauto32.dll을 통하여 운영체제 레벨에서 자동적으로 제공되기 때문에 별도의 마샬링 코드를 제공할 필요가 없다.

그러나 커스텀 COM 인터페이스에 대해서는 마샬링 코드를 포함하는 프록시/스텝 DLL을 제공해야 한다. 클라이언트가 커스텀 COM 인터페이스가 서버에서 리턴 될 때 ole32.dll은 레지스트리에서 해당 인터페이스에 대한 프록시/스텁 DLL을 찾아 클라이언트와 서버의 프로세스 영역에 각각 로드 하게 된다.

커스텀 COM 인터페이스에 대하여 마샬링 코드를 포함하는 프록시/스텁 DLL을 생성하는 일은 매우 복잡하고 어려운 일이다. 그러나 IDL 을 사용하여 인터페이스를 정의한 다음, MIDL 컴파일러를 사용하여 프록시와 스텁 코드를 포함하는 DLL을 생성하는 방법으로 편리하게 구현할 수 있다.

MIDL 컴파일러로 IDL 파일을 컴파일하려면 도스 창에서 다음 명령을 입력한다.

midl FileName.idl

도스 창에서 다음과 같은 옵션으로 MIDL 컴파일러를 실행시켜도 같은 결과를 얻게 된다. 생성할 파일명을 변경하고자 한다면 옵션 다음에 원하는 파일명을 입력하면 된다.

midl /h FileName.h /iid FileName_i.c /proxy FileName_p.c FileName.idl

Visual C++ 개발환경에게 IDL 파일을 컴파일할 때 MIDL.EXE 를 사용한다는 사실을 알려준다.

[Project/Settings] 왼쪽 트리뷰에서 IDL 파일 노드를 선택하고, 오른쪽 속성 페이지에서는 [Custom Build] 탭을 선택한 후 [Build Command(s)] 리스트에 다음 명령을 추가한다.

midl.exe /c_ext /ms_ext /app_config $(InputPath)

또한, [Output File(s)] 리스트에는 다음 명령을 추가한다.

FileName.h FileName_p.c FileName_i.c DllData.c

MIDL 컴파일러는 다음과 같은 4개의 파일을 생성하게 된다.

FileName.h

IDL 파일에 지정된 모든 인터페이스를 정의한 코드를 포함하는 헤더 파일

FileName_i.c

IDL 파일에서 사용되는 모든 GUID를 정의하는 C 파일

FileName_p.c

IDL 파일에 지정된 인터페이스에 대한 프록시/스텁 코드를 구현한 C파일

DllData.c

프록시/스텁 코드를 포함하는 DLL을 구현하는 C파일

MIDL 컴파일러가 생성한 파일은 C와 C++ 프로그램에서 모두 사용될 수 있다. 그러나 한가지 단점은 이들 생성된 파일의 내용을 전혀 이해할 수 없다는 것이다. 하지만 굳이 이해할 필요는 없기 때문에 이것을 단점이라고 할 수도 없을 것이다.

프록시/스텁 DLL을 생성하기 위해서는 MIDL 컴파일러가 생성한 파일을 컴파일하고 링크해야 한다. 여러분은 Visual C++ 개발 환경에서 Win32 Dynamic Link Library 유형의 프로젝트를 생성한 후 이들 파일을 프로젝트에 추가하면 된다.

이때 이 프로젝트에는 RPC 런타임 임포트 라이브러리(RPCRT4.LIB)가 링크되어야 하며, 생성된 프록시/스텁 코드가 자기 등록(self registration)을 할 수 있게 하기 위해서는 DLLDATA.C에 대하여 REGISTER_PROXY_DLL 매크로를 정의해야 한다. DLLDATA.C파일을 수정하여 직접 이 매크로를 추가하는 것은 바람직하지 않다. 그것은 MIDL 컴파일러가 IDL 파일을 다시 컴파일할 때 완전히 파일 내용을 갱신하기 때문이다. 따라서 [Project Setting] 대화상자에서 DLLDATA.C에 대하여 REGISTER_PROXY_DLL 매크로를 정의하는 것이 좋다.

이와 함께, 인-프로세스 서버가 익스포트 해야 하는 4개의 함수 (DllGetClassObject, DllCanUnloadNow, DllRegisterServer, DllUnregisterServer)와 함께, MIDL이 생성한 GetProxyDllInfo 함수를 익스포트 하기 위해 다음과 같은 .DEF 파일을 작성한 후 프로젝트에 추가해야 한다.

LIBRARY                          FileName.dll

DESCRIPTION      'Proxy/Stub DLL'

EXPORTS

DllGetClassObject           @1         PRIVATE

DllCanUnloadNow             @2         PRIVATE

GetProxyDllInfo                @3         PRIVATE

DllRegisterServer             @4         PRIVATE

DllUnregisterServer          @5         PRIVATE

이제 여러분은 이 프로젝트를 컴파일/링크 한 후, REGSVR32.EXE를 사용하여 레지스트리에 등록하면 된다. 성공적으로 프록시/스텁 DLL이 레지스트리에 등록되었다면, 레지스트리의 HKEY_CLASSES_ROOT\Interface 밑에 IDL 파일에 지정된 인터페이스의 IID 서브키가 추가된다. 이 서브 키의 ProxyStubClsid32 키에는 이 인터페이스에 사용될 프록시/스텁 DLL의 CLSID가 저장된다. 다시 HKEY_CLASSES_ROOT\CLSID 밑에 프록시/스텁 DLL의 CLSID 서브 키의 InprocServer32 키에는 해당 프록시/스텁 DLL 파일의 절대경로명이 저장된다.

HKEY_CLASSES_ROOT\Interface

ProxyStubClsid32

HKEY_CLASSES_ROOT\CLSID

InprocServer32

3.5.4.   효율적인 마샬링을 위한 IDL 속성

포인터가 매개변수로 프로세스 영역 사이에 넘겨질 때 잠재적인 비효율성의 원인이 된다. 포인터에 대하여 마샬링 코드가 수행될 때 해당 포인터가 가리키는 메모리의 모든 데이터를 복사하여 서버로 넘겨지게 된다. 또한, 포인터가 out속성을 갖는다면 메모리의 데이터는 다시 서버에서 복사되어 클라이언트로 넘겨져야 한다. 포인터가 복잡한 데이터 구조를 가리킨다면 마샬링 코드는 더욱 복잡해지게 된다. 다음 IDL 속성은 포인터에 대하여 효율적으로 마샬링을 하기 위한 정보를 제공하게 된다.

IDL 속성

설명

ref

포인터가 단지 레퍼런스이며 메서드 안에서 포인터 값이 변경되지 않는다는 것을 나타낸다. 포인터가 가리키는 주소의 데이터는 변경될 수 있다. 또한, 포인터는 다른 포인터와 공유하지 않는 고유한 메모리를 가리킨다는 것을 나타낸다. 이 속성의 포인터에 대한 마샬링 코드는 가장 작고 빠르다.

unique

포인터는 다른 포인터와 공유하지 않는 고유한 메모리를 가리킨다는 것을 나타낸다. 그러나, 메서드 안에서 포인터 값은 변경될 수 있기 때문에 마샬링 코드는 서버에서 포인터가 가리키는 데이터를 클라이언트로 복사해와야 한다.

ptr

메서드 안에서 포인터 값이 변경될 수 있으며 공유 메모리 영역을 가리킬 수도 있다는 것을 나타낸다. 이 속성의 포인터에 대한 마샬링 코드는 가장 복잡하다.

HRESULT Method([in, ref] long* arg1, [out, unique] long* arg2);

인터페이스 속성 리스트에 pointer_default 예약어를 사용하여 포인터 매개변수에 다른 속성이 지정되지 않은 경우에 어떻게 처리할 것인가에 대한 디폴트 정보를 제공할 수 있다.

[

uuid(0xx…),

object,

pointer_default(unique)

]

만약 널 문자(‘\0’)로 끝나는 문자열 포인터를 넘겨주기 원한다면 string 속성을 사용하는 것이 좋다. 매개변수에 string 속성이 지정되면 MIDL 컴파일러는 이 매개변수가 문자열이라는 것을 알 수 있고, 따라서 널 문자를 찾아 문자열의 길이를 결정하게 된다. COM 은 문자열에 wchar_t* 또는 LPOLESTR 데이터 형이나 OLESTR 매크로를 사용한다.

HRESULT Method([in, string] wchar_t* arg1, [out, string] wchar_t* arg2);

IDL은 배열을 명확하게 설명할 수 있게 하는 속성을 제공한다. 이것은 메서드가 호출될 때 클라이언트와 서버 사이에 배열을 어떻게 넘겨줄 것인가를 정확하게 최적화 할 수 있게 해준다.

size_is

복사되어야 하는 배열 요소의 전체 개수를 지정한다.

max_is

0번째 배열 요소에서부터 복사되어야 하는 최대 배열 요소를 지정한다.

first_is

복사를 시작해야 하는 첫 번째 배열 요소를 지정한다.

last_is

복사해야 하는 마지막 배열 요소를 지정한다.

일반적으로 first_is 속성과 같이 사용된다.

이들 배열 IDL 속성과 함께 사용되는 값은 괄호 안에 지정되며, 상수나 다른 매개변수의 변수명을 사용할 수 있다. 다차원 배열의 경우에는 괄호 안에 콤마(,)로 구분한다.

// 배열에서 두 배열 요소의 값을 바꾸는 Swap 메서드를 정의하는 예

HRESULT Swap([in] short e1, [in] short e2,

[in, out, first_is(e1), last_is(e2)] long numarray[]);

// 배열 IDL 속성은 포인터와 함께 사용할 수도 있다.

HRESULT Swap([in] short e1, [in] short e2,

[in, out, first_is(e1), last_is(e2)] long *numarray);

// 만약 메서드의 매개변수가 인터페이스 포인터를 리턴 해야 하는 경우에는 iid_is 속성과 함께 IUnknown*를 사용해야 한다.

HRESULT GetInterface([in] const IID& iid, [out, iid_is(iid)] IUnknown** ppv);

MIDL 은 포인터가 가리키는 것이 무엇인지를 정확하게 알아야 해당 포인터가 가리키는 데이터를 마샬링하는 방법을 할 수 있게 된다. 따라서 매개변수에 void*를 절대로 사용할 수 없으며, 일반적인 인터페이스 포인터를 넘겨주어야 할 경우에는 IUnknown*를 사용해야 한다. 이 경우에 iid_is 속성을 사용하여 MIDL 에게 사용하게 될 인터페이스의 IID가 무엇인지를 알려주어야 한다.

COM 은 클라이언트와 서버 모두에서 메모리를 할당하고 해제하는데 사용할 수 있는 하나의 디폴트 메모리 관리자를 제공함으로써 메모리 누수 현상을 방지할 수 있게 한다. COM 의 디폴트 메모리 관리자를 사용하려면 CoTaskMemAlloc, CoTaskMemFree COM 라이브러리 함수를 사용해야 한다.

// IDL에서 인터페이스 메서드가 다음과 같이 정의되었다고 하자

HRESULT Method([in] int size, [out, unique] wchar_t* outParam);

// 서버에서 이 메서드를 다음과 같이 구현할 수 있다.

HRESULT CMyServer::Method(int size, wchar_t* outParam)

{

//…

outParam = (wchar_t*)::CoTaskMemAlloc(size); // size 바이트 할당

// 할당된 메모리 사용

}

// 이때 클라이언트에서는 다음과 같이 이 메서드를 사용한 후 에는 CoTaskMemFree 함수를 호출하여 할당된 메모리를 해제해야 한다.

LPOLESTR lpOutStr;

hr = pServer->Method(100, lpOutStr);

if (SUCCEEDED(hr))

{

// lpOutStr 사용

}

CoTaskMemFree(lpOutStr);

3.5.5.   로컬 서버 구현 개요

로컬 서버는 인-프로세스 서버와는 달리 자신의 프로세스 영역 안에서 실행되므로 WinMain 함수를 갖는다.

인-프로세스 서버의 경우 다음과 같은 4개의 익스포트 함수를 통하여 구현 할 수 있었다.

1. DllGetClassObject

2. DllCanUnloadNow

3. DllRegisterServer

4. DllUnregisterServer

로컬 서버에서는 이들 익스포트 함수가 갖고 있는 기능을 대체할 만한 기능을 제공해야 한다.

다음은 그 예이다.

int APIENTRY WinMain(HINSTANCE hInstance,

HINSTANCE hPrevInstance,

LPSTR     lpCmdLine,

int       nCmdShow)

{

// TODO: Place code here.

HRESULT hr;

MSG;

hr = CoInitialize(NULL);

if(FAILED(hr)) {

MessageBox(NULL,

"COM 라이브러리를 초기화할 수 없습니다!",

NULL, MB_OK);

return 0;

}

if (lstrcmpiA(lpCmdLine, "-RegServer") == 0 ||

lstrcmpiA(lpCmdLine, "/RegServer") == 0) {

g_hInstance = hInstance;

RegisterServer();

return 0;

}

else

if (lstrcmpiA(lpCmdLine, "-UnregServer") == 0 ||

lstrcmpiA(lpCmdLine, "/UnregServer") == 0) {

UnregisterServer();

return 0;

}

if (lstrcmpiA(lpCmdLine, "-Embedding") == 0 ||

lstrcmpiA(lpCmdLine, "/Embedding") == 0)

OpenFactory();

while(GetMessage(&msg, NULL, 0, 0)) {

TranslateMessage(&msg);

DispatchMessage(&msg);

}

CloseFactory();

::CoUninitialize();

return 0;

}

3.5.6.   CoRegisterClassObject / CoRevokeClassObject

클라이언트의 CoCreateInstance 요청에 의하여 COM 이 로컬 서버를 로드 할 때, 레지스트리에 저장된 로컬 서버의 경로명에 ‘-Embedding’ 명령행 매개변수를 추가한다. 이때 로컬 서버는 명령 행에서 WinMain 함수에 넘어온 매개변수의 값이 Embedding 옵션을 포함하고 있는지를 검사하여 클래스 팩토리 COM 개체를 등록하는 작업을 수행하여야 한다. 로컬 서버가 종료하면 등록된 클래스 팩토리 COM 개체를 해제해야 한다.

1.       COM 은 내부적으로 등록된 클래스 팩토리 COM 개체를 저장하는 ROT(Running Object Table) 이라고 하는 테이블을 관리한다.

2.       클라이언트가 CoGetClassObject 함수를 호출할 때 COM은 먼저 이 ROT 에서 클라이언트가 요청한 CLSID에 대한 클래스 팩토리 COM 개체가 등록되어 있는지를 검사한다.

3.       만약 클래스 팩토리 COM 개체가 이 ROT에 등록되어 있지 않다면 COM 은 레지스트리에서 해당 CLSID와 관련된 로컬 서버에 대한 정보를 꺼내와 ‘Embedding’ 매개변수와 함께 해당 로컬 서버를 실행시킨다.

4.       이때 로컬 서버가 해야 하는 역할은 COM 이 가능한 한 빨리 클래스 팩토리 COM 개체를 찾을 수 있도록 해당 클래스 팩토리 COM 개체를 ROT에 등록하는 일을 수행하는 것이다.

5.       로컬 서버에서 클래스 팩토리 COM 개체를 등록하기 위해서는 COM 라이브러리의 CoRegisterClassObject 함수를 사용한다.

6.       이때 로컬 서버는 메시지 루프에 들어오기 전에 자신이 제공하는 모든 클래스 팩토리를 ROT에 등록해야만 한다.

DWORD g_dwRegister;

CFAddBack* g_pFactory;

BOOL OpenFactory(void)

{

BOOL bOK = FALSE;

HRESULT hr;

g_pFactory = new CFAddBack;

if(g_pFactory != NULL) {

g_pFactory->AddRef();

hr = ::CoRegisterClassObject(CLSID_AddBack,

g_pFactory,

CLSCTX_LOCAL_SERVER,

REGCLS_MULTIPLEUSE,

&g_dwRegister);

bOK = SUCCEEDED(hr);

if(!bOK) {

g_pFactory->Release();

delete g_pFactory;

}

}

else

bOK = FALSE;

return bOK;

}

STDAPI CoRegisterClassObject(

REFCLSID rclsid,         // 등록될 클래스 팩토리 컴포넌트와 관련된 CLSID

LPUNKNOWN pUnk,     // 클래스 팩토리 컴포넌트의 IUnknown 인터페이스 포인터

DWORD dwClsContext,   // 클래스 팩토리의 실행 컨텍스트

DWORD flags,            // 클래스 팩토리가 생성할 수 있는 컴포넌트의 수

LPDWORD lpdwRegister  // 리턴 값 포인터

);

1.       하나의 로컬 서버 인스턴스가 하나의 컴포넌트에 대하여 단 하나의 인스턴스만 서비스 할 경우
CLSCTX_LOCAL_SERVER, REGCLS_SINGLEUSE

2.       하나의 로컬 서버 인스턴스가 하나의 컴포넌트에 대하여 여러 개의 인스턴스를 서비스 할 경우
CLSCTX_LOCAL_SERVER, REGCLS_MULTI_SEPARATE
이것은 재미있는 상황을 발생시킨다. 예를 들어, 몇 개의 COM 개체를 등록하는 로컬 서버가 있다고 하자. 또한 이 로컬 서버 자신이 등록하고 있는 COM 개체 중의 하나를 사용할 필요가 있다고 하지. 위 문장을 사용하여 클래스 팩토리 COM 개체를 등록했다면 로컬 서버가 자기 자신의 COM 개체를 서비스하기 위해 또 다른 자기자신의 로컬 서버 인스턴스를 로드하게 될 것이다. 이것은 대부분의 상황에서 절대로 효율적이지 못하다. 따라서 로컬 서버를 자신의 인-프로세스 서버로 등록해야 한다. 이것을 하기 위해서는 CLSCTX_LOCAL_SERVER 와 CLSCTX_INPROC_SERVER 플래그를 결합해야 한다.

::CoRegisterClassObject(clsid,

pIUnknown,

CLSCTX_LOCAL_SERVER | CLSCTX_INPROC_SERVER,

REGCLS_MULTI_SEPARATE,

&dwRegister);

::CoRegisterClassObject(clsid,

pIUnknown,

CLSCTX_LOCAL_SERVER,

REGCLS_MULTIPLEUSE,

&dwRegister);

로컬 서버가 종료하면 ROT테이블에 등록된 클래스 팩토리를 제거해야 한다.

BOOL CloseFactory(void)

{

BOOL bOK = TRUE;

HRESULT hr;

if (g_dwRegister != 0) {

hr = CoRevokeClassObject(g_dwRegister);

if (FAILED(hr))

bOK = FALSE;

}

if(g_pFactory)

g_pFactory->Release();

return bOK;

}

ROT 내부 테이블에 등록된 클래스 팩토리를 제거하기 위해 COM 라이브러리의 CoRevokeClassObject 함수를 사용한다.

STDAPI CoRevokeClassObject(

DWORD dwRegister

);

CoRevokeClassObject 함수는 dwRegister 라는 DWORD 형의 매개변수 만을 갖는다. 이 매개변수에 지정되어야 하는 값은 CoRevokeClassObject 함수의 실행 결과로 리턴 되는 마지막 매개변수인 DWORD 포인터페이 저장된 값이다. 이 값은 클래스 팩토리에 대한 매직 쿠키(magic cookie)로 사용된다.

쿠키(Cookie)

쿠키란 용어는 어떤 것을 식별할 수 있는 데이터 구조체를 가리키는데 사용된다. 예를 들어, 클라이언트가 서버에게 리소스를 요청하면, 서버는 리소스를 할당하고 쿠키를 클라이언트에게 넘겨준다. 이때 클라이언트는 미래에 리소스를 식별하기 위해 쿠키를 사용할 수 있다. 클라이언트에게 있어서 쿠키란 서버를 제외하고는 아무런 의미가 없는 값일 수도 있다.

3.5.7.   로컬 서버 종료

인-프로세스 서버 경우에는 수동적으로 메모리에서 언로드된다. 즉, COM 라이브러리가 CoFreeUnusedLibraries 함수를 호출하면, 인-프로세스 서버의 DllCanUnloadNow 함수가 호출되고, 이 함수에서는 인-프로세스 서버에 사용중인 COM 컴포넌트가 없을 때 S_OK 를 리턴 함으로써 CoFreeUnusedLibraries 함수가 DLL을 메모리에서 언로드하게 한다.

이에 반하여 로컬 서버인 경우에는 다음과 같은 조건을 만족하면 능동적으로 종료된다.

1.       COM 개체 카운터가 현재 0이고, 클라이언트가 IClassFactory::LockServer(FALSE)를 호출함으로써 마지막 로크 카운터가 0이 될 때.

2.       로크 카운터가 현재 0이고, 클라이언트가 IUnknown::Release 를 호출하여 마지막 COM 개체 카운터가 0이 될 때

여기서 중요한 사실은 인-프로세스 서버에서는 COM 개체 카운터와 로크 카운터가 모두 0일 때 수동적으로 언로드되지만, 로컬서버에서는 위의 두 경우에 즉, 마지막으로 COM 개체 카운터나 로크 카운터가 감소하여 0 상태가 된 경우에 능동적으로 자신을 종료시켜야 한다는 것이다.

HRESULT __stdcall CFAddBack::CreateInstance(

LPUNKNOWN pUnkOuter,

REFIID riid,

LPVOID* ppv)

{

HRESULT hr = E_FAIL;

CAddBack* pAddBack = NULL;

*ppv = NULL;

if(pUnkOuter != NULL)

hr = CLASS_E_NOAGGREGATION;

else {

pAddBack = new CAddBack;

if(pAddBack != NULL) {

// COM 개체 카운터를 증가시킨다.

++g_cObjects;

// 클라이언트에서 요청한 인터페이스 포인터를

// CAddBack COM 개체에게 요청한다.

hr = pAddBack->QueryInterface(riid, ppv);

if(FAILED(hr)) {

// 실패한 경우 COM 개체 카운터를 감소시킨다.

--g_cObjects;

delete pAddBack;

// 가능하다면 종료한다.

CloseExe();

}

}

else

hr = E_OUTOFMEMORY;

}

return hr;

}

HRESULT __stdcall CFAddBack::LockServer(BOOL bLock)

{

if(bLock)

++g_cLocks;

else

--g_cLocks;

return S_OK;

}

ULONG __stdcall CAddBack::Release(void)

{

if(--m_cRef == 0) {

// COM 개체 카운터를 감소시킨다.

--g_cObjects;

delete this;

// 가능하다면 종료한다.

CloseExe();

}

return m_cRef;

}

void CloseExe (void)

{

if (g_cObjects == 0 && g_cLocks == 0)

PostQuitMessage(0);

}

4.      Visual C++ COM 컴파일러

4.1.   COM 재원 C++ 컴파일러 개요

4.2.   형식 라이브러리

4.3.   #import 선행 처리기 지시어

4.4.   C++ 언어 확장

4.5.   COM 지원 클래스

4.6.   내장 COM 지원 AddFront 예제 프로그램 작성