1. C程序調用時,調用者必須預先知道接口規范(如,參數類型、參數字節長度、參數順序等)。由於不同語言這些規范有所不同,COM未解決不同語言之間調用,提供了IDispatch接口。
2. IDispatch要求其實例必須自我描述,即拿到一個對象后,可從對象中直接獲取調用方式,而無須預先明確。
3. IDispatch中通過VT_TYPE來指定相關類型,如 VT_I4為4字節整形、VT_BSTR為unicode字符串,VT_DISPATCH表示是一個IDispatch對象
4. 給對象中每一屬性或函數(Method)分配一個整形Id和一個字符串name,調用者可以通過name字符串確定如何調用。如,若name為"length"的屬性,調用者就理解為長度。由於這里通常是根據name來理解相應屬性,因此name描述應足夠准確。如,以"length()"為名稱的函數實現整數相加功能就是不恰當的。
5. 使用IDispatch對象時,首相調用 IDispatch::GetIDsOfNames()將屬性、函數名稱作為參數,獲取對應的屬性、函數id。
6. 再調用IDispatch::Invoke() 將id作為參數,實際調用功能。
7. 若為獲取屬性值,則 Invoke()調用時,傳入 Dispatch_PropertyGet標志。
8. 若為設置屬性值,則Invoke()調用時,傳入 Dispatch_PropertyPut標志。並在 DispParams參數中指定修該屬性改為何值。DispParams結構說明見后。
9. 若為調用函數,則 Invoke()調用時,傳入 Dispatch_Method標志。若該Method需要參數,則通過IDispatch::Invoke()的DispParams參數指定。
10. DispParams結構使用舉例:
DISPPARAMS dispparams;
dispparams.rgdispidNamedArgs = &dispidOfNamedArgs;
dispparams.cArgs = 1;
dispparams.cNamedArgs = 1;
dispparams.rgvarg = new VARIANTARG[1];
dispparams.rgvarg[0].vt = VT_I4;
dispparams.rgvarg[0].intVal = 123;
a. 上面代碼是一個用於給Method傳參的簡單例子,先創建一個DispParams對象
b. cArgs指定Method中的參數個數。
c. cNamedArgs指定Method中已經命名的參數個數。(命名參數是對應無名參數的概念。有些語言可定義不定參數,此時IDispatch的描述中不會給參數分配名稱,而是調用時以無名參數存在。如,JS中 Array對象的push()方法,可支持不定個數的參數)
d. rgvarg 為實際參數數組,每一元素表示一個參數,其中.vt表明此元素的數據類型,intVal項是一個C++聯合結構,如vt == VT_I4時,應以intVal = xxx方式賦值;若 vt == VT_BSTR,則應以 bstrVal = xxx方式賦值
11. 舉例:兩個參數,都是無名稱參數,第一個為整形,第二個為BSTR型
DISPPARAMS dispparams;
dispparams.rgdispidNamedArgs = NULL;
dispparams.cArgs = 2;
dispparams.cNamedArgs = 0;
dispparams.rgvarg = new VARIANTARG[2]; // 2個參數,分配2個空間
dispparams.rgvarg[0].vt = VT_I4; // 整形
dispparams.rgvarg[0].intVal = 123;
dispparams.rgvarg[1].vt = VT_BSTR; // 字符串型
dispparams.rgvarg[1].bstrVal = L"abcd";
IDispatch接口是COM自動化的核心。其實,IDispatch這個接口本身也很簡單,只有4個方法:
- IDispatch : public IUnknown
- {
- public:
- virtual HRESULT STDMETHODCALLTYPE GetTypeInfoCount(
- /* [out] */ __RPC__out UINT *pctinfo) = 0;
- virtual HRESULT STDMETHODCALLTYPE GetTypeInfo(
- /* [in] */ UINT iTInfo,
- /* [in] */ LCID lcid,
- /* [out] */ __RPC__deref_out_opt ITypeInfo **ppTInfo) = 0;
- virtual HRESULT STDMETHODCALLTYPE GetIDsOfNames(
- /* [in] */ __RPC__in REFIID riid,
- /* [size_is][in] */ __RPC__in_ecount_full(cNames) LPOLESTR *rgszNames,
- /* [range][in] */ __RPC__in_range(0,16384) UINT cNames,
- /* [in] */ LCID lcid,
- /* [size_is][out] */ __RPC__out_ecount_full(cNames) DISPID *rgDispId) = 0;
- virtual /* [local] */ HRESULT STDMETHODCALLTYPE Invoke(
- /* [annotation][in] */
- _In_ DISPID dispIdMember,
- /* [annotation][in] */
- _In_ REFIID riid,
- /* [annotation][in] */
- _In_ LCID lcid,
- /* [annotation][in] */
- _In_ WORD wFlags,
- /* [annotation][out][in] */
- _In_ DISPPARAMS *pDispParams,
- /* [annotation][out] */
- _Out_opt_ VARIANT *pVarResult,
- /* [annotation][out] */
- _Out_opt_ EXCEPINFO *pExcepInfo,
- /* [annotation][out] */
- _Out_opt_ UINT *puArgErr) = 0;
- };
GetTypeInfoCount和GetTypeInfo以后再說。
先來看看比較熟悉的GetIDsOfNames和Invoke。
GetIDsOfNames
這個函數的主要功能就是:把COM接口的方法名字和參數(可選)映射成一組DISPID。DISPID就是一個LONG型:
- typedef LONG DISPID;
GetIDsOfNames()可以獲取方法和屬性。先來看一個例子,COM接口IMyCar
- [
- object,
- uuid(21B794E2-4857-4576-8FC2-CDAB2A486600),
- dual,
- nonextensible,
- pointer_default(unique)
- ]
- interface IMyCar : IDispatch{
- [id(1)] HRESULT Run();
- [id(2)] HRESULT AddGas([in] LONG add, [out] LONG* total);
- [propget, id(3)] HRESULT Gas([out, retval] LONG* pVal);
- };
這個接口里面有一個方法AddGas,它有兩個參數,一個輸入,一個輸出。它的實現基本如下:
- STDMETHODIMP CMyCar::AddGas(LONG add, LONG* total)
- {
- // TODO: Add your implementation code here
- m_Gas += add;
- *total = m_Gas;
- return S_OK;
- }
試試如何獲取AddGas函數的id和參數index,看下面的代碼。數組里面的第一個元素是方法名字,第二個/第三個是參數名字。
- CComPtr<IMyCar> spCar;
- spCar.CoCreateInstance(CLSID_MyCar);
- DISPID PropertyID[3] = {0};
- BSTR PropName[3];
- PropName[0] = SysAllocString(L"AddGas");
- PropName[1] = SysAllocString(L"add");
- PropName[2] = SysAllocString(L"total");
- HRESULT hr = spCar->GetIDsOfNames(IID_NULL, PropName, 3, LOCALE_SYSTEM_DEFAULT, PropertyID);
- SysFreeString(PropName[0]);
- SysFreeString(PropName[1]);
- SysFreeString(PropName[2]);
運行一下,可以得到如下結果:
PropertyID數組里面可以得到3個值:2, 0, 1.
2代表的是AddGas的id,跟idl文件里面的一樣。
0表示"add"是第一個參數,1表示"total"是第二個參數。
Invoke
Invoke是IDispatch里面非常重要的一樣函數,方法調用就靠這個函數了。函數原型:
- HRESULT Invoke(
- [in] DISPID dispIdMember,
- [in] REFIID riid,
- [in] LCID lcid,
- [in] WORD wFlags,
- [in, out] DISPPARAMS *pDispParams,
- [out] VARIANT *pVarResult,
- [out] EXCEPINFO *pExcepInfo,
- [out] UINT *puArgErr
- );
每一個參數的說明,看下面,從MSDN截來的。
先來看一個調用例子:
- CComVariant avarParams[2];
- avarParams[1].vt = VT_I4;
- avarParams[1] = 4;
- LONG vTotal = 0;
- avarParams[0].vt = VT_I4 | VT_BYREF;
- avarParams[0] = &vTotal;
- DISPPARAMS params = { avarParams,
- NULL, // Dispatch identifiers of named arguments.
- 2, // Number of arguments.
- 0 }; // Number of named arguments.
- hr = spCar->Invoke(PropertyID[0], IID_NULL, LOCALE_USER_DEFAULT, DISPATCH_METHOD, ¶ms, NULL, NULL, NULL);
avarParams是一個具有2個元素的數組,它表示要調用的方法的參數,注意這里的參數是逆序的。比如avarParam[1]存放的是第一個參數,是一個輸入參數;avarParams[0]存放的是第二個參數,是一個輸出參數。
運行一下:
spCar里面的m_Gas初始值是0,我們調用的時候給它加了4,那么輸出就應該是4.看上面的運行結果,vTotal確實是4.
這樣我們就通過Invoke成功調用了COM組件的方法(而不是通過spCar->AddGas)。注意Invoke里面的第一個參數是一個DISPID,這個是從GetIDsOfNames來的。
使用Invoke也可以調用COM對象的屬性。
比如上面的idl里面的Gas。
調用屬性跟方法差不多,如果我們想讀取一個屬性,那么可以這么調:
- DISPID PropertyID2[1] = { 0 };
- BSTR PropName2[1];
- PropName2[0] = SysAllocString(L"Gas");
- hr = spCar->GetIDsOfNames(IID_NULL, PropName2, 1, LOCALE_SYSTEM_DEFAULT, PropertyID2);
- SysFreeString(PropName2[0]);
- DISPPARAMS params2 = { NULL,
- NULL,
- 0,
- 0
- };
- CComVariant Result;
- hr = spCar->Invoke(PropertyID2[0], IID_NULL, LOCALE_USER_DEFAULT, DISPATCH_PROPERTYGET, ¶ms2, &Result, NULL, NULL);
運行可以得到結果:
注意,Invoke的第五個參數啥都沒,屬性值是從第六個參數返回出來的。如果是方法調用的話,那么COM方法參數需要從第五個參數傳進入,第六個參數是函數調用的返回值HRESULT.
我沒有試過屬性的put,不知道是從哪里傳入,當有需要的時候查一下MSDN就行了。
以上就是GetIDsOfNames和Invoke的簡要說明以及例子。之后再來分析更多的細節。
完整客戶端代碼:
- // ConsoleApplication4.cpp : Defines the entry point for the console application.
- //
- #include "stdafx.h"
- #include <thread>
- #include <atlbase.h>
- #include <atlcom.h>
- #include <algorithm>
- #include <vector>
- #include <memory>
- #include "../MyCom/MyCom_i.h"
- #include "../MyCom/MyCom_i.c"
- int _tmain(int argc, _TCHAR* argv[])
- {
- CoInitializeEx(NULL, COINIT_MULTITHREADED);
- CComPtr<IMyCar> spCar;
- spCar.CoCreateInstance(CLSID_MyCar);
- // use IDispatch
- DISPID PropertyID[3] = {0};
- BSTR PropName[3];
- PropName[0] = SysAllocString(L"AddGas");
- PropName[1] = SysAllocString(L"add");
- PropName[2] = SysAllocString(L"total");
- HRESULT hr = spCar->GetIDsOfNames(IID_NULL, PropName, 3, LOCALE_SYSTEM_DEFAULT, PropertyID);
- SysFreeString(PropName[0]);
- SysFreeString(PropName[1]);
- SysFreeString(PropName[2]);
- CComVariant avarParams[2];
- avarParams[1].vt = VT_I4;
- avarParams[1] = 4;
- LONG vTotal = 0;
- avarParams[0].vt = VT_I4 | VT_BYREF;
- avarParams[0] = &vTotal;
- DISPPARAMS params = { avarParams,
- NULL, // Dispatch identifiers of named arguments.
- 2, // Number of arguments.
- 0 }; // Number of named arguments.
- hr = spCar->Invoke(PropertyID[0], IID_NULL, LOCALE_USER_DEFAULT, DISPATCH_METHOD, ¶ms, NULL, NULL, NULL);
- DISPID PropertyID2[1] = { 0 };
- BSTR PropName2[1];
- PropName2[0] = SysAllocString(L"Gas");
- hr = spCar->GetIDsOfNames(IID_NULL, PropName2, 1, LOCALE_SYSTEM_DEFAULT, PropertyID2);
- SysFreeString(PropName2[0]);
- DISPPARAMS params2 = { NULL,
- NULL,
- 0,
- 0
- };
- CComVariant Result;
- hr = spCar->Invoke(PropertyID2[0], IID_NULL, LOCALE_USER_DEFAULT, DISPATCH_PROPERTYGET, ¶ms2, &Result, NULL, NULL);
- spCar.Release();
- CoUninitialize();
- return 0;
- }
相關的COM組件的代碼:
- STDMETHODIMP CMyCar::AddGas(LONG add, LONG* total)
- {
- // TODO: Add your implementation code here
- m_Gas += add;
- *total = m_Gas;
- return S_OK;
- }
- STDMETHODIMP CMyCar::get_Gas(LONG* pVal)
- {
- // TODO: Add your implementation code here
- *pVal = m_Gas;
- return S_OK;
- }