c 写跨语言dll,让COM组件可被跨语言调用

错误修正

首先修正一下上篇(《》)中的例子的一个小问题。类厂的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,来试试看。加入外部引用:

e08b359da9a30ee9b42d78776909f424.png

这里显示的都是类型库,而我们根本没有注册类型库……

类型库

类型库信息一般位于PE文件的资源段,如下图的TypeLib资源:

4d05f47a0b70fd3e120e8ce293cf89ca.png

类型库也可以单独存在于一个扩展名为.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}。单独编译这个文件,会在源代码目录产生四个文件:

e679a9df13b4e71b3eac18b18cc85739.png

同时还会在$(IntDir)(通常为$(ProjectDir)\$(Configuration),即源代码目录下的Debug或Release目录)产生一个COMProvider.tlb。

COMProvider_h.h中包含了原先Interface.h中得所有信息。现在可以把原先#inlcude “Interface.h”的地方都改成“COMProvider_h.h”了。

然后添加一个空的资源脚本文件(*.rc),查看代码:

81a3b9dc925de563298ecff0b2166a5b.png

找到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目录:

ed17b2a7e2fa152c4cffe8c16ae8d1bd.png

加入$(IntDir):

f76f35a0b012b7412ece4ae908856100.png

因为编译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项目,选择菜单“工程”|“引用...”:

71ce646ea8f354f72a0ecabc52e3dc99.png

看到一个可用的引用列表:

860a4169a1a1639dcc2c85836d8eeeda.png

我们的DLL已经赫然在目了!勾上它!

然后在默认窗口上放个按钮加几行代码:

PrivateSubCommand1_Click()

DimobjAsCOMProviderLib.SampleClass

obj = CreateObject("Streamlet.COMProvider.SampleClass.1")

obj.SampleMethod()

EndSub

运行结果如下:

84e483b6eb0be7780647d16ddd1e1228.png

运行成功。

可见,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的情况,运行结果如下图:

2594e1b4723526cdd97df63eb9e7d325.png

VB6

还是VB6,不过这次通过自动化接口调用。去掉对COMProvider的引用,然后改写代码:

PrivateSubCommand1_Click()

DimobjAsObject

Set obj = CreateObject("Streamlet.COMProvider.SampleClass.1")

obj.SampleMethod

EndSub

同样能够运行:

ce075cc7b546cc29c5e8e4f4d15f8890.png

要测试确实是通过自动化接口运行的而不是通过虚函数表运行的,可以把暴露IDispatch那一行注释掉,结果就报错了:

cc186576efcb8a8957b9bec4ce7abc87.png

网页

再写个网页玩一下:

-//W3C//DTD XHTML 1.0 Transitional//EN""http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

Test

varobjCom =newActiveXObject("Streamlet.COMProvider.SampleClass.1");

objCom.SampleMethod();

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值