COM是基于二进制的组件模块,从设计之初就以支持所有语言作为它的一个目标,这篇文章主要探讨COM的跨语言部分。
一般COM接口的实现肯定是以某一具体语言来实现的,比如说使用VC++语言,这就造成了一个问题,不同的语言对于接口的定义,各个变量的定义各不相同,如何让使用vc++或者说Java等其他语言定义的接口能被别的语言识别?为了达到这个要求,定义了一种文件格式idl——(Interface Definition Language)接口定义语言,IDL提供一套通用的数据类型,并以这些数据类型来定义更为复杂的数 据类型。一般来说,一个文件有下面几个部分说明
import "unknwn.idl";
[
object,
uuid(CF809C44-8306-4200-86A1-0BFD5056999E)
]
interface IMyString : IUnknown
{
HRESULT Init([in] BSTR bstrInit);
HRESULT GetLength([out, retval] ULONG *pretLength);
HRESULT Find([in] BSTR bstrFind, [out, retval] BSTR* bstrSub);
};
[
uuid(ADF50A71-A8DD-4A64-8CCA-FFAEE2EC7ED2),
version(1.0)
]
library ComDemoLib
{
importlib("stdole32.tlb");
[
uuid(EBD699BA-A73C-4851-B721-B384411C99F4)
]
coclass CMyString
{
interface IMyString;
};
};
上面的例子中定义了一个IMyString接口继承自IUnknown接口,函数参数列表中in表示参数为输入参数,out表示它为输出参数,retval表示该参数是函数的返回值。import导入了一个库文件类似于include。而importlib导入一个tlb文件,我们可以将其看成VC++中的#pragma comment导入一个lib库 从上面不难看出一个IDL文件至少有3个ID,一个是接口ID,一个是库ID,还有一个就是实现类的ID 在VC环境中通过midl命令可以对该文件进行编译,编译会生成下面几个我们在编写实现时会用到的重要文件:
一般我们需要在dll文件中导出下面几个全局的导出函数:
STDAPI DllRegisterServer(void);
STDAPI DllUnregisterServer(void);
STDAPI DllGetClassObject(const CLSID & rclsid, const IID & riid, void ** ppv);
STDAPI DllCanUnloadNow(void);
其中DllRegisterServer用来向注册表中注册模块的相关信息,主要注测在HKEY_CLASSES_ROOT中,主要定义下面几项内容:
const TCHAR *g_RegTable[][3] = {
{ _T("CLSID\\{EBD699BA-A73C-4851-B721-B384411C99F4}"), 0, _T("FirstComLib.MyString")}, //组件ID
{ _T("CLSID\\{EBD699BA-A73C-4851-B721-B384411C99F4}\\InprocServer32"), 0, (const TCHAR*)-1 }, //组建路径
{ _T("CLSID\\{EBD699BA-A73C-4851-B721-B384411C99F4}\\ProgID"), 0, _T("FirstComLib.MyString")}, //组件名称
{ _T("CLSID\\{EBD699BA-A73C-4851-B721-B384411C99F4}\\TypeLib"), 0, _T("{ADF50A71-A8DD-4A64-8CCA-FFAEE2EC7ED2}") }, //类型库ID
{ _T("FirstComLib.MyString"), 0, _T("FirstComLib.MyString") }, //组件的字符串名称
{ _T("FirstComLib.MyString\\CLSID"), 0, _T("{EBD699BA-A73C-4851-B721-B384411C99F4}")}, //组件的CLSID
{ _T("FirstComLib.MyString\\CurVer"), 0, _T("FirstComLib.MyString.1.0") }, //组件版本
{ _T("FirstComLib.MyString.1.0"), 0, _T("FirstComLib.MyString") }, //当前版本的项目名称
{ _T("FirstComLib.MyString.1.0\\CLSID"), 0, _T("{EBD699BA-A73C-4851-B721-B384411C99F4}")} //当前版本的CLSID
};
使用上一篇博文的代码,来循环注册这些项即可 DllGetClassObject:该函数用来生成对应的工厂类,而工厂类负责产生对应接口的实现类。 DllCanUnloadNow:函数用来询问是否可以卸载对应的dll,一般在COM中有两个全局的引用计数,用来记录当前内存中有多少个模块中的类,以及当前有多少个线程在使用它,如果当前没有线程使用或者存在的对象数为0,则可以卸载
实现部分的整体结构图如下:
由于所有类都派生自IUnknown,所在在这里就不显示这个基类了。 每个实现类都对应了一个它具体的类工厂,而项目中CMyString类的类厂的定义如下:
class CMyClassFactory : public IClassFactory
{
public:
CMyClassFactory();
~CMyClassFactory();
STDMETHOD(CreateInstance)(IUnknown *pUnkOuter, REFIID riid, void **ppvObject);
STDMETHOD(LockServer)(BOOL isLock);
STDMETHOD(QueryInterface)(REFIID riid, void **ppvObject);
STDMETHOD_(ULONG, AddRef)(void);
STDMETHOD_(ULONG, Release)(void);
protected:
ULONG m_refs;
};
STDMETHOD宏展开如下:
#define STDMETHOD(method) virtual HRESULT __stdcall method
所以上面的代码展开后就变成了:
virtual HRESULT __stdcall CreateInstance((IUnknown *pUnkOuter, REFIID riid, void **ppvObject);
另外3个派生自IUnknown接口就没什么好说的,主要说说另外两个: CreateInstance:主要用来生成对应的实现类,然后再调用实现类——CMyString的QueryInterface函数生成对应的接口 LockServer:当前是否被锁住:如果传入的值为TRUE,则表示被锁住,对应的锁计数器+1, 否则 -1 至于CMyString类的代码与之前的大同小异,也就没什么说的。 其他语言想要调用,以该项目为例,一般会经历下面几个步骤:
这些全局函数的作用与之前的相同,它里面多了一个_Module的全局对象,该对象类似于MFC中的CWinApp类,它用来表示整个项目的实例,里面封装了对于引用计数的管理,以及对项目中各个接口注册信息的管理,所以看DllRegisterServer等函数就会发现它们里面其实很简单,大部分的工作都由_Module对象完成。 整个IDL文件的定义如下:
import "oaidl.idl";
import "ocidl.idl";
[
object,
uuid(E3BD0C14-4D0C-48F2-8702-9F8DBC96E154),
dual,
helpstring("IMyString Interface"),
pointer_default(unique)
]
interface IMyString : IDispatch
{
};
[
uuid(A61AC54A-1B3D-4D8E-A679-00A89E2CBE93),
version(1.0),
helpstring("FirstAtlCom 1.0 Type Library")
]
library FIRSTATLCOMLib
{
importlib("stdole32.tlb");
importlib("stdole2.tlb");
[
uuid(11CBC0BE-B2B7-4B5C-A186-3C30C08A7736),
helpstring("MyString Class")
]
coclass MyString
{
[default] interface IMyString;
};
};
里面的内容与上一次的内容相差无几,多了一个helpstring属性,该属性用于产生帮助信息,当使用者在调用接口函数时IDE会将此提示信息显示给调用者。 由于有系统框架给我们做的大量的工作,我们再也不用关心像引用计数的问题,只需要将精力集中在编写接口的实现上,减少了不必要的工作量。
至此从结构上说明了为了实现跨语言COM组件内部做了哪些工作,当然只有这些工作是肯定不够的,后面会继续说明它所做的另一块工作——提供的一堆通用的变量类型。