지난 회까지는 C++만을 사용하여 COM 컴포넌트를 하나 만들어 보았다. 이 컴포넌트는 IUnknown만을 지원하기 때문에 VB, 파워빌더
등의 RAD 개발 환경 하에서는 사용할 수가 없었다. 전 회에서 이야기한 것처럼 IDispatch 인터페이스를 지원하는 컴포넌트는 VB를 비롯한
거의 모든 윈도우 개발 환경하에서 사용할 수 있다. 이번 회에는 지난 회에서 만들었던 컴포넌트를 IDispatch를 지원하는 것으로
업그레이드해보고 이를 VB에서 사용하는 예를 보이도록 하겠다.
1. IDispatch 인터페이스란 ?
전 회에서도 이야기했지만 VB와 같은 언어에는 포인터라는 개념이 없다(사실 포인터가 완전히
지원되지 않는 것은 아니고 함수 포인터만 지원된다). 이러한 환경 하에서는 초창기 COM에 맞게 만들어진 컴포넌트들을 호출하려면 무조건 포인터를
사용해야 한다. 따라서 초창기 COM 컴포넌트들은 포인터의 개념이 없는 프로그래밍 언어에서는 사용할 수 없었다. 그래서 만들어진 표준
인터페이스가 IDispatch라는 것이다. 이 인터페이스를 통해서 COM 컴포넌트내의 메소드들이나 프로퍼티를 호출할 수 있다. 즉
IDispatch를 이해하도록 개발 환경을 만들면 COM 컴포넌트를 사용할 수 있는 것이다. 요즘 웬만한 윈도우 개발 환경은 모두
IDispatch를 지원한다. 이렇게 IDispatch를 지원하는 인터페이스를 듀얼 인터페이스(Dual interface)라고 한다.
초창기 COM에서처럼 포인터를 이용해 컴포넌트의 메소드나 프로퍼티를 호출하는 방식은 아무래도 IDispatch 인터페이스를 통한 호출
방식보다 속도가 빠르다. 컴파일되는 시점에 어느 부분을 호출할 것인지 그 주소가 아예 결정되기 때문인데 이에 비해 IDispatch를 통한 호출
방식은 런 타임에 호출되는 부분이 결정되기 때문에 속도가 느리다. IDispatch 인터페이스에는 다음과 같은 멤버 함수들이 존재한다. 즉,
IDispatch 인터페이스를 지원하고 싶다면 다음의 함수들을 구현해주면 된다.
GetTypeInfoCount – 인터페이스가 타입 정보를 제공해줄 수 있는지 여부를 알아낸다.
GetTypeInfo –
인터페이스의 타입 정보를 제공해준다.
GetIDsOfNames – 메소드나 프로퍼티의 이름으로부터 ID(이를 DISPID라고 한다)를
알아낸다.
Invoke – 메소드나 프로퍼티의 ID를 바탕으로 메소드나 프로퍼티를 호출한다.
모두 4개의 함수가 제공되는데 그
중에서 위의 두 개의 함수는 이 인터페이스의 타입 정보가 제공되는지 나타낸다. 전 회에서도 이야기했지만 COM 컴포넌트는 자신이 어떤 메소드와
프로퍼티를 갖고 있는지 명시해놓은 타입 라이브러리라는 것을 갖는다고 했다. 이는 .tlb라는 확장자를 갖는 별도의 파일로 존재하기도 하고
컴포넌트 파일내에 리소스의 일부로 아예 포함되기도 한다. 우리가 만드는 컴포넌트는 타입 라이브러리를 제공하지 않는다. 왜 ? 직접 만들기에는
너무 복잡하기 때문이다. ATL이나 MFC, VB 등으로 컴포넌트를 만들면 자동으로 타입 라이브러리가 만들어지고 컴포넌트 파일의 일부로
삽입된다.
IDispatch 인터페이스의 동작 방식
IDispatch 인터페이스를 통한 메소드 호출은 DISPID라는 번호를 통한 호출이 된다.
즉, 코딩을 할 때는 메소드나 프로퍼티의 이름을 갖고 호출하지만 결국은 이게 각각의 메소드와 프로퍼티에 부여된 숫자로 변경된 다음에 호출이
이루어지는 것이다. 예를 들어 VB에서 MyCom.Interface라는 ProgID를 갖는 컴포넌트의 DisplayMemorySize라는
메소드를 호출하는 코드를 보자.
Dim myObj As Object
Set myObj = CreateObject(“MyCom.Interface”)
myObj.DisplayMemorySize
Set
myObj = Nothing
CreateObject는 COM API로 치면 CoGetClassObject와 IClassFactory의 CreateInstance까지
수행해주는 역할을 한다. 주의할 것은 VB의 CreateObject 함수는 모든 COM 컴포넌트를 다 지원하는 것이 아니라는 점이다. 위에서
이야기한 것처럼 IDispatch 인터페이스를 지원하는 컴포넌트만 만든다. 지원되지 않는 컴포넌트를 생성하려고 하면 생성할 수 없다는 에러
메시지를 낸다. 그런데 위의 코드에서 myObj의 타입을 보면 Object 타입이다. 이 타입은 비주얼 베이직에서 범용 컴포넌트 타입으로
사용된다. 이 타입의 단점은 문법 오류가 체크되지 않는다는 것이다. 사용할 컴포넌트가 어떤 놈인지 정확히 명시하지 않고 사용할 수 있기 때문에
실행해보지 않고서는 없는 메소드나 프로퍼티를 호출했는지 알 수 없다는 것이다. 대신에 융통성은 아주 좋다.
myObj.DisplayMemorySize 코드의 실행 부분을 자세히 설명하자면 먼저 DisplayMemorySize라는 메소드의 DISPID를
알아내기 위해 IDispatch의 GetIDsOfNames 메소드를 호출한다. DISPID를 알아냈으면 이를 바탕으로 IDispatch의
Invoke 메소드를 호출한다. 만일 호출한 메소드가 없다면 GetIDsOfNames에서 에러를 리턴할 것이다. 이는 실제로 실행되는 상황이
아니면 알 수 없다. 그래서 이를 후지정(Late-binding)이라고 한다. 이 방식의 장점은 융통성이 많다는 것이다. 사용하는 컴포넌트의
DISPID가 변경되어도 컴포넌트를 사용하는 프로그램의 코드는 변경할 필요가 없다.
만일 위의 컴포넌트에 타입 라이브러리가 제공된다면 컴포넌트의 타입을 지정할 때 아예 정확한 타입을 지정해줄 수 있다. 예를 들어 위의
코드에서 Dim myObj As Object 부분을 Dim myObj As MyCom.Interface와 같은 식으로 자세한 타입을 지정해 줄
수 있다는 것이다. 그러면 컴파일하는 시점에 문법 오류는 체크할 수 있다. MyCom.Interface와 같은 타입은 원래 VB가 모르는 새로운
타입이다. 이런 새로운 타입이 어떻게 구성되어 있는지 개발 환경(여기서는 VB)에게 알려야 한다. VB에서 새로운 타입의 지정은
프로젝트(Project) 메뉴의 ***(Reference) 명령을 통해 가능하다. 추가하고 싶은 타입의 타입 라이브러리가 들어있는 파일을
선택해주면 된다. 이런 방식은 타입 라이브러리를 통해 호출되는 메소드나 프로퍼티의 DISPID를 미리 알 수 있다. 그렇기 때문에 실행시에
앞서와 같이 GetIDsOfNames와 Invoke를 거쳐 메소드나 프로퍼티가 호출되는 것이 아니라 바로 Invoke만을 호출하게 된다. 그래서
이를 선지정(Early-binding)이라고 한다. 이는 속도적인 측면에서는 장점이 존재하지만 컴포넌트의 DISPID가 변경되었을 경우에는 그
컴포넌트를 사용하는 프로그램의 코드를 다시 컴파일해주어야 한다는 단점이 있다.
——————————————————————————–
참고
VB와 VBScript
VB와 VBScript는 아주 유사하다. VBScript는 VB의 경량급 버전으로 IIS에서 ASP 프로그래밍을
하거나 IE 상의 클라이언트 스크립트로 사용할 수 있다. VBScript는 VB와 비교할 때 폼 등의 UI와 관련된 부분이 빠져 있고 타입도
Variant 타입 하나만을 지원한다. 즉, VBScript에서 컴포넌트를 만들어 쓰게 되면 무조건 후지정(late-binding) 방식이 되는
것이다.
——————————————————————————–
소스를 살펴본 독자라면 알겠지만 아주 간단한 기능을 제공하는 컴포넌트였음에도 불구하고 소스의 양은 만만치 않았다. 누차 이야기하지만 COM
컴포넌트를 직접 구현할 경우에는 코딩의 양도 늘어나고 COM 자체에 대한 해박한 지식을 필요로 한다. 다음 회에서는 지금까지 만들어본 컴포넌트를
ATL을 이용하여 만드는 과정을 설명하도록 하겠다. 보면 알겠지만 훨씬 더 간편하게 만들 수 있다.
2. IDispatch 인터페이스의 추가
그럼 이제 IDispatch 인터페이스를 추가해보자. 지난 회에 만들었던 소스를 바탕으로
작업을 진행하겠다. 먼저 MyCom_i.h에 IMyInterface 인터페이스의 정의가 들어있는데 IMyInterface의 선조를
IUnknown에서 IDispatch로 변경한다.
interface IMyInterface : public IDispatch
{
virtual STDMETHODIMP
DisplayOSType() = 0;
virtual STDMETHODIMP DisplayMemorySize() = 0;
};
여기에 맞춰 CMyInterface의 정의에도 IDispatch의 4 개의 함수를 추가한다.
class CMyInterface : public IMyInterface
{
….
// IUnknown
methods
STDMETHODIMP QueryInterface(REFIID,
LPVOID*);
STDMETHODIMP_(DWORD) AddRef();
STDMETHODIMP_(DWORD)
Release();
// IDispatch method
STDMETHODIMP GetTypeInfoCount(UINT*
pctinfo);
STDMETHODIMP GetTypeInfo(UINT iTInfo, LCID lcid, ITypeInfo**
ppTInfo);
STDMETHODIMP GetIDsOfNames(REFIID riid, LPOLESTR* rgszNames,
UINT cNames, LCID lcid, DISPID* rgDispId);
STDMETHODIMP Invoke(DISPID
dispIdMember, REFIID riid, LCID lcid, WORD wFlag,
DISPPARAMS *pDispParam,
VARIANT *pVarResult, EXCEPINFO *pExcInfo, UINT *puArgErr);
// IMyInterface methods
STDMETHODIMP DisplayOSType();
STDMETHODIMP
DisplayMemorySize();
};
MyCom.cpp에 위의 4개 함수의 정의를 만들어 넣어야 하는데 그러기에 앞서 QueryInterface 메소드에 지원되는 인터페이스
중의 하나로 IDispatch를 추가해야 한다.
STDMETHODIMP CMyInterface::QueryInterface(REFIID riid, LPVOID
*ppReturn)
{
*ppReturn = NULL;
if (IsEqualIID(riid,
IID_IUnknown))
*ppReturn = this;
else if (IsEqualIID(riid,
IID_IDispatch))
*ppReturn = this;
else if (IsEqualIID(riid,
IID_IMyInterface))
*ppReturn = (IMyInterface*)this;
if
(*ppReturn)
{
(*(LPUNKNOWN*)ppReturn)->AddRef();
return
S_OK;
}
return E_NOINTERFACE;
}
이제 IDispatch의 함수들을 구현해보자. 앞에서 이야기했던 것처럼 예제 컴포넌트는 타입 정보를 제공하지는 않을 것이다. 그렇기 때문에
GetTypeInfoCount와 GetTypeInfo는 아주 간단하게 구현된다.
STDMETHODIMP CMyInterface::GetTypeInfoCount(UINT* pctinfo)
{
return
E_NOTIMPL;
}
STDMETHODIMP CMyInterface::GetTypeInfo(UINT iTInfo, LCID lcid, ITypeInfo**
ppTInfo)
{
return E_NOTIMPL;
}
이제 GetIDsOfNames와 Invoke를 구현해야 하는데 이는 상당히 복잡하다. 그나마 IMyInterface에서 제공하는 두 개의
메소드(DisplayMemorySize와 DisplayOSType)의 인자가 없기 때문에 간단하다. 먼저 GetIDsOfNames의 구현 코드
내용부터 보면 다음과 같다.
STDMETHODIMP CMyInterface::GetIDsOfNames(REFIID riid, LPOLESTR* rgszNames,
UINT cNames, LCID lcid, DISPID* rgDispId)
{
if (IID_NULL !=
riid)
return DISP_E_UNKNOWNINTERFACE;
if (cNames != 1)
return DISP_E_UNKNOWNNAME;
// rgszNames로 들어온 유니코드를 ANSI 문자열로 변경한다.
int nLen =
WideCharToMultiByte(CP_ACP, 0, rgszNames[0], -1, NULL, 0, NULL, NULL);
LPSTR
lpName = (LPSTR)malloc(nLen);
WideCharToMultiByte(CP_ACP, 0,
rgszNames[0], -1, lpName, nLen, NULL, NULL);
if (strcmp(lpName,
“DisplayOSType”) == 0)
rgDispId[0] = DISPID_DISPLAYOSTYPE;
else if
(strcmp(lpName, “DisplayMemorySize”) == 0)
rgDispId[0] =
DISPID_DISPLAYMEMORYSIZE;
else
{
free(lpName);
return
DISP_E_UNKNOWNNAME;
}
free(lpName);
return S_OK;
}
GetIDsOfNames는 앞서 이야기한 것처럼 메소드나 프로퍼티의 이름이 주어졌을 때 이 것의 DISPID를 돌려주는 역할을 한다. 첫
번째 인자인 REFIID riid로는 반드시 IID_NULL이 들어오도록 되어 있다. 두 번째 인자인 LPOLESTR* rgszNames로는
함수 이름과 인자 이름들이 넘어온다. 인자들의 경우에는 명명인자(Named parameter)일 경우에만 넘어온다. 예제의 경우 메소드들이 아예
인자가 없기 때문에 여기로는 메소드 이름 하나만 넘어올 일밖에 없다. 주의할 것은 여기로 넘어오는 값은 유니코드라는 점이다. 세 번째 인자는 두
번째가 가리키는 배열의 크기이다. 예제에서는 항상 1이 될 것이다. 1이 아닌 값이 들어온다면 무엇인가 잘못된 것이다. 네 번째 인자는 언어
코드인데 무시해도 관계없다. 다섯 번째 인자가 가장 중요한데 두 번째 인자로 넘어온 메소드와 인자 이름들에 상응하는 DISPID를 이리로
넘겨준다. 위의 코드를 보면 유니코드를 ANSI 문자열로 변경하기 위해 WideCharToMultiByte라는 API를 사용하고 있는데 잘
알아두기 바란다(반대로 ANSI 문자열에서 유니코드 문자열을 만들어내려면 MultiByteToWideChar API를 사용해야 한다). 이
함수는 인자를 주기에 따라서 유니코드 문자열을 ANSI 문자열로 변경할 때 필요한 메모리의 크기도 알아낼 수 있다. 아무튼
DisplayOSType의 경우에는 DISPIDID로 1을 부여하고 DisplayMemorySize의 경우에는 DISPID로 2를 부여하였다.
다음으로 Invoke 메소드의 코드 내용을 살펴보도록 하자. 이 메소드는 DISPID로 지정된 메소드를 실제로 호출하는 역할을 수행한다.
STDMETHODIMP CMyInterface::Invoke(DISPID dispIdMember, REFIID riid, LCID
lcid, WORD wFlags,
DISPPARAMS *pDispParams, VARIANT *pVarResult, EXCEPINFO
*pExcepInfo, UINT *puArgErr)
{
if (IID_NULL != riid)
return
DISP_E_UNKNOWNINTERFACE;
pVarResult = new VARIANT;
if (dispIdMember ==
DISPID_DISPLAYOSTYPE)
{
pVarResult->vt =
VT_ERROR;
pVarResult->scode = DisplayOSType();
}
else if
(dispIdMember == DISPID_DISPLAYMEMORYSIZE)
{
pVarResult->vt =
VT_ERROR;
pVarResult->scode =
DisplayMemorySize();
}
else
{
delete pVarResult;
return
DISP_E_MEMBERNOTFOUND;
}
delete pVarResult;
return S_OK;
}
첫 번째 인자인 DISPID dispIdMember로 호출하고자 하는 메소드나 프로퍼티의 DISPID가 들어온다. 두 번째 인자인
REFIID riid로는 IID_NULL이 들어가야 한다. 세 번째 인자인 LCID lcid는 언어 코드로 상관없다. 네 번째 인자인 WORD
wFlags로는 이것이 메소드 호출인지 프로퍼티 호출인지를 나타내는 값(DISPATCH_METHOD, DISPATCH_PROPERTYGET,
DISPATCH_PROPERTYPUT, DISPATCH_PROPERTYPUTREF)이 들어간다. 다섯 번째 인자인 DISPPARAMS
*pDispParams로는 호출하는 메소드나 프로퍼티의 인자 정보가 들어온다. 예제에서는 사용되지 않는다. 여섯 번째 인자인 VARIANT
*pVarResult로는 메소드나 프로퍼티의 리턴 값을 받는다. 여기서 주의할 것은 이 부분은 호출하는 쪽에서 메모리를 잡아서 넘겨주는 것이
아니라 Invoke 함수내에서 메모리를 잡아서 넘겨주어야 한다는 것이다. 위의 코드에도 그런 부분(pVarResult = new
VARIANT)을 볼 수 있다. 일곱 번째 인자인 EXCEPINFO *pExcepInfo로는 예외 상황 관련 정보를 받는다. 여덟 번째 인자인
UINT *puArgErr로는 잘못 값이 주어진 첫 번째 인자의 인덱스를 지정해준다. 예제에서는 일곱 번째와 여덟 번째 인자 역시 사용하지
않는다.
이 것으로 IDispatch 인터페이스 관련 부분의 코딩은 끝이 났다. 이 컴포넌트를 실제로 VB 같은 개발 환경에서 사용하려면
ProgID가 부여되어야 한다. 이 컴포넌트의 ProgID는 “MyCom.Interface”라고 하겠다. 이를 레지스트리에 등록하는 부분을
추가하기 위해 MyCom.cpp의 RegisterServer 함수에 관련 부분을 추가하였다.
3. 테스트 프로그램의 작성
이제 이렇게 만든 컴포넌트를 사용하는 프로그램을 만들어 보도록 하자. 전 회에서는 C++로만 테스트
프로그램을 만들었다. 이제는 IDispatch 인터페이스가 지원되기 때문에 VB로도 테스트 프로그램을 만들어 보도록 하겠다.
C++ 테스트 프로그램 (TestCom)
먼저 C++에서 IDispatch 인터페이스를 통해서 IMyInterface의 메소드들을
호출하는 예를 보도록 하자. CoGetClassObject와 IClassFactory::CreateInstance를 호출하는 부분은 전 회에서
보았던 테스트 프로그램의 소스와 동일하다. IDispatch 인터페이스를 얻어내고 호출하려는 메소드의 DISPID를 GetIDsOfNames를
통해 얻어낸 다음 Invoke를 호출한다.
// 먼저 IDispatch 인터페이스를 얻는다.
if
(pMyInf->QueryInterface(IID_IDispatch, (void **)&pDispatch) !=
S_OK)
{
pMyInf->Release();
CoUninitialize();
}
pvResult
= NULL;
szMember = L”DisplayMemorySize”; // 문자열 앞에 L을 붙이면 유니코드 문자열이
된다.
pDispatch->GetIDsOfNames(IID_NULL, &szMember, 1,
LOCALE_USER_DEFAULT, &dispID);
pDispatch->Invoke(dispID, IID_NULL,
LOCALE_USER_DEFAULT, DISPATCH_METHOD,
NULL, pvResult, NULL, NULL);
if
(pvResult)
free(pvResult);
pvResult = NULL;
szMember =
L”DisplayOSType”;
pDispatch->GetIDsOfNames(IID_NULL, &szMember, 1,
LOCALE_USER_DEFAULT, &dispID);
pDispatch->Invoke(dispID, IID_NULL,
LOCALE_USER_DEFAULT, DISPATCH_METHOD,
NULL, pvResult, NULL, NULL);
if
(pvResult)
free(pvResult);
pDispatch->Release();
위의 코드에서 볼 수 있는 것처럼 IDispatch 인터페이스를 통한 호출은 좀 번거롭다. 하지만 VB에서는 아주 깨끗한 코드가 나온다.
VB 테스트 프로그램 (TestComVB)
예제 컴포넌트는 타입 라이브러리를 지원하지 않기 때문에 선지정 방식으로 사용할 수는 없다.
아래의 코드를 보면 알 수 있듯이 VB에서 아래의 호출 코드를 알아서 IDispatch 인터페이스를 사용하는 코드로 변경하여 실행주기 때문에
간편하기 그지 없다.
Dim comMy As Object
Set comMy =
CreateObject(“MyCom.Interface”)
comMy.DisplayMemorySize
comMy.DisplayOSType
Set
comMy = Nothing
다음 회에는 같은 컴포넌트를 ATL를 이용해 만들어 보도록 하겠다. 그 과정을 통해서 ATL이 얼마나 강력한 COM 개발도구인지 알 수
있을 것이다.
——————————————————————————–
Copyright
2000ⓒ 한기용 Designed By 한기용