컴포넌트 오브젝트 모델(COM)은 마이크로소프트가 개발한 소프트웨어 구성 요소들의 응용 프로그램 이진 인터페이스 표준이다.
응용 프로그램 이진 인터페이스란? 응용 프로그램과 운영 체제 또는 응용 프로그램과 해당 라이브러리, 마지막으로 응용 프로그램의 구성요소 간에서 사용되는 낮은 수준의 인터페이스이다.
ABI는 API와 다르다, API가 소스 코드에서 사용 된다면 ABI는 바이너리에서 호완이 가능하다는 점이 다르다.
ABI(Application Binary Interface)란?
ABI란 두개의 바이너리 프로그램 모듈 사이의 인터페이스를 말한다.
기계 수준, 이진값 수준에서 인터페이스를 뜻한다. 이진은 Binary로 0과 1을 뜻하는데 컴퓨터에서 가장 low-level을 나타낸다. ABI는 이렇게 바이너리 수준에서의 인터페이스르 뜻한다.
위 정의에서 말하는 모듈은 라이브러리 혹은 Operating System과 사용자에 의해서 실행되는 프로그램을 의미한다.
윈도우 환경에서 내 프로그램을 돌리는 경우를 생각해 볼 수 있다.
그런데 ABI가 호환되어야 프로그램을 돌릴 수 있다. 맥 환경에서 Window 응용 프로그램을 돌릴 수 없는 것이 ABI가 호환되지 않기 때문이다.
ABI와 API의 비교
•
API는 함수의 argument를 전달하는 순서를 정의한다면,
ABI는 이 arguement를 어떻게 전달할지에 대한 방법을 정의한다. (ex. register, stack 등)
•
API가 라이브러리의 일부 함수를 정의한다면,
ABI는 코드가 라이브러리 파일 안에 어떻게 저장될 것인지를 정의한다. 이를 통해서 라이브러리를 사용한 어떤 프로그램도 원하는 함수를 실행할 수 있게 된다.
•
API는 소스코드 레벨에서 호환이 된다면, ABI는 바이너리 수준(이진 수준, 기계 수준)에서 호환이 된다.
COM을 이용해 개발된 프로그램들은 프로세스간 통신과 동적 오브젝트 생성이 가능하다. 소프트웨어 개발에서는 COM라는 용어를 종종 OLE,OLE 자동화, ActiveX, COM+, DCOM 기술을 포함하는 포괄적 개념으로 사용한다.
다양한 플랫폼에서 COM이 구현되었지만, 주로 마이크로소프트 윈도우에서 사용 된다. COM은 닷넷 프레임워크와 같은 다른 기술로 대체되리라 전망된다.
세부 사항
COM 프로그래머는 COM 컴포넌트들을 사용 하여 소프트웨어를 개발한다. 각각의 COM 컴포넌트들은 클래스 아이디(CLSIDs)로 식별하는데, 클래스 아이디는 전역 고유 식별자(GUID) 형식으로 되어있다. COM 컴포넌트의 기능은 하나 이상의 인터페이스를 통해 노출된다. 컴포넌트에서 지원하는 인터페이스들은 인터페이스 아이디(IIDs)로 식별하는 데 이것 역시 GUID이다.
COM 인터페이스는 C,C++, 비주얼 베이직과 같은 프로그래밍 언어 및 윈도 플랫폼에서 구현된 여러 종류의 스크립트 언어와 바인딩된다. 컴포넌트에 대한 접근은 모두 인터페이스 메소드를 통해 이루어 지는데, 이를 통해 프로세스간(inter-process) 또는 심지어 컴퓨터간(inter-computer) 프로그래밍(후자는 DCOM을 사용한다.) 같은 기법이 허용 된다.
인터페이스
모든 COM 컴포넌트는 표준 IUnknown 인터페이스를 구현한다. 실제 모든 COM 인터페이스는 IUnknown 인터페이스로부터 상속된다. IUnknown 인터페이스는 AddRef(), Release(), QueryInterface() 이렇게 3개의 메서드로 이루어진다.
IUnknown
{
public:
BEGIN_INTERFACE
virtual HRESULT STDMETHODCALLTYPE QueryInterface(
/* [in] */ REFIID riid,
/* [iid_is][out] */ __RPC__deref_out void __RPC_FAR *__RPC_FAR *ppvObject) = 0;
virtual ULONG STDMETHODCALLTYPE AddRef( void) = 0;
virtual ULONG STDMETHODCALLTYPE Release( void) = 0;
template<class Q>
HRESULT
QueryInterface(Q** pp)
{
return QueryInterface(__uuidof(Q), (void **)pp);
}
END_INTERFACE
};
C++
복사
•
AddRef(), Release(): 참조 카운팅을 구현하며 따라서 인터페이스의 수명을 관리하는 데 사용된다.
•
QueryInterface(): 인터페이스 아이디로 지정한 인터페이스를 컴포넌트로부터 얻어오는 데 사용된다. QueryInterface()는 C++의 dynamic_cast<> 또는 D, 자바, C#의 캐스트와 비슷한 역할을 수행한다.
COM 컴포넌트의 인터페이스는 반사적(reflexive)이며, 대칭적(symmetric)이고, 추이적(transitive)인 특성을 보여야 한다.
•
반사적 특성 : QueryInterface() 메서드를 호출하여 인터페이스를 얻을 경우 항상 동일한 인스턴스로부터 인터페이스를 반환해야 함을 나타낸다.
•
대칭적 특성 : 인터페이스 B가 인터페이스 A의 QueryInterface()를 통해 얻어졌다면, 인터페이스 A 역시 인터페이스 B로부터 얻을 수 있어야 함을 나타낸다.
•
추이적 특성 : 만약 인터페이스 B가 인터페이스 A로부터 얻을 수 있고 인터페이스 C가 인터페이스 B로부터 얻을 수 있다면, 인터페이스 C는 인터페이스 A로부터 얻을 수 있어야 함을 나타낸다.
인터페이스는 가상 메소드 테이블에 대한 포인터로 구성된다. 가상 함수 테이블은 인터페이스가 정의하는 함수 선언을 실제로 구현하고 있는 함수들에 대한 포인터의 리스트를 포함하며, 인터페이스에 선언된 함수의 순서와 동일한 순서대로 함수 포인터를 저장한다. 이러한 함수 포인터의 구조체를 전달하는 기법은 OLE 1.0이 자신의 시스템 라이브러리와 통신하기 위해 사용한 방법과 매우 유사하다.
COM은 컴포넌트끼리 서로 통신이 가능하도록 많은 다른 표준 인터페이스를 정의하고 있다. 예를 들어 IStream 인터페이스는 데이터 스트림을 나타내는 컴포넌트로 말미암아 노출된다. (파일을 읽고 쓰기 위한 FileStream 컴포넌트가 한 예이다.) 이런 컴포넌트는 스트림 읽기 및 쓰기를 할 수 있는 Read와 Write 메서드를 갖는다. 다른 표준 인터페이스인 IOleObject는 컨테이너에 연결되거나 포함되는 컴포넌트로 말미암아 노출된다. IOleObject는 호출자가 컴포넌트의 테두리 사각형 크기를 결정할 수 있게 하거나 컴포넌트가 'Open', 'Save' 같은 동작을 지원하는지 여부를 확인할 수 있게 하는 등의 기능을 지원하는 메서드를 포함하고 있다.
클래스
COM에서 클래스는 coclass라고 불린다.
coclass는 하나 이상의 인터페이스를 구체적으로 구현한 것이다. coclass는 COM 컴포넌트 개발을 지원하는 모든 언어(예를 들어 C++나 비주얼 베이직 등)로 작성할 수 있다.
윈도 개발에 대해 COM이 한 주요 기여 중 하나는 인터페이스를 구현으로부터 분리한다는 개념을 도입했다는 점이다. COM에서는 객체를 인터페이스를 통하지 않고 직접 접근할 수 없기 때문에 반드시 인터페이스와 구현이 분리되어야 한다. 이와 함께 COM은 단일 인터페이스 다중 구현을 지원한다. 단일 인터페이스 다중 구현 개념은 애플리케이션이 실행 중에 인터페이스에 대한 여러 구현 중 하나를 선택하여 인터페이스를 인스턴스화 할 수 있다는 것을 의미한다.
[
uuid(12345678-ABCD-EF12-3456-7890ABCDEF01),
helpstring("SimpleCalculator Library")
]
library SimpleCalculatorLib
{
import "oaidl.idl";
import "ocidl.idl";
[
uuid(12345678-ABCD-EF12-3456-7890ABCDEF02),
helpstring("ISimpleCalculator Interface")
]
interface ISimpleCalculator : IDispatch
{
[id(1), helpstring("method Add")] HRESULT Add(int a, int b, int *pVal);
[id(2), helpstring("method Subtract")] HRESULT Subtract(int a, int b, int *pVal);
};
[
uuid(12345678-ABCD-EF12-3456-7890ABCDEF03),
helpstring("CSimpleCalculator Class")
]
coclass CSimpleCalculator
{
[default] interface ISimpleCalculator;
};
};
C++
복사
인터페이스 정의 언어와 타입 라이브러리
타입 라이브러리는 COM 타입들을 설명하는 메타데이터를 포함한다. 이러한 COM 타입들에 대한 설명은 인터페이스 정의 언어(Interface Definition Language, IDL)를 통해 작성되며 컴파일 될 수 있도록 텍스트 파일로 저장된다.
COM 컴포넌트 개발에서 IDL을 사용하여 먼저 타입들을 정의하면서 시작하는 건 흔히 있는 일이다. IDL 파일은 개발자가 어떠한 프로그래밍 언어와도 관련되지 않는 방법으로 객체 지향적인 클래스, 인터페이스, 구조체, 열거형 및 다른 사용자 정의 타입들을 정의할 수 있도록 해준다.
IDL 파일은 MIDL 컴파일러에서 타입 라이브러리(.TLB 파일)로 컴파일된다. 이러한 타입 라이브러리에 저장된 바이너리 메타데이터는 VB, VC++, 델파이 등과 같은 다양한 프로그래밍 언어로 처리하게 된다. 이와 같은 처리의 결과로 각각의 언어 컴파일러는 자신의 언어에 맞는 형식(VB를 위한 VB 클래스, VC++를 위한 클래스, 구조체, 매크로 및 typedef 등)을 만들게 되고, 이런 형식들은 .TLB 파일(궁극적으로는 원래 정의된 IDL 파일)에 정의된 coclass를 나타내게 된다.
오브젝트 프레임워크로서의 COM
COM의 기반이 되는 개념들은 객체 지향성에 그 뿌리를 두고 있다. COM은 프로그램을 객체 지향적으로 개발하고 배치하기 위한 인터페이스이다.
COM은 런타임 프레임워크이기 때문에 실행 중에 타입들을 개별적으로 식별하거나 지정할 수 있다. 이를 위해 전역 고유 식별자(GUID)가 사용된다. 각각의 COM 타입에는 실행 시 식별 될 수 있도록 GUID가 할당된다.
COM 타입에 대한 정보를 컴파일시 및 실행시에 접근할 수 있게 하기 위해, COM은 타입 라이브러리를 소개하고 있다. 타입 라이브러리의 효과적인 사용을 통해 COM은 오브젝트 사이의 상호 동작을 지원하는 동적 프레임워크로서의 기능을 수행한다.
IDL에서의 다음과 같은 coclass 정의 예제를 살펴 보자 :
coclass MyObject
{
[default] interface IMyObject;
[default, source] dispinterface _IMyObjectEvents;
};
JavaScript
복사
위의 코드는 MyObject라는 이름의 COM 클래스를 선언하고 있다. 이 클래스는 IMyObject라는 이름의 인터페이스를 구현하며, _IMyObjectEvents 이름의 이벤트 인터페이스를 지원한다.
이벤트 인터페이스 부분을 무시하면, 이것은 다음과 같은 C++ 클래스 정의와 개념적으로 동일하다 :
class CSomeObject : public ISomeInterface
{
...
...
...
};
JavaScript
복사
위의 코드에서 ISomeInterface는 C++ 가상 클래스이다.
이제 다시 MyObject COM 클래스를 보자 : MyObject를 위한 coclass가 IDL에 정의되고, 그것으로부터 타입 라이브러리가 컴파일되었다면, 각각의 언어 컴파일러는 이러한 타입 라이브러리를 읽고 적절히 해석하여, 개발자가 COM이 이해할 수 있는 coclass인 MyObject를 구현하고 궁극적으로 바이너리 실행 코드를 생성할 수 있도록 필요한 모든 코드를 (각각의 언어에 적합한 형태로) 생성해야 할 의무가 있다.
COM coclass가 구현되고 시스템에서 사용할 수 있게 되었다면, 이제 이를 어떻게 인스턴스화 할 지의 문제가 뒤따르게 된다. 현재 C++ 같은 언어에서는, CoCreateInstance() API를 인스턴스화 할 coclass의 CLSID (CLSID_MyObject)와 해당 coclass에서 사용하려는 인터페이스 IID (IID_IMyObject)를 지정하여 사용할 수 있다. CoCreateInstance()를 호출하는 다음과 같은 코드는 :
CoCreateInstance
(
CLSID_MyObject,
NULL,
CLSCTX_INPROC_SERVER,
IID_IMyObject,
(void**)&m_pIMyObject
);
JavaScript
복사
개념적으로 다음의 C++ 코드와 동일하다 :
ISomeInterface* pISomeInterface = NULL;
pISomeInterface = new CSomeObject();
JavaScript
복사
첫 번째 경우는 COM 서브 시스템에게 IMyObject 인터페이스를 구현하는 오브젝트에 대한 포인터를 얻어오되 이 인터페이스를 구현하는 CLSID_MyObject 구현을 통해 얻어오고 있다. 두 번째 경우는 ISomeInterface 인터페이스를 구현하는 C++ 클래스의 인스턴스를 CSomeObject 클래스를 통해 생성하고 있다.
이 두개가 (개념적으로) 같다는 것은 명확하다. coclass는 COM 세계에서의 객체 지향적 클래스이다. coclass의 주요 특징은 (1) 원래부터 바이너리 규약이고, 따라서 결과적으로 (2) 프로그래밍 언어에 독립적이다.
CoClass 예제 코드
다음은 간단한 계산기를 구현하는 CoClass의 예제 코드입니다.
1. IDL 파일 (SimpleCalculator.idl)
[
uuid(12345678-ABCD-EF12-3456-7890ABCDEF01),
helpstring("SimpleCalculator Library")
]
library SimpleCalculatorLib
{
import "oaidl.idl";
import "ocidl.idl";
[
uuid(12345678-ABCD-EF12-3456-7890ABCDEF02),
helpstring("ISimpleCalculator Interface")
]
interface ISimpleCalculator : IDispatch
{
[id(1), helpstring("method Add")] HRESULT Add(int a, int b, int *pVal);
[id(2), helpstring("method Subtract")] HRESULT Subtract(int a, int b, int *pVal);
};
[
uuid(12345678-ABCD-EF12-3456-7890ABCDEF03),
helpstring("CSimpleCalculator Class")
]
coclass CSimpleCalculator
{
[default] interface ISimpleCalculator;
};
};`
JavaScript
복사
2. C++ 구현 파일 (SimpleCalculator.cpp)
C++
#include <windows.h>#include "SimpleCalculator.h"class CSimpleCalculator : public ISimpleCalculator
{
public:
STDMETHOD(Add)(int a, int b, int *pVal)
{
*pVal = a + b;
return S_OK;
}
STDMETHOD(Subtract)(int a, int b, int *pVal)
{
*pVal = a - b;
return S_OK;
}
};
class CSimpleCalculatorFactory : public IClassFactory
{
// ... IClassFactory 인터페이스 구현 ...
};
// DllRegisterServer, DllUnregisterServer 함수 구현
JavaScript
복사
3. 클라이언트 코드
C++
#include <windows.h>#include <comutil.h>#include "SimpleCalculator.h"int main()
{
CoInitialize(NULL);
ISimpleCalculator *pCalculator = NULL;
HRESULT hr = CoCreateInstance(CLSID_CSimpleCalculator, NULL, CLSCTX_INPROC_SERVER,
IID_ISimpleCalculator, (void**)&pCalculator);
if (SUCCEEDED(hr))
{
int result;
pCalculator->Add(5, 3, &result);
printf("5 + 3 = %d\n", result);
pCalculator->Release();
}
CoUninitialize();
return 0;
}
JavaScript
복사
코드 설명
•
IDL 파일:
◦
coclass 키워드를 사용하여 CoClass를 정의합니다.
◦
[default] 속성은 클라이언트가 CoClass를 생성할 때 기본적으로 사용할 인터페이스를 지정합니다.
•
C++ 구현 파일:
◦
CoClass는 인터페이스를 구현하는 클래스입니다.
◦
IClassFactory 인터페이스를 구현하여 CoClass의 인스턴스를 생성합니다.
•
클라이언트 코드:
◦
CoCreateInstance 함수를 사용하여 CoClass의 인스턴스를 생성합니다.
◦
생성된 객체의 인터페이스 포인터를 통해 메서드를 호출합니다.