这是我翻译的DirectX8.1SDK文档,第一次翻译,有些词汇翻译的不太准。
1. 什么是COM对象
COM对象同C++对象的不同:
A. COM对象比C++对象有强制性的更严格的封装。COM对象的方法被一个或多个接口组织起来。要使用一个方法,必须创建一个对象并且从对象中得到合适的接口。
B. COM对象不像C++对象那样创建。有几种方式创建COM对象,但都使用了特定的COM技术。
C. 必须通过特定的COM技术控制COM对象的生存期。
D. COM对象不需要显式地加载。
E. COM是一种二进制规范。
对象与接口:
A. 一个对象可以展示出任意数目的接口。
B. 多个对象可能展示出同样的接口。
COM对象要求接口的定义在其被发布后就不能被改变。
GUIDs,COM大量使用GUID是为了:
A. 标志一个唯一的特定COM对象。
B. 标志一个唯一的特定接口。
HRESULT,所有的COM方法都返回一个32位整数HRESULT。HRESULT中包含了两个信息:
A. 方法成功还是失败。
B. 方法提供的操作的结果的详细信息。
应该用SUCCESS / FAILED 宏来进行方法返回值的判断。
指针的地址
IDirect3DDevice8** ppReturnedDeviceInterface
不同于C++,你不能直接访问COM对象方法。取而代之,应该取得一个展示了该方法的接口指针。
二重指针的原因是你不能直接创建接口指针。必须调用某种Create方法取得接口指针。
声明一个变量指向该接口,并将改变量的地址传递给Create方法。
2. 创建COM对象
有几种方法创建COM对象,最常用的两种是:
A. 通过给函数CoCreateInstance传递对象的CLSID直接创建。该函数创建一个对象的实例,并且返回所指定的接口指针。
B. 通过调用DirectX创建对象的方法或函数间接创建。该方法创建一个对象并且返回对象上的一个接口指针。如果通过这种方法创建对象的话,通常不能指定返回哪个接口指针。
在创建对象之前,必须调用CoInitialize对COM初始化。如果间接创建对象,由对消创建方法处理初始化。如果需要通过CoCreateInstance创建对象,则必须显式调用CoInitialize。当结束的时候,必须通过调用CoUninitialize进行卸载(uninitialize)。如果调用了CoInitialize则必须调用CoUninitialize进行匹配。典型地,应用程序在启动过程中显式初始化COM,并在清理过程中卸载COM。
直接创建:
IDirectPlay8Peer* g_pDP = NULL;
...
CoInitialize(NULL );
...
hr = CoCreateInstance( CLSID_DirectPlay8, NULL, CLSCTX_INPROC_SERVER,
IID_IDirectPlay8Peer, (LPVOID*) &g_pDP);
if (FAILED (hr))
{
MessageBox (NULL, TEXT ("Failed Creating IDirectPlay8Peer. "),
TEXT ("DirectPlay Sample"), MB_OK | MB_ICONERROR);
return FALSE;
}
间接创建:
IDirect3DDevice8 *g_pd3dDevice = NULL;
...
if (FAILED (g_pD3D->CreateDevice(D3DADAPTER_DEFAULT,
3DDEVTYPE_HAL,
hWnd,
D3DCREATE_SOFTWARE_VERTEXPROCESSING,
&d3dpp,
&g_pd3dDevice)))
return E_FAIL;
3. 使用COM接口
当对象创建后,创建函数返回一个接口的指针。可以通过这个指针访问接口的方法。语法上与通过指针访问C++方法一样。
IDirectPlay8Peer* g_pDP = NULL;
...
CoInitialize (NULL);
...
hr = CoCreateInstance ( CLSID_DirectPlay8, NULL, CLSCTX_INPROC_SERVER,
IID_IDirectPlay8Peer, (LPVOID*) &g_pDP);
hr = g_pDP->Initialize( NULL, DirectPlayMessageHandler, 0 );
请求另外的接口
在许多时候,从创建方法得到的接口指针正好是你所需要的。事实上,从一个对象只导出一个IUnknown以外的接口并不常见。许多对象导出多个接口,你有可能需要其中的一些。如果你需要的不仅仅是从创建方法返回的那一个接口的话,并不需要创建一个新对象。取而代之的是使用对象的IUnkonwn::QueryInterface方法请求其他的接口指针。
如果使用CoCreateInstance创建对象,可以申请一个IUnknown接口指针,并通过调用IUnkonwn::QueryInterface申请需要的接口。然而,这种方法当只需要一个接口的时候并不方便,并且在创建不允许指定返回特定指针的时候一点也没有用。实际上,并不需要显式的包含一个IUnknown接口指针,因为所有COM接口都继承了或扩展了IUnknown接口。
扩展一个接口类似于继承一个C++类。子接口展示了所有父接口的方法,并且加上了一个或多个自己的方法。事实上,经常看到用“继承自”代替“扩展”。需要记住的是继承是内嵌在对象中的。你的程序不能继承或扩展一个对象的接口。然而,可以用一个子接口调用子或父的方法。
因为所有的接口都是IUnknown的子接口,你可以用已经拥有的对象的任何一个接口调用QueryInterface。这个时候,必须提供所申请的接口的IID以及一个接口指针的地址。
IDirectSoundBuffer8* pDSBPrimary = NULL;
IDirectSound3DListener8* pDSListener;
...
if(FAILED(hr = g_pDS->CreateSoundBuffer( &dsbd, &pDSBPrimary, NULL )))
return hr;
if(FAILED(hr = pDSBPrimary->QueryInterface(IID_IDirectSound3DListener8,
(LPVOID *)&pDSListener)))
return hr;
4. 管理一个COM对象的生存期
当创建好一个对象时,系统分配给必要的内存资源。当不再需要对象时,应该将其销毁掉(destroy)。系统就能将那些内存用在别的地方。对C++对象而言,可以直接通过new和delete操作符控制对象的生存期。COM不允许直接创建或销毁对象。原因是同一个对象可能被许多程序使用。如果一个程序破坏了一个对象,其他的程序可能就会失败。取而代之的是COM用一套引用计数的系统控制对象的生存期。
一个对象的引用计数是该对象的接口被申请的次数。每当一个接口被申请的时候,就增加引用计数。当应用程序不再需要接口的时候释放该接口,就减少引用计数。只要引用计数大于零,对象就保留在内存中。当引用计数为零时,对象销毁自身。你无需知道一个对象的引用计数。只要适当的取得或释放接口,对象就有合适的生存期。
注意:适当的处理引用计数是COM编程中很重要的一部分。处理得不好很容易导致内存泄露。COM程序员最常见的错误就是没有释放接口。这样的话,引用计数永远不会为零,对象就会一直驻留在内存中。
增加或减少引用计数
当取得一个新的接口指针的时候,必须通过调用IUnknown::AddRef来增加引用计数。然而,你的应用程序无需调用这个方法。当通过一个对象创建方法,或是通过IUnknown::QueryInterface取得对象接口指针时,对象会自动增加引用计数。然而,当你通过别的方法创建接口指针的时候,比如说拷贝一个接口指针,必须显式调用IUnknown::AddRef。否则,当你释放原始指针的时候,尽管你可能仍需使用拷贝指针,对象也可能被销毁。
IDirectSoundBuffer8* pDSBPrimary = NULL;
IDirectSound3DListener8* pDSListener = NULL;
IDirectSound3DListener8* pDSListener2 = NULL;
...
//Create the object and obtain an additional interface.
//The object increments the reference count.
if(FAILED(hr = g_pDS->CreateSoundBuffer( &dsbd, &pDSBPrimary, NULL )))
return hr;
if(FAILED(hr=pDSBPrimary->QueryInterface(IID_IDirectSound3DListener8,
(LPVOID *)&pDSListener)))
return hr;
//Make a copy of the IDirectSound3DListener8 interface pointer.
//Call AddRef to increment the reference count and to ensure that
//the object is not destroyed prematurely
pDSListener2 = pDSListener;
pDSListener2->AddRef();
...
//Cleanup code. Check to see if the pointers are still active.
//If they are, call Release to release the interface.
if(pDSBPrimary != NULL)
{
pDSBPrimary->Release();
pDSBPrimary = NULL;
}
if(pDSListener != NULL)
{
pDSListener->Release();
pDSListener = NULL;
}
if(pDSListener2 != NULL)
{
pDSListener2->Release();
pDSListener2 = NULL;
}
5. 使用C访问COM对象
尽管C++是最常用的COM编程语言,你仍然可以使用C访问COM对象。通过相对直接但是需要某些更加复杂的语法。
A. 在所有方法的参数列表的开始要添加一个额外的参数。该参数必须设成接口指针。
B. 必须显式引用对象的vtable。
每一个COM对象都有一个vtable,包含了对象展示的方法指针的列表。一个接口指针指向vtable中合适的位置,vtable中依次包含了指向所调用的方法的指针。Vtable在文档的其他地方并没有被提到,因为对C++而言,vtable是不可见的。然而,如果要通过C调用COM方法,必须包含一个显式引用vtable的间接层。
C++:
g_pDP->Initialize( NULL, DirectPlayMessageHandler, 0 );
C:
g_pDP->lpVtbl->Initialize(g_pDP,NULL, DirectPlayMessageHandler, 0);
某些组件在其头文件中包含了一些宏定义,用于决定正确的调用约定。详情请参阅“使用宏调用DirectX COM方法”。
6. 使用宏调用DirectX COM对象
许多Microsoft® DirectX®接口都为每个方法定义了宏,使在应用程序使用这些方法更方便。可以在接口声明的同一个头文件中找到这些宏的定义。这些宏设计成被C和C++程序使用。要使用C++宏,必须定义_cplusplus。否则,将使用C宏。宏的语法在两种语言中是一样的,但是在头文件中包含了扩展成合适的调用约定的独立的宏定义集。
#if !defined(__cplusplus) || defined(CINTERFACE)
...
#define IDirect3D8_GetAdapterIdentifier(p,a,b,c) (p)->lpVtbl->GetAdapterIdentifier(p,a,b,c)
...
#else
...
#define IDirect3D8_GetAdapterIdentifier(p,a,b,c) (p)->GetAdapterIdentifier(a,b,c)
...
#endif
要使用这些宏中的一个,首先必须取得关联的接口的指针。宏的第一个参数必须设置为该指针。其他的参数影射到方法的参数。宏的返回值为方法所返回的HRESULT值。
pD3D为IDirect3D8接口指针:
hr = IDirect3D8_GetAdapterIdentifier(pD3D,
Adapter,
dwFlags,
pIdentifier);
7. DirectX COM文档约定
根据约定,在本文档中提到的COM方法及接口都是通过相应的C++类名来引用。因此,接口IDirectPlay8Peer的Initialize方法为IDirectPlay8Peer::Initialize。这种约定的主要原因是,不同的接口可能导出同样名字但是功能及语法完全不同的方法。比如,许多接口都有Init或Initialize方法,但是功能以及参数可能十分不同。使用C++类名是一种唯一标识方法的捷径。