错误修正
首先修正一下上篇(《》)中的例子的一个小问题。类厂的CreateInstance里面,上次是这么写的:
STDMETHODIMPClassFactory::CreateInstance(_In_opt_IUnknown*pUnkOuter,
_In_REFIIDriid,
_COM_Outptr_void**ppvObject)
{
if(riid==__uuidof(ISampleInterface) &&m_clsid ==__uuidof(SampleClass))
{
ISampleInterface*p =newSampleClass;
p->QueryInterface(riid,ppvObject);
returnS_OK;
}
returnCLASS_E_CLASSNOTAVAILABLE;
}
其中一开始就检查了IID,如果不是ISampleInterface,就返回错误,错误信息是“类无效”(应该是“接口不存在”),这不科学。后面p->QueryInterface的时候,还会对IID做一次检查,因此前面的IID检查可以去掉。实际上,有些使用者在获取类厂后,会来个CreateInstance(..., IID_IUnknown, ...),这是个合理的行为,应该予以支持,而像上面这样写就不支持了。纠正为:
STDMETHODIMPClassFactory::CreateInstance(_In_opt_IUnknown*pUnkOuter,
_In_REFIIDriid,
_COM_Outptr_void**ppvObject)
{
if(m_clsid ==__uuidof(SampleClass))
{
ISampleInterface*p =newSampleClass;
p->QueryInterface(riid,ppvObject);
returnS_OK;
}
returnCLASS_E_CLASSNOTAVAILABLE;
}
同理,DllGetClassObject中,原先是:
STDAPIDllGetClassObject(_In_REFCLSIDrclsid,_In_REFIIDriid,_Outptr_LPVOID*ppv)
{
if(riid==__uuidof(IClassFactory) &&rclsid==__uuidof(SampleClass))
{
IClassFactory*p =newClassFactory(rclsid);
p->QueryInterface(riid,ppv);
returnS_OK;
}
returnCLASS_E_CLASSNOTAVAILABLE;
}
做了IID和CLSID的双重检查。而IID刚才说过了,具体类的QueryInterface会检查;CLSID,类厂的CreateInstance会检查,因此这里大可不必检查。改为:
STDAPIDllGetClassObject(_In_REFCLSIDrclsid,_In_REFIIDriid,_Outptr_LPVOID*ppv)
{
IClassFactory*p =newClassFactory(rclsid);
returnp->QueryInterface(riid,ppv);
}
引言
好了,回到主题。看过COM介绍的,一般都会听说,哦,可以跨语言调用,真牛逼!好吧,现在就来调调看。写段VBScript:
Setobj = WScript.CreateObject("Streamlet.COMProvider.SampleClass.1")
obj.SampleMethod
使用CScript调用这个脚本,报错:
Test.vbs(1, 1) Microsoft VBScript运行时错误:类不能支持Automation操作
不支持……需要组件支持“自动化”操作。这是脚本。
还有非脚本的,比如古老的VB6,来试试看。加入外部引用:
这里显示的都是类型库,而我们根本没有注册类型库……
类型库
类型库信息一般位于PE文件的资源段,如下图的TypeLib资源:
类型库也可以单独存在于一个扩展名为.tlb的文件里。
本来一直想避开IDL写COM,可是产生TLB的工作做不了,于是只好借助IDL编译器来产生TLB了。于是把工程改造一下。
原先有个Interface.h,内容为:
#include
struct__declspec(uuid("{83C783E3-F989-4E0D-BFC5-631273EDFFDA}"))
ISampleInterface:publicIUnknown
{
STDMETHOD(SampleMethod)()PURE;
};
class__declspec(uuid("{0DECBFF5-A8A5-49E8-9962-3D18AAC6088E}"))
SampleClass;
本来用于定义接口,提供给使用者用。现在不需要了。添加一个COMProvider.idl,内容为:
import"oaidl.idl";
import"ocidl.idl";
[
object,
uuid(83C783E3-F989-4E0D-BFC5-631273EDFFDA),
]
interfaceISampleInterface : IUnknown
{
[id(1)] HRESULT SampleMethod();
};
[
uuid(22935FC2-282E-4727-B40F-E55128EA1072),
version(1.0),
]
libraryCOMProviderLib
{
importlib("stdole2.tlb");
[
uuid(0DECBFF5-A8A5-49E8-9962-3D18AAC6088E)
]
coclassSampleClass
{
[default]interfaceISampleInterface;
};
};
import"shobjidl.idl";
后面library这一段便定义了类型库,类型库ID为{22935FC2-282E-4727-B40F-E55128EA1072}。单独编译这个文件,会在源代码目录产生四个文件:
同时还会在$(IntDir)(通常为$(ProjectDir)\$(Configuration),即源代码目录下的Debug或Release目录)产生一个COMProvider.tlb。
COMProvider_h.h中包含了原先Interface.h中得所有信息。现在可以把原先#inlcude “Interface.h”的地方都改成“COMProvider_h.h”了。
然后添加一个空的资源脚本文件(*.rc),查看代码:
找到3 TEXTINCLUDE(第一个红框),改成:
3 TEXTINCLUDE
BEGIN
"1 TYPELIB ""COMProvider.tlb""\r\n"
"\0"
END
再找到第二个红框,改成:
#ifndefAPSTUDIO_INVOKED
/
//
// Generated from the TEXTINCLUDE 3 resource.
//
1 TYPELIB"COMProvider.tlb"
/
#endif// not APSTUDIO_INVOKED
再改下项目属性中的资源的Include目录:
加入$(IntDir):
因为编译IDL后,TLB文件是产生在$(IntDir)的。
现在重新编译,用资源工具(比如resource hacker)查看,发现资源里有了TypeLib信息了。
然后改下DllRegisterServer中得代码,加几行注册TypeLib的:
STDAPIDllRegisterServer(void)
{
TCHARszModulePath[MAX_PATH] = {};
GetModuleFileName(g_hModule, szModulePath,ARRAYSIZE(szModulePath));
xl::Registry::SetString(HKEY_CLASSES_ROOT,
_T("CLSID\\{0DECBFF5-A8A5-49E8-9962-3D18AAC6088E}\\InprocServer32"),
_T(""),
szModulePath);
xl::Registry::SetString(HKEY_CLASSES_ROOT,
_T("CLSID\\{0DECBFF5-A8A5-49E8-9962-3D18AAC6088E}\\ProgID"),
_T(""),
_T("Streamlet.COMProvider.SampleClass.1"));
xl::Registry::SetString(HKEY_CLASSES_ROOT,
_T("Streamlet.COMProvider.SampleClass.1"),
_T(""),
_T("SampleClass Class"));
xl::Registry::SetString(HKEY_CLASSES_ROOT,
_T("Streamlet.COMProvider.SampleClass.1\\CLSID"),
_T(""),
_T("{0DECBFF5-A8A5-49E8-9962-3D18AAC6088E}"));
xl::Registry::SetString(HKEY_CLASSES_ROOT,
_T("CLSID\\{0DECBFF5-A8A5-49E8-9962-3D18AAC6088E}\\TypeLib"),
_T(""),
_T("{22935FC2-282E-4727-B40F-E55128EA1072}"));
xl::Registry::SetString(HKEY_CLASSES_ROOT,
_T("TypeLib\\{22935FC2-282E-4727-B40F-E55128EA1072}\\1.0"),
_T(""),
_T("COMProvider Type Library"));
xl::Registry::SetString(HKEY_CLASSES_ROOT,
_T("TypeLib\\{22935FC2-282E-4727-B40F-E55128EA1072}\\1.0\\0\\Win32"),
_T(""),
szModulePath);
returnS_OK;
}
好了,改造完成,重新编译吧。
在VB6中调用COM组件
新建一个VB6项目,选择菜单“工程”|“引用...”:
看到一个可用的引用列表:
我们的DLL已经赫然在目了!勾上它!
然后在默认窗口上放个按钮加几行代码:
PrivateSubCommand1_Click()
DimobjAsCOMProviderLib.SampleClass
obj = CreateObject("Streamlet.COMProvider.SampleClass.1")
obj.SampleMethod()
EndSub
运行结果如下:
运行成功。
可见,VB6调用COM对象,只需要COM对象带上类型库就可以了,可以不需要自动化。但是WSH环境下的VBScript仍然需要自动化支持。
自动化
“自动化”这个名字起得很好听,但实际上,只是“要求接口支持IDispatch”这么个含义。下面我们来支持自动化。
IDL
首先修改IDL,添加属性“dual”(实际上不加dual也可以通过下文所有测试,这是为啥?有木有达人来解释下?),以及把基类修改为IDispatch:
import"oaidl.idl";
import"ocidl.idl";
[
object,
uuid(83C783E3-F989-4E0D-BFC5-631273EDFFDA),
dual
]
interfaceISampleInterface :IDispatch
{
[id(1)] HRESULT SampleMethod();
};
[
uuid(22935FC2-282E-4727-B40F-E55128EA1072),
version(1.0),
]
libraryCOMProviderLib
{
importlib("stdole2.tlb");
[
uuid(0DECBFF5-A8A5-49E8-9962-3D18AAC6088E)
]
coclassSampleClass
{
[default]interfaceISampleInterface;
};
};
import"shobjidl.idl";
改完后最好先单独编译一下IDL,以便IDE能认识新的符号。然后修改SampleClass.h,把基类修改为IDispatch,暴露IDispatch,然后添加IDispatch的四个方法:
classSampleClass:publicxl::ComClass,
publicxl::IDispatchImpl
{
public:
SampleClass();
~SampleClass();
public:
STDMETHOD(SampleMethod)();
public:// IDispatch Methods
STDMETHOD(GetTypeInfoCount)(UINT*pctinfo);
STDMETHOD(GetTypeInfo)(UINTiTInfo,LCIDlcid,ITypeInfo**ppTInfo);
STDMETHOD(GetIDsOfNames)(REFIIDriid,LPOLESTR*rgszNames,UINTcNames,LCIDlcid,DISPID*rgDispId);
STDMETHOD(Invoke)(DISPIDdispIdMember,
REFIIDriid,
LCIDlcid,
WORDwFlags,
DISPPARAMS*pDispParams,
VARIANT*pVarResult,
EXCEPINFO*pExcepInfo,
UINT*puArgErr);
public:
XL_COM_INTERFACE_BEGIN(SampleClass)
XL_COM_INTERFACE(ISampleInterface)
XL_COM_INTERFACE(IDispatch)
XL_COM_INTERFACE_END()
};
接下来我们逐一实现这四个方法。等等……先把IDispatch的四个方法声明去掉,使用IDispatchImpl中默认的,看看会是什么结果:
Test.vbs(1, 1) Microsoft VBScript运行时错误:对象不支持此操作: 'obj.SampleMethod'
嗯,错误提示变了。好吧,继续老老实实把这四个方法都写了。
GetTypeInfoCount
首先是GetTypeInfoCount,如果有类型信息(可使用GetTypeInfo获取),就从输出参数会返回1,否则返回0。实现如下:
STDMETHODIMPSampleClass::GetTypeInfoCount(UINT*pctinfo)
{
if(pctinfo==nullptr)
{
returnE_INVALIDARG;
}
*pctinfo= 1;
returnS_OK;
}
GetTypeInfo
第二个函数,GetTypeInfo,顾名思义,取类型信息。我们知道,类型信息源在IDL文件,IDL编译产生了二进制的TLB,TLB被我们放在了DLL资源里。遗憾的是,IDL编译器并不产生生成TypeInfo的源代码,我们也无心手工构造一个TypeInfo对象,所以只好从资源里读取TLB,然后解析TLB得到ITypeInfo。这个过程分两步,第一步是使用LoadTypeLib得到一个TypeLib对象,第二步是使用ITypeLib的GetTypeInfoOfGuid方法取得某个接口的TypeInfo。因为GetTypeInfo可能会被多次调用,所以不适合每次都跑整个过程。第一步是针对整个模块的,可以在模块加载的时候做;第二步是针对某个接口的,可以在COM类构造的时候做。
DllMain附近改动如下:
#include
#include
HMODULEg_hModule =nullptr;
ITypeLib*g_pTypeLib =nullptr;
voidGetTypeInfo()
{
TCHARszModulePath[MAX_PATH] = {};
GetModuleFileName(g_hModule, szModulePath,ARRAYSIZE(szModulePath));
LoadTypeLib(szModulePath, &g_pTypeLib);
}
voidReleaseTypeInfo()
{
if(g_pTypeLib !=nullptr)
{
g_pTypeLib->Release();
g_pTypeLib =nullptr;
}
}
BOOLAPIENTRYDllMain(HMODULEhModule,DWORDul_reason_for_call,LPVOIDlpReserved)
{
switch(ul_reason_for_call)
{
caseDLL_PROCESS_ATTACH:
g_hModule =hModule;
GetTypeInfo();
break;
caseDLL_THREAD_ATTACH:
break;
caseDLL_THREAD_DETACH:
break;
caseDLL_PROCESS_DETACH:
ReleaseTypeInfo();
break;
default:
break;
}
returnTRUE;
}
然后SampleClass中加个成员变量ITypeInfo*m_pTypeInfo,构造函数和析构函数改为:
SampleClass::SampleClass() : m_pTypeInfo(nullptr)
{
InterlockedIncrement(&g_nModuleCount);
if(g_pTypeLib !=nullptr)
{
g_pTypeLib->GetTypeInfoOfGuid(__uuidof(ISampleInterface), &m_pTypeInfo);
}
}
SampleClass::~SampleClass()
{
if(m_pTypeInfo !=nullptr)
{
m_pTypeInfo->Release();
m_pTypeInfo =nullptr;
}
InterlockedDecrement(&g_nModuleCount);
}
最后,GetTypeInfo实现为:
STDMETHODIMPSampleClass::GetTypeInfo(UINTiTInfo,LCIDlcid,ITypeInfo**ppTInfo)
{
if(m_pTypeInfo ==nullptr)
{
returnE_FAIL;
}
if(iTInfo!= 0)
{
returnDISP_E_BADINDEX;
}
returnm_pTypeInfo->QueryInterface(__uuidof(ITypeInfo), (LPVOID*)ppTInfo);
}
GetIDsOfNames
有个API可以帮我们搞定这件事,DispGetIDsOfNames,需要传入一个ITypeInfo *,而我们刚才已经获取了ITypeInfo *并保存在成员中了,恰好用上:
STDMETHODIMPSampleClass::GetIDsOfNames(REFIIDriid,LPOLESTR*rgszNames,UINTcNames,LCIDlcid,DISPID*rgDispId)
{
if(riid!=IID_NULL)
{
returnE_INVALIDARG;
}
returnDispGetIDsOfNames(m_pTypeInfo,rgszNames,cNames,rgDispId);
}
Invoke
也有个API,DispInvoke,传入ITypeInfo *外,还要传入一个IDispatch实现类的指针,这个指针当然是SampleClass自己咯。
STDMETHODIMPSampleClass::Invoke(DISPIDdispIdMember,
REFIIDriid,
LCIDlcid,
WORDwFlags,
DISPPARAMS*pDispParams,
VARIANT*pVarResult,
EXCEPINFO*pExcepInfo,
UINT*puArgErr)
{
if(riid!=IID_NULL)
{
returnE_INVALIDARG;
}
returnDispInvoke(this, m_pTypeInfo,dispIdMember,wFlags,pDispParams,pVarResult,pExcepInfo,puArgErr);
}
至此,自动化工作全部完成。
脚本调用
WSH
先测试一下最初的VBScript调用COM的情况,运行结果如下图:
VB6
还是VB6,不过这次通过自动化接口调用。去掉对COMProvider的引用,然后改写代码:
PrivateSubCommand1_Click()
DimobjAsObject
Set obj = CreateObject("Streamlet.COMProvider.SampleClass.1")
obj.SampleMethod
EndSub
同样能够运行:
要测试确实是通过自动化接口运行的而不是通过虚函数表运行的,可以把暴露IDispatch那一行注释掉,结果就报错了:
网页
再写个网页玩一下:
-//W3C//DTD XHTML 1.0 Transitional//EN""http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
TestvarobjCom =newActiveXObject("Streamlet.COMProvider.SampleClass.1");
objCom.SampleMethod();