COM学习笔记(三)

1. IDispatch接口
(1) 自动化与IDispatch接口
自动化是基于COM的IDispatch接口的,IDispatch接口提供了一系列的方法,使得客户机程序能够在基于自动化的服务器上动态访问组件的功能。这种动态调用与COM的自定义接口技术不同:使用自定义接口时,客户机程序需要了解组件接口的一些编译时信息,这些信息要么通过类型库里获取,要么通过编译时包含组件接口声明的头文件来获取(如imath.h),这种是组件接口的前绑定(编译时绑定)。如果一个组件在实现它的方法时使用IDispatch接口而不是自定义的接口,则能够提供更多功能。客户机程序与服务器应用程序之间交互,必须对组件的接口方法使用后绑定。这种安排方式使得服务器组件在更改它的接口时不需要客户机程序重新编译和链接。使用IDispatch接口的另一个重要的好处是它的通用调度机制(universal marshaling),并不需要创建并随之附带一个proxy/stud DLL.

(2) IDispatch接口结构
大多数COM接口与IMath的例子非常相似,它们提供了一个结构,该结构需要一个严格按规定实现的抽象类。 IDispatch略有不同,它给Vtable格式的接口添加了一个间接层次,客户机程序不用通过Vtable指针去访问组件的功能,例如我们使用IMath接口访问组件功能时就需要一个IMath指针。

如左图所示,现在Vtable不必直接访问IMath接口,相反,第一次对接口的调用必须是IDispatch::Invoke,该方法包含一个参数,用于把对方法的调用映射到派发映射中的一个入口。 方法 说明
Invoke Invoke方法提供了IDispatch接口大部分功能,8个参数中最重要的是DISPID,此参数被映射为 派发表中一个确定的偏移量,用来确定哪个接口方法将被调用。
GetIDsOfNames 为客户机程序提供了一种将基于文本的自动化服务器属性或方法名(如Add)映射到DISPID 数字值的功能。经映射以后就可以使用DISPID和Invoke函数来访问组件的方法了。

//Step1 — Create an instance of the math component and QI for IDispatch
CLSIDFromProgID(“COM.Learning.Note.Math”, &clsid);
IDispatch pDispatch = 0;
CoCreateInstance(clsid, … IID_IDispatch, (void**)&pDispatch);

//Step2 — Get the DISPID for Add
DISPID dispid;
pDispatch->GetIDsOfNames(“Add”, &dispid);

//Step3 — Build the parameters based on the context of the call and then invoke the method
pDispatch->Invoke(dispid, parameters, &lresult, …);

//Step4 — Release the pointer
pDispatch->Release();
(3) 双向接口
一个双向接口(Dual interface)相当于把一个自定义接口(如IMath)和一个标准的IDispatch接口结合在了一起。如上面右图所示。如果服务器有一个进程内的实现(DLL),则不需要调度。此时客户机陈故乡可以直接绑定到自定义接口方法,而且调用时效率很高。儿当客户机程序需要后绑定功能时,就可以使用IDispatch实现。

2. 自动化数据类型
自动化为参数和返回值在不同进程和计算机之间进行传输提供了标准的调度方法,它是通过调度器OLEAUT32.DLL实现的。
(1) VARIANT数据类型
VARIANT数据类型提供了一种十分有效的机制,可以实现各种不同的自动化数据的传输,因为VARIANT中既包含了数据本身,还包含了数据的类型。VARIANT数据类型实际上就是一个包含不同数据类型的大的union,使用一个标识符来表示什么类型的数据正存放在union中。e.g.

VARIANT varResult;
VariantInit(&varResult);
varResult.vt = VT_l4;
varResult.lVal = 1045;
相关API: 函数 使用目的
VariantInit 将变量初始化为VT_EMPTY
VariantClear 通过释放变量所占用的内存来初始化VARIANT类型变量, 当客户端程序需要从服务器传来的VARIANT类型变量清除时,可使用
VariantChangeType 将变量从一种数据类型强制转换为另一种类型
使用VARIANT变量可以实现自动化方法的重载。e.g.

STDMETHODIMP CMath::Add(VARIANT varOp1, VARIANT varOp2, VARIANT *pvarResult)
{
HRESULT hr;
hr = VariantChangeType(&varOp1, &varOp1, 0, VT_l4);
if (FAILED(hr))
return(DISP_E_TYPEMISMATCH);
hr = VariantChangeType(&varOp2, &varOp2, 0, VT_l4);
if (FAILED(hr))
return(DISP_E_TYPEMISMATCH);
if (pvarResult)
{
VariantInit(pvarResult);
pvarResult->vt = VT_l4;
pvarResult->lVal = varOp1.lVal + varOp2.lVal;
}
return S_OK;
}
再举一个不需要转换的例子:

//If we have two strings, append them
if (varOp1.vt == VT_BSTR && varOp2.vt == VT_BSTR)
{
VariantInit(pvarResult);
CComBSTR bstr(varOp1.bstrVal);
bstr.AppendBSTR(varOp2.bstrVal);
//return the concatenated string
pvarResult->vt = VT_BSTR;
pvarResult->bstrVal = bstr.Copy();
return S_OK;
}
(2) SafeArray数据类型
SafeArray最早是一个VB数据类型,后来成为COM中的一员。一个SafeArray可以有多个维,它使用一个引用计数器来实现跨进程的内存管理。使用SafeArray时不能直接访问它的数据成员,应使用COM所提供的响应API函数。 函数 使用目的
SafeArrayCreate 创建一个SafeArray
SafeArrayDestroy 删除一个SafeArray
SafeArrayGetElement 从数组中获取指定的元素(成员)
SafeArrayPutElement 将一个元素加到数组中
SafeArrayGetLBound 获取数组的下界
SafeArrayGetUBound 获取数组的上界
需要注意的是安全数组并不是从0或1开始的,所以下边界和上边界是必须的。使用示例:

STDMETHODIMP CMath::Sum(VARIANT varOp1, long *plResult)
{
//Make sure we have an array of longs
if (!(varOp1.vt & VT_l4))
return DISP_E_TYPEMISMATCH;
if (!(varOp1.vt & VT_l4))
return DISP_E_TYPEMISMATCH;

//The parameter may be a reference
SAFEARRAY *psa;
if (varOp1.vt & VT_BYREF)
psa = *(varOp1.pparray);
else
psa = varOp1.parray;

//Get the lower and upper bounds
long lLBound, lUBound;
SafeArrayGetLBound(psa, 1, &lLBound);
SafeArrayGetUBound(psa, 1, &lUBound);

//Sum the elements of the array
long lSum = 0;
for (long i = lLBound; i &le lUBound; i++)
{
long lValue;
SafeArrayGetElement(psa, &i, lValue);
lSum +=lValue;
}
*plResult = lSum;
return S_OK;
}
而传送SafeArray的代码如下实例:

//Try calling our Sum method
//We first build a safe array whose values are 0, 1, 2, 3, 4
SAFEARRAY *psaARrray = 0;
SAFEARRAYBOUND rgsabound[1];
rgsabound[0].lLbound = 0;
rgsabound[0].cElements = 10;
psaArray = SafeArrayCreate(VT_l4, 1, rgsabound);

//Fill the array with values
for (int i = 0; i < 10; i++)
SafeArrayPutElement(psaArray, (long*)&i, &i);
VARIANT varArray;
(&varArray)->vt = VT_ARRAY | VT_l4;
(&varArray)->pArray = psaArray;

//Call the method
long lResult;
pMath2->Sum(varArray, &lResult);

3. 实现一个派发接口
我们首先讲述一个服务器端的组件,它提供了一个派发接口和双向接口,之后讨论客户机程序中如何访问该方法的功能。如果一个组件通过IDispatch接口向外公开它的功能,则它被看作一个automation component.

// access to the global variables
extern DWORD g_dwObjs;
extern DWORD g_dwLocks;
DEFINE_GUID( CLSID_Math, 0xA988BD40,0x9F1A,0x11CE,0x8B,0x9F,0×10,0×00,0x5A,0xFB,0x7D,0×30);

const DISPID_ADD = 1;
const DISPID_SUBTRACT = 2;
const DISPID_MULTIPLY = 3;
const DISPID_DIVIDE = 4;

class Math : public IDispatch
{
protected:
// Reference count
DWORD m_dwRef;

public:
Math();
~Math();

public:
// IUnknown
STDMETHOD(QueryInterface)( REFIID, void** );
STDMETHOD_(ULONG, AddRef());
STDMETHOD_(ULONG, Release());

// IDispatch
STDMETHOD(GetTypeInfoCount)( UINT* pctinfo );
STDMETHOD(GetTypeInfo)( UINT itinfo, LCID lcid, ITypeInfo** pptinfo );
STDMETHOD(GetIDsOfNames)( REFIID riid, OLECHAR** rgszNames, UINT cNames, LCID lcid, DISPID* rgdispid );
STDMETHOD(Invoke)( DISPID dispid,REFIID riid,LCID lcid,WORD wFlags,DISPPARAMS FAR* pDispParams,
VARIANT FAR* pvarResult,EXCEPINFO FAR* pExcepInfo,unsigned int FAR* puArgErr );

};
对于内部自动化实现,只需要实现GetIDsOfNames和Invoke两个方法即可。

STDMETHODIMP Math::GetIDsOfNames( REFIID riid,
OLECHAR** rgszNames,
UINT cNames,
LCID lcid,
DISPID* rgdispid )
{
// To make things simple, we only support 1 name at a time
if ( cNames > 1 )
return( E_INVALIDARG );

// Convert the member name to ANSI
CHAR szAnsi[128];
long lLen = WideCharToMultiByte( CP_ACP,
0,
rgszNames[0],
wcslen( rgszNames[0] ),
szAnsi,
sizeof( szAnsi ),
0,
0 );
szAnsi[lLen] = ‘\0′;

// Compare the member name to see if it’s one that we have
// and return the correct DISPID
if ( strncmp( “Add”, szAnsi, 3 ) == 0 )
rgdispid[0] = DISPID_ADD;
else if ( strncmp( “Subtract”, szAnsi, 8 ) == 0 )
rgdispid[0] = DISPID_SUBTRACT;
else if ( strncmp( “Multiply”, szAnsi, 8 ) == 0 )
rgdispid[0] = DISPID_MULTIPLY;
else if ( strncmp( “Divide”, szAnsi, 6 ) == 0 )
rgdispid[0] = DISPID_DIVIDE;
else
return( DISPID_UNKNOWN );

return S_OK;
}

STDMETHODIMP Math::Invoke( DISPID dispid,
REFIID riid,
LCID lcid,
WORD wFlags,
DISPPARAMS FAR* pDispParams,
VARIANT FAR* pvarResult,
EXCEPINFO FAR* pExcepInfo,
unsigned int FAR* puArgErr )
{
// All of our methods take two parameters
if ( !pDispParams ||
pDispParams->cArgs != 2 )
return( DISP_E_BADPARAMCOUNT );

// We don’t support named arguments
if ( pDispParams->cNamedArgs > 0 )
return( DISP_E_NONAMEDARGS );

// Break out the parameters and coerce them
// to the proper type
HRESULT hr;
VARIANT varOp1;
VARIANT varOp2;

// Coerce the variant into the desired type
// In this case we would like a long
VariantInit( &varOp1 );
hr = VariantChangeType( &varOp1,
&(pDispParams->rgvarg[1]),
0,
VT_I4 );
// If we can’t get a long return invalidate argument
if ( FAILED( hr ))
return( DISP_E_TYPEMISMATCH );

// Coerce the variant into the desired type
// In this case we would like a long
VariantInit( &varOp2 );
hr = VariantChangeType( &varOp2,
&(pDispParams->rgvarg[0]),
0,
VT_I4 );

// If we can’t get a long return invalidate argument
if ( FAILED( hr ))
return( DISP_E_TYPEMISMATCH );

// Initialize the return value
// If there isn’t one, then just return
if ( pvarResult )
{
VariantInit( pvarResult );
pvarResult->vt = VT_I4;
}
else
return S_OK;

switch( dispid )
{
case DISPID_ADD:
pvarResult->lVal = varOp1.lVal + varOp2.lVal;
return S_OK;

case DISPID_SUBTRACT:
pvarResult->lVal = varOp1.lVal – varOp2.lVal;
return S_OK;

case DISPID_MULTIPLY:
pvarResult->lVal = varOp1.lVal * varOp2.lVal;
return S_OK;

case DISPID_DIVIDE:
pvarResult->lVal = varOp1.lVal / varOp2.lVal;
return S_OK;

default:
return( DISP_E_MEMBERNOTFOUND );
}
}
总而言之,客户机程序必须把所有方法参数(他们存储在VARIANT类型变量中)填充到 DISPPARAMS结构,并且如果方法需要返回值则必须提供pvarResult参数。接下来必须对参数进行检查,以确保参数的内容与你期望的一致。然后把VARIANT类型变量强制转换为long类型。最后把所得到的运算结果封装到方法提供的VARIANT变量中,并把它返回给客户机程序。为了让客户机程序能够查询组件的派发接口(IDispatch)或Vtable(IMath)接口,从而进一步访问组件的功能,需要实现QueryInterface().

STDMETHODIMP Math::QueryInterface( REFIID riid, void** ppv )
{
*ppv = NULL;

if ( riid == IID_IUnknown ||
riid == IID_IDispatch ||
riid == IID_IMath)
*ppv = this;

if ( *ppv )
{
( (LPUNKNOWN)*ppv )->AddRef();
return( S_OK );
}
return E_NOINTERFACE;
}
上述代码对组件实现的三个接口提出的任何请求都由一个Vtable来处理,原因是这三个接口被组织在一个Vtable中了。

Posted in 未分类 | 1 Comment

COM学习笔记(二)

1. ATL和COM数据类型(1) 接口指针
大家已经知道接口指针实际上是指向一个C++抽象类Vtable的指针,从所实现的方法返回一个接口指针 是很有必要的。e.g.

STDMETHODIMP CMath::get_AdvancedMath(IAdvancedMath **ppVal)
{
GetUnknow()->QueryInterface(IID_IAdvancedMath, (void**)ppVal);
return S_OK;
}
上面代码中我们用ATL的GetUnknow函数得到一个IUnknown指针,然后通过该指针来查询IAdvancedMath接口。 QueryInterface将自动增加引用计数。

(2) C++智能指针(Smart pointer)
Smart-pointer指的是在处理指针时隐藏了许多内存管理技术的C++类。ATL中智能指针封装了QueryInterface() / Release() 和CoCreateInstance() / Release()两对方法,因此使用该类的用户不用担心COM指针是否被释放。ATL提供两类智能 指针类:CComPtr和CComQIPtr.

(3) CComPtr
ATL的CComPtr模板类提供了基本的Smart Pointer功能,可以把CComPtr类当作一个COM接口指针来使用。e.g.

CComPtr ptrMath;
HRESULT hr;
hr = CoCreateInstance(CLSID_Math, NULL, CLSCTX_LOCAL_SERVER,
IID_IMath, (void**) &ptrMath);
long ptrMath->Add(1234, 5678, &lResult);
在上面例子中我们没有调用Release,但也可以通过调用CComPtr::Release明确释放类所占用的系统资源。另外释放类 所占用的资源也可以使用赋值操作符类实现:

ptrMath.Release();//or ptrMath = 0;
要注意的是CComPtr直接想外公开了一个Release方法,因此你可能错误地两次释放接口资源,因为当类的实例超出它的 范围时还会执行一次释放资源的操作。

(4) CComQIPtr
使用COM时经常需要用一个接口指针调用QueryInterface来得到另外一个接口,ATL的CComQIPtr智能指针类被实例化时 将自动调用QueryInterface, 只需要所请求的接口的IID。e.g.

CComQIptr ptrAdvancedMath(ptrMath);
if (ptrAdvancedMath)
ptrAdvancedMath->Factorial(12, &lResult);

(5) BSTR
BSTR被声明为OLECHAR*,说明它是一个Unicode字符串。API函数SysAllocString将在一个Unicode字符串前创建一个DWORD值, 并用来描述字符串的长度。之后使用SysFreeString释放所占用的内存空间,这是COM内存管理的一种规定。我们接下来 详细讨论。

2. COM的内存管理
♦ 对于只带in属性的参数,调用者负责分配和释放这些参数所需要的内存;
♦ 对于只带out属性的参数,服务器负责分配内存,调用者(客户机)负责释放;

♦ 对于具备in和out属性的参数,调用者分配内存并负责释放参数所占用的内存,但服务器可以选择重新分配内存。

Posted in 未分类 | 2 Comments

COM学习笔记(一)

1. 接口COM的主要工作是处理接口(Interface)的实现和使用上的问题。下面的C++类 声明实例展示了一个具有四种方法的的公共接口:

class Math
{
public:
long Add(long, long);
long Substract(long, long);
long Multiply(long, long);
long Divide(long, long);
};
COM接口同上述C++类的公共接口相似,不同点是COM采用了一种与语言无关、位置透明的方式。
2. 利用Vtable实现COM接口C++使用Vtable来实现多态功能,而COM则使用C++的Vtable来建立一个COM 接口。一个COM接口实际就是一个指向Vtable结构的指针。首先建立Vtable:

class IMath
{
public:
virtual long Add(long, long) = 0;
virtual long Substract(long, long) = 0;
virtual long Multiply(long, long) = 0;
virtual long Divide(long, long) = 0;
};
接着再从该抽象类中派生出子类并提供各自的实现,这一步思路很简明。

class Math : public IMath
{
public:
long Add(long, long);
long Substract(long, long);
long Multiply(long, long);
long Divide(long, long);
};
COM只能通过一个Vtable指针来提供对它的组件的访问,所以对“组件实现”的访问是无法完成的。 COM的核心概念就是通过使用Vtable来为组件的功能提供接口,最后一个COM接口只不过是一个指向 Vtable(一个具有C++风格的接口)的指针的指针。
3. 对COM接口的访问了解了如何描述一个COM接口,下面看一下客户应用程序怎样访问并使用该接口。第一种方法就是 使用CoCreateInstance. 这个函数是客户应用程序在创建组件实例时使用的:

//Create an instance and return the IMath interface
IMath *pMath;
HRESULT hr = CoCreateInstance(CLSID_Math, NULL, CLSCTX_INPROC, IID_IMath, (void**)&pMath);
//Use IMath
long lResult = pMath->Multiply(44, 33);
首先声明一个指向抽象类IMath的指针,然后要求COM创建一个组件的实例,该最贱由类标识符 CLSID_Math标识,之后返回了一个指向Vtable结构的指针的指针,有了这个指针就可以访问IMath 接口显示出来的所有函数。
访问组件接口的另一种技术能够满足对多个接口的组件访问某一特定接口,见下部分说明。

4. 组件的多接口特性假设我们想要在IMath中添加3种高级方法:阶乘、Fibonacci数列、Draw。总共的7中接口方法是有 一定分类的:前四种简单运算,后两种高级运算,Draw提供可视化。COM提供了把一个组件的功能 分割到多个接口的能力,每一个接口都把一个小的、准确定义的功能集展示出来,然后使用组件 的客户可以直接与所需要的功能块打交道。

class IMath
{
public:
virtual long Add(long, long) = 0;
virtual long Substract(long, long) = 0;
virtual long Multiply(long, long) = 0;
virtual long Divide(long, long) = 0;
};
class IAdvancedMath
{
public:
virtual long Factorial(short sOp) = 0;
virtual long Fibonacci(short sOp) = 0;
};
class IDraw
{
public:
virtual void Draw() = 0;
};

class Math : public IMath, public IAdvancedMath, public IDraw
{
public:
//Implementation of each interface here
};
现在你的C++类拥有了3个不同的Vtable,也就是说Math现在包含了3个基于COM的接口。如果 一个客户仅仅想要画出Math组件,那么只需要访问并理解组件的IDraw接口就够了。而COM则通过 最重要的接口IUnknow,完成了指定接口的机制。
(1). 标准COM接口
上边的Math组件还不是实际意义上的COM组件,所有的COM组件都需要实现一个被称为IUnknow的标准 COM接口。IUnknow定义如下:

class IUnknow
{
virtual HRESULT QueryInterface(REFIID riid, void **ppv) = 0;
virtual ULONG AddRef() = 0;
virtual ULONG Release() = 0;
};
其中QueryInterface为一个组件客户提供了一种标准途径,使客户可以在指定的组件里要求使用一个 特定的接口。第一个参数rrid是一个对特定接口ID的引用,第二个参数void **是接口指针返回的位置。 AddRef和Release则在组件实例里进行生存期方面的管理。
(2). IUnknow的实现
COM需要IUnknow接口在任何COM对象中出现,并且所有的COM接口都包含IUnknow接口。

class IMath : public IUnknow {/*…同上*/};
class IAdvancedMath : public IUnknow {/*…同上*/};
class Math : public IMath, public IAdvancedMath
{
//We also have to implement IUnknown’s methods:
public:
HRESULT QueryInterface(REFIID riid, void **ppv);
ULONG AddRef();
ULONG Release();
//Implement IMath interface:
/*同上*/
//Implement IAdvancedMath interface:
/*同上*/
};
每个COM接口都需要一个IUnknow的实现,但因为C++具有多继承方式的性质,所以只需要实现一次 IUnknow方法就可以了。下面的代码是客户通过COM接口进行实例化并访问组件功能的例子:创建组件, 要求使用它的IUnknow接口,调用成功后对IUnknow指针进行查询,寻找IMath接口,并随后使用它的功能。

//Create an instance and retrivev its IUnknow interface
IUnknown *pUnk;
HRESULT hr = CoCreateInstance(CLSID_Math, NULL, CLSCTX_INPROC, IID_IUnknow, (void**)&pUnk);
//Query for IMath
IMath *pMath;
pUnk->QueryInterface(IID_IMath, (void**)&pMath);
//Use IMath
long lResult = pMath->Multiply(44, 33);

5. 其他涉及COM的问题(1). C++里关于COM的宏:STDMETHOD和STDMETHODIMP
COM和ATL广泛使用C/C++宏来隐藏在不同平台下的实现细节。COM接口的声明和定义中可以使用四种宏: STDMETHOD, STDMETHODIMP, STDMETHOD_, STDMETHODIMP_。前两个宏表示从某一方法中返回的HRESULT, 后两个只和IUnknown的AddRef和Release一起使用,表示一个由用户指定的返回值。要知道除了AddRef和 Release以外,每一个COM的接口方法都应该返回一个HRESULT。但是现在IMath接口中的方法返回了long 数值,这样就必须把计算结果从返回值转变为指针传递型参数。这一步是关键而且必须的,因为必须 返回一个HRESULT.
IMath接口因此需要按照如下方法声明,使用COM的宏并像指针一样把返回值移到尾部:

class IMath : public IUnknown
{
public:
STDMETHOD(Add) (long, long, long*) PURE;
STDMETHOD(Subtract) (long, long, long*) PURE;
STDMETHOD(Multiply) (long, long, long*) PURE;
STDMETHOD(Divide) (long, long, long*) PURE;
};
STDMETHOD的实际展开和目标平台的类型以及使用的语言有关,Win32下C++的展开如下:

//OBJBASE.H
#define STDMETHODCALLTYPE __stdcall
#define STDMETHOD(method) virtual HRESULT STDMETHODCALLTYPE method
#define PURE = 0
#define STDMETHODIMP HRESULT STDMETHODCALLTYPE
下面是Math类里IMath方法的声明与实现

class Math : public IMath, public IAdvancedMath
{
public:
STDMETHOD(Add) (long, long, long*);
STDMETHOD(Subtract) (long, long, long*);
STDMETHOD(Multiply) (long, long, long*);
STDMETHOD(Divide) (long, long, long*);
};
STDMETHODIMP Math::Add(long lOp1, long lOp2, long* pResult)
{
*pResult = lOp1 + lOp2;
return S_OK;
}
(2). COM和Unicode

Posted in 未分类 | Leave a comment