上一篇文章为:
Froser:COM编程攻略(十六 名字对象IMoniker与对象运行表ROT)zhuanlan.zhihu.com这一篇文章,我们将详细介绍COM的类型库。
一、类型库
在第八篇文章中,我们简单地提到了:
3. Type Library (tlb)
MIDL在生成各种.c的文件的同时,也会生成一个tlb文件。tlb文件将接口符号化(可以认为是建立反射关系),供COM可感知的环境(如VB)使用。通过Type Library,可以实现反射功能,也可以拿出这个Library的诸多信息。
简单地来看,idl文件是我们定义我们接口的文本文件,tlb就是idl的二进制版本。它所表达的内容和tlb一模一样,都表示接口的元信息:分别有哪些接口?它们方法接受几个参数?接口中方法的顺序是怎样的?等等。
我们在idl中可以用关键字library来表示一个类型库:
import "oaidl.idl";
import "ocidl.idl";
[object, uuid(8B82ACF5-2A77-4385-B254-4BC5C6F12FB5)]
interface IMessage : IUnknown
{
HRESULT Print();
};
[
uuid(dfa98c1f-2f81-4115-8dd5-692d91ee7342),
helpstring("This is a DEMO component"),
version(1.0),
]
library ATLProject1Lib
{
importlib("stdole2.tlb");
[uuid(B8922344-0FD5-4630-A4D7-DD9C9321BBB1)]
coclass Message
{
interface IMessage;
};
};
其中,只有library范围内的coclass Message才会写入对应的信息到tlb,IMessage接口并不能把自身的信息生成到tlb中。每一个library都由一个唯一的uuid来表示,例如上面的例子是dfa98c1f-2f81-4115-8dd5-692d91ee7342。helpstring表示它的一个描述,可以在VB编辑器中添加引用时,或者其它IDE中看到这段文字(通常是作为一个友好的显示名),version表示这个库的版本。
接下来,importlib("stdole2.tlb")表示,我们生成的tlb中还会包含IUnknown、IDispatch等接口的tlb信息。
因为library中的类已经有了类型信息,所以它能够被反射,那么它就支持了默认的Marshal/Unmarshal。因此,MIDL不会为它生成Proxy和Stub的相关代码。只有在library之外的接口,如IMessage,MIDL才会为它生成Proxy和Stub。
由于IDL被设计成支持任何语言,所以它对于各种语言自己定义的基本类型,有自己的一套转换规则,我们可以从下面的文档中获取:
Data Type Conversions - Win32 appsdocs.microsoft.com一旦一个tlb被生成了,我们除了让单独提供它之外,我们可以将它嵌入RC文件中。嵌入RC文件会使程序更加整洁,因为你只需要提供dll或者exe就可以了。嵌入tlb的方法是,在RC文件中添加下面这一行:
1 TYPELIB "你的tlb文件名"
例如,在使用VS生成一个ATL dll工程之后,我们使用刚刚的idl,那么我们会得到这样的RC文件:
// 上面一些关于程序本身的信息已经省略
/
//
// REGISTRY
//
IDR_ATLPROJECT1 REGISTRY "ATLProject1.rgs"
/
//
// String Table
//
STRINGTABLE
BEGIN
IDS_PROJNAME "ATLProject1"
END
#endif // 中文(简体,中国) resources
/
#ifndef APSTUDIO_INVOKED
/
//
// Generated from the TEXTINCLUDE 3 resource.
//
1 TYPELIB "ATLProject1.tlb"
/
#endif // not APSTUDIO_INVOKED
IDR_ALTPROJECT1对应的是工程中的rgs文件,也就是ATL会解析它来自注册写注册表的脚本文件,我们用过很多次了。IDS_PROJNAME则是表示当前工程名,是一个字符串。最后的TYPELIB表示将生成的tlb嵌入到这个模块中,成为它的一部分。
同时,它会为我们生成一个dllmain.h和dllmain.cpp:
//dllmain.h
class CATLProject1Module : public ATL::CAtlDllModuleT< CATLProject1Module >
{
public :
DECLARE_LIBID(LIBID_ATLProject1Lib)
DECLARE_REGISTRY_APPID_RESOURCEID(IDR_ATLPROJECT1, "{dfa98c1f-2f81-4115-8dd5-692d91ee7342}")
};
extern class CATLProject1Module _AtlModule;
//dllmain.cpp
CATLProject1Module _AtlModule;
// DLL 入口点
extern "C" BOOL WINAPI DllMain(HINSTANCE hInstance, DWORD dwReason, LPVOID lpReserved)
{
hInstance;
return _AtlModule.DllMain(dwReason, lpReserved);
}
这上面两段宏,用于注册类型库。LIBID_ATLProject1Lib表示我们library所定义的uuid,也就是dfa98c1f-2f81-4115-8dd5-692d91ee7342,IDR_ATLPROJECT1则是我们在资源文件中所看到的rgs文件。
二、注册类型库
在读取类型信息之前,必须要注册类型库到注册表。类型库在注册表的HKEY_CLASSES_ROOTTypeLib中,它下面每一个键表示的是一个类型库,对应着库的uuid:
如上图所示,一个uuid为{00020430-0000-0000-C000-000000000046}的库,它下面有2个子键——1.0和2.0,对应着它在idl中定义的版本。其中1.0的名字叫做OLE Automation,也就是用于做OLE自动化的库。在每个版本号的子键下,又有它的一些更多信息。例如在0/win32键下面,存放着它的tlb的位置——如果它没有内嵌tlb,那么它就表示tlb的文件路径,如果它内嵌了tlb,那么就是模块的位置:
除了在HKCRTypeLib中注册自己的TypeLib GUID外,我们应当将自己的CLSID和TypeLib关联起来,关联的方法是,在HKCRCLSID{自己的CLSID}下,新建一个TypeLib键,值为其TypeLib的GUID。如果它要指明某个TypeLib版本,那么建立一个Version键,里面值为TypeLib版本号,如1.0:
三、ATL dll工程注册流程
如果是使用VS中的ATL模板生成dll工程,那么它为实现DllRegisterServer供regsvr32.dll调用。
以上面的dllmain源代码为例:ATL会按照如下方式为我们注册类型库:
//dllmain.h
class CATLProject1Module : public ATL::CAtlDllModuleT< CATLProject1Module >
{
public :
DECLARE_LIBID(LIBID_ATLProject1Lib)
DECLARE_REGISTRY_APPID_RESOURCEID(IDR_ATLPROJECT1, "{dfa98c1f-2f81-4115-8dd5-692d91ee7342}")
};
以上代码,宏展开后会变成这样:
class CATLProject1Module : public ATL::CAtlDllModuleT< CATLProject1Module >
{
public :
static void InitLibId() throw()
{
ATL::CAtlModule::m_libid = LIBID_ATLProject1Lib;
}
static LPCOLESTR GetAppId() throw()
{
return OLESTR("{dfa98c1f-2f81-4115-8dd5-692d91ee7342}");
}
static const TCHAR* GetAppIdT() throw()
{
return _T("{dfa98c1f-2f81-4115-8dd5-692d91ee7342}");
}
static HRESULT WINAPI UpdateRegistryAppId(_In_ BOOL bRegister) throw()
{
ATL::_ATL_REGMAP_ENTRY aMapEntries [] =
{
{ OLESTR("APPID"), GetAppId() },
{ NULL, NULL }
};
return ATL::_pAtlModule->UpdateRegistryFromResource(IDR_ATLPROJECT1, bRegister, aMapEntries);
}
};
此时我们增加我们工厂类和IMessage实现类的代码:
class MessageImpl
: public CComObjectRoot
, public CComCoClass<MessageImpl, &CLSID_Message>
, public IMessage
{
public:
DECLARE_CLASSFACTORY()
BEGIN_COM_MAP(MessageImpl)
COM_INTERFACE_ENTRY(IMessage)
END_COM_MAP()
MessageImpl();
~MessageImpl();
public:
DECLARE_REGISTRY_RESOURCEID(IDR_MessageImpl);
STDMETHODIMP Print() override;
};
OBJECT_ENTRY_AUTO(CLSID_Message, MessageImpl);
和以往一样,我们将MessageImpl实现其IMessage::Print函数,并使用OBJECT_ENTRY_AUTO将它暴露出去。
留意到,如果我们使用Add Wizard创建的这个类,那么它会为我们新建一个MessageImpl.rgs注册文件,其资源名叫作IDR_MessageImpl,并且通过宏DECLARE_REGISTRY_RESOURCEID(IDR_MessageImpl)展开在了MessageImpl中。DECLARE_REGISTRY_RESOURCEID主要是实现了UpdateRegistry方法,能够读取对应的rgs文件,来写注册表:
static HRESULT WINAPI UpdateRegistry(_In_ BOOL bRegister) throw();
因此,VS会为你在每一个通过OBJECT_ENTRY_AUTO暴露出去的工厂类创建一个rgs文件,并且实现一个解析它写注册表的UpdateRegistry方法。
当模块需要被注册时(通过regsvr32.exe调用):
模块在启动时,CATLProject1Module实例被创建,它从基类先调用了CATLProject1Module::InitLibId,将LIBID_ATLProject1Lib设置到全局变量上:
template <class T>
class ATL_NO_VTABLE CAtlModuleT :
public CAtlModule
{
public :
CAtlModuleT() throw()
{
T::InitLibId(); // T就是CATLProject1Module
}
...
}
然后在DllRegisterServer流程中,首先是CATLProject1Module::RegisterAppID()被调用,然后是CATLProject1Module::RegisterServer被调用。现在具体分析这2步:
HRESULT DllRegisterServer(
_In_ BOOL bRegTypeLib = TRUE) throw()
{
LCID lcid = GetThreadLocale();
SetThreadLocale(LOCALE_SYSTEM_DEFAULT);
// registers object, typelib and all interfaces in typelib
T* pT = static_cast<T*>(this);
HRESULT hr = pT->RegisterAppId();
if (SUCCEEDED(hr))
hr = pT->RegisterServer(bRegTypeLib);
SetThreadLocale(lcid);
return hr;
}
1、RegisterAppId流程——注册AppID
AppID是多个CLSID对应的一个统一的GUID,相同的AppID的类,拥有相同的配置,而这些配置项记录在了AppID注册表的键中。这个步骤,会将AppID注册到注册表。详细请看:
Froser:COM编程攻略(十九 AppID、Dll代理)zhuanlan.zhihu.com2、RegisterServer流程——解析rgs脚本;注册组件类别与类型库
当CATLProject1Module::RegisterAppId调用完毕后,CATLProject1Module::RegisterServer将被调用。最终,它会调入AtlComModuleRegisterServer方法:
ATLINLINE ATLAPIINL AtlComModuleRegisterServer(
_Inout_ _ATL_COM_MODULE* pComModule,
_In_ BOOL bRegTypeLib,
_In_opt_ const CLSID* pCLSID)
{
ATLASSERT(pComModule != NULL);
if (pComModule == NULL)
return E_INVALIDARG;
ATLASSERT(pComModule->m_hInstTypeLib != NULL);
HRESULT hr = S_OK;
for (_ATL_OBJMAP_ENTRY_EX** ppEntry = pComModule->m_ppAutoObjMapFirst; ppEntry < pComModule->m_ppAutoObjMapLast; ppEntry++)
{
if (*ppEntry != NULL)
{
_ATL_OBJMAP_ENTRY_EX* pEntry = *ppEntry;
if (pCLSID != NULL)
{
if (!IsEqualGUID(*pCLSID, *pEntry->pclsid))
continue;
}
hr = pEntry->pfnUpdateRegistry(TRUE);
if (FAILED(hr))
break;
hr = AtlRegisterClassCategoriesHelper( *pEntry->pclsid,
pEntry->pfnGetCategoryMap(), TRUE );
if (FAILED(hr))
break;
}
}
if (SUCCEEDED(hr) && bRegTypeLib)
{
ATLASSUME(pComModule->m_hInstTypeLib != NULL);
hr = AtlRegisterTypeLib(pComModule->m_hInstTypeLib, 0);
}
return hr;
}
AtlComModuleRegisterServer主要做了2件事情。
第一件:遍历所有通过ENTRY_OBJECT宏暴露的类,比如我们这里就是MessageImpl,调用它的UpdateRegistery,由刚刚我们提到的每个CCoClass里面的DECLARE_REGISTRY_RESOURCEID展开的函数。然后它通过AtlRegisterClassCategoriesHelper注册了其组件类别(https://zhuanlan.zhihu.com/p/141397471)。
第二件:通过AtlRegisterTypeLib方法,注册类型库。
一般的情况是全局的注册。对于全局的注册,使用RegisterTypeLib:
RegisterTypeLib function (oleauto.h) - Win32 appsdocs.microsoft.com它对应的反注册函数为UnRegisterTypeLib:
UnRegisterTypeLib function (oleauto.h) - Win32 appsdocs.microsoft.com微软还提供了一个简单的注册和读取函数:LoadTypeLibEx
LoadTypeLibEx function (oleauto.h) - Win32 appsdocs.microsoft.comHRESULT LoadTypeLibEx(
LPCOLESTR szFile,
REGKIND regkind,
ITypeLib **pptlib
);
它的作用是从szFile中读取一个类型库,例如我们通过GetModuleFileName拿到当前模块路径传递给szFile,那么就假定了我们的这个模块是内嵌了一个tlb的。类型库结果从pptlib返回。regkind表示一些额外的行为。如果是REGKIND_REGISTER,那么它在通过LoadTypeLib读取类型库之后会调用RegisterTypeLib()来进行注册;如果是REGKIND_DEFAULT,那么只读取而不注册。
最终,我们的rgs可以这么来写:
HKCR
{
NoRemove CLSID
{
NoRemove {B8922344-0FD5-4630-A4D7-DD9C9321BBB1} = s 'Message'
{
ProgID = s 'Message'
TypeLib = s '{dfa98c1f-2f81-4115-8dd5-692d91ee7342}'
Version = s '1.0'
InProcServer32 = s '%MODULE%'
{
val ThreadingModel = s 'Both'
}
}
}
}
上面rgs脚本,指明了我们的类Message所关联的TypeLib以及其版本(1.0)。TypeLib本身的注册(HKCRTypeLib),ATL会为我们自动完成。
总的来说,一个模块的注册流程是:
- 通过模块rgs来写注册表
- 注册AppID
- 如果是个服务模块,则注册服务
- 遍历每个暴露的工厂类,通过它们的rgs来写注册表
- 遍历每个暴露的工厂类,注册其组件类别。
- 注册类型库
服务、AppID和组件类别我会在今后的文章中介绍。 ATL在注册过程中还有一些细节,例如设置当前Locale,然后注册完毕后还原等,这里就不展开了。如果自己实现注册功能,可以参考上面的6步来做。
四、读取类型库
1、通过LoadTypeLib读取类型库
LoadTypeLib function (oleauto.h) - Win32 appsdocs.microsoft.comLoadTypeLib在上文有提到,是从某个文件(可能是tlb,也可能是嵌入了tlb的dll或者exe)中获取类型库的信息。LoadTypeLibEx正是调用它来从文件读取一个类型库信息:
HRESULT LoadTypeLib(
LPCOLESTR szFile,
ITypeLib **pptlib
);
对于给定的一个CLSID,我们可以通过查找注册表的HKCRTypeLib{CLSID}{Version}Win32|Win64,得到模块或者tlb的位置,然后作为szFile参数传递给LoadTypeLib第一个参数,获取一个类型库接口。微软提供了一个辅助函数LoadRegTypeLib,帮助我们完成上面查询注册表的工作:
HRESULT LoadRegTypeLib(
REFGUID rguid, // 库GUID
WORD wVerMajor, // 主版本号
WORD wVerMinor, // 次版本号
LCID lcid, // 语言ID
ITypeLib **pptlib // 结果
);
ITypeLib定义如下:
interface ITypeLib : IUnknown
{
// 有多少类型?
[local]
UINT GetTypeInfoCount(
void
);
// 通过索引获取类型信息
HRESULT GetTypeInfo(
[in] UINT index,
[out] ITypeInfo ** ppTInfo
);
// 通过索引获取类型的类别,如是枚举?还是CoClass?
HRESULT GetTypeInfoType(
[in] UINT index,
[out] TYPEKIND * pTKind
);
// 从GUID中检索类型信息
HRESULT GetTypeInfoOfGuid(
[in] REFGUID guid,
[out] ITypeInfo ** ppTinfo
);
// 省略
// ......
}
这个接口可以枚举出所有的类型信息,以及库相关的信息。
因为我们知道一个库中包含了很多接口、枚举等,因此可以通和ITypeLib::GetTypeInfo配合ITypeLib::GetTypeInfoCount,来遍历所有的类型信息,或者是用ITypeLib::GetTypeInfoOfGuid来检索某一个CLSID的类型信息。
一个类型所包含的元信息由ITypeInfo表示,它包含很多方法。它的强大程度如同QT中的QMetaObject:
ITypeInfo (oaidl.h) - Win32 appsdocs.microsoft.com比较常见的方法有ITypeInfo::GetIDsOfNames(通过名字拿id),ITypeInfo::Invoke(通过id调用对应的方法),这些在IDispatch中被运用。我们在
Froser:COM编程攻略(八 动态调用与IDispatch接口)zhuanlan.zhihu.com上面的文章中已经说到过,ATL提供的IDispatchImpl,正是通过拿IDispatch的ITypeInfo,来动态转发消息到自己对应的接口。
2、通过IProvideClassInfo获取ITypeInfo
有某些原因导致我们无法拿到类型库或者某个接口的类型信息。例如,这个接口没有写在idl的library中,那么我们就无法通过文件的方式拿ITypeLib和ITypeInfo了。
如果这样一个类支持返回自己的类型信息,那么它必须要实现下面的接口:
interface IProvideClassInfo : IUnknown
{
// Get a pointer to the type information for this CLSID.
HRESULT GetClassInfo([out] ITypeInfo** ppTI);
}
大部分的类型因为本身已经注册到了注册表,并且存在于tlb中,所以它们有大致如下的实现:
HRESULT MessageImpl::GetClassInfo(ITypeInfo** pTypeInfo)
{
ITypeLib* pTypeLib;
LoadRegTypeLib(LIBID_ATLProject1Lib, 1, 0, LANG_NEUTRAL,
&pTypeLib); // 从注册表获取ITypeLib
HRESULT hr = pTypeLib->GetTypeInfoOfGuid(CLSID_Message,
&pTypeInfo); // 检索一个COCLASS的类型信息
pTypeLib->Release();
return hr;
}
IProvideClassInfo拥有一个增强版本IProvideClassInfo2:
interface IProvideClassInfo2 : IProvideClassInfo
{
HRESULT GetGUID(
[in] DWORD dwGuidKind,
[out] GUID * pGUID
);
}
它可以提供一个类型default source dispinterface。它的作用我们在连接点一篇文章中讲过:
Froser:COM编程攻略(十四 连接点与其ATL实现)zhuanlan.zhihu.com例如,我们假设有个回调接口_IATLSimpleObjectEvents,它在idl下表示为
coclass ATLSimpleObject
{
[default] interface IATLSimpleObject;
[default, source] dispinterface _IATLSimpleObjectEvents;
};
那么ATLSimpleObject::GetGUID就可以实现为:
HRESULT ATLSimpleObject::GetGUID(DWORD dwGuidKind, GUID* pGUID)
{
if(pGUID == NULL)
return E_INVALIDARG;
*pGUID = IID__IATLSimpleObjectEvents;
return S_OK;
}
我们并没有用到dwGuidKind这个参数,它表示我们返回的是哪种IID,就目前情况,它仅仅支持传入GUIDKIND_DEFAULT_SOURCE_DISP_IID,表示拿一个default source dispinterface。
五、动态创建tlb
tlb格式是微软的MIDL.exe所生成的文件格式,它没有暴露太多细节,对于它的内容布局,都是魔法。我们很少需要自己生成一个tlb格式,因为MIDL.exe会为我们生成。不过,如果我们在开发一个IDE,需要自己解析文本,写tlb文件,那么微软也是提供支持了的。
微软提供了一个函数CreateTypeLib2,返回一个接口,我们可以用这个接口来操作tlb文件:
HRESULT CreateTypeLib2(
SYSKIND syskind, // 系统类型,如SYS_WIN32
LPCOLESTR szFile, // 创建的tlb文件
ICreateTypeLib2 **ppctlib // 结果
);
例如:
ICreateTypeLib2* pCreateTypeLib2;
CreateTypeLib2(SYS_WIN32, L"D:mylib.tlb", &pCreateTypeLib2);
上面的代码将会创建一个D盘下的mylib.tlb。
一旦我们拿到ICreateTypeLib2接口,那么就可以开始搞事情了。它里面有非常多的编辑tlb的方法:
ICreateTypeLib (oaidl.h) - Win32 appsdocs.microsoft.com例如我们以下的idl文件:
[
uuid(dfa98c1f-2f81-4115-8dd5-692d91ee7342),
version(1.0),
]
library ATLProject1Lib
{
};
如果使用ICreateTypeLib2来生成,那么大概是这样:
GUID LIBID_ATLProject1Lib =
{0xdfa98c1f,0x2f81,0x4115,
{0x8d,0xd5,0x69,0x2d,0x91,0xee,0x73,0x42}};
pCreateTypeLib2->SetGuid(LIBID_ATLProject1Lib);
OLECHAR szName[] = L"ATLProject1Lib";
pCreateTypeLib2->SetName(szName);
pCreateTypeLib2->SetVersion(1, 0);
通过ICreateTypeInfo::CreateTypeInfo,我们可以往library中添加类型。例如:
[
uuid(dfa98c1f-2f81-4115-8dd5-692d91ee7342),
version(1.0),
]
library ATLProject1Lib
{
[object, uuid(8B82ACF5-2A77-4385-B254-4BC5C6F12FB5)]
interface IMessage : IUnknown
{
HRESULT Print();
};
};
如果使用ICreateTypeLib2来生成,则是:
ICreateTypeInfo* pCreateTypeInfoInterface;
pCreateTypeLib2->CreateTypeInfo(L"IMessage", TKIND_INTERFACE,
&pCreateTypeInfoInterface);
pCreateTypeInfoInterface->SetGuid(.....); /* 设置它的Guid */
.... /* 添加方法等 */
ICreateTypeLib2::CreateTypeInfo,返回的新创建的对象类型为ICreateTypeInfo,我们可以再来为它设置Guid等属性:
ICreateTypeInfo (oaidl.h) - Win32 appsdocs.microsoft.comICreateTypeInfo的增强版本叫ICreateTypeInfo2,它提供了删除功能,从ICreateTypeInfo实例QueryInterface ICreateTypeInfo2的IID,便能得到它的接口。
通过ICreateTypeInfo::SetTypeFlags,可以给类型加上dual、oleautomation、hidden等idl中的关键字,并且定义是否能从ITypeInfo中调用CreateInstance创建自己。flags一览:
TYPEFLAGS (oaidl.h) - Win32 appsdocs.microsoft.com通过ICreateTypeInfo::AddImplType,我们可以表示一个类型实现了另外一个类型。它需要传入一个表示类型的引用索引HREFTYPE,通过ICreateTypeInfo::AddRefTypeInfo获得。
通过ICreateTypeInfo::SetImplTypeFlags,我们可以为它实现的接口设置source, default, restricted等关键字。
通过ICreateTypeInfo::AddFuncDesc,我们可以增加一个函数。它需要传一个麻烦的FUNCDESC结构:
https://docs.microsoft.com/en-us/windows/win32/api/oaidl/ns-oaidl-funcdescdocs.microsoft.com其中用到的结构体ELEMDESC,表示返回值或者参数类型的描述,详见MSDN:
https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-oaut/e14ff3cf-034a-4884-a498-fc7586f7160cdocs.microsoft.com通过ICreateTypeInfo::SetFuncAndParamNames,可以为创建的函数的参数命名。VB中支持按照参数名传参,DISPARAMS中也有对应的字段表示参数顺序(见https://zhuanlan.zhihu.com/p/134143144)。
最后,调用ICreateTypeInfo::LayOut,为你创建的函数设置虚表的偏移,为dual接口设置相关的描述。在此之后,布局已定,ICreateTypeInfo的方法不应该再被调用。
在最后的最后,一定要调用ICreateTypeLib::SaveAllChanges()来保存当前所有的操作,否则一切工作都是徒劳的。
ICreateTypeLib(2)、ICreateTypeInfo(2)系列接口,操作的其实是一颗tlb对象树,在里面增加、删减或者修改结点。
六、创建tlb实例
假设我们要用代码来创建一个idl对应的tlb:
import "oaidl.idl";
import "ocidl.idl";
[
uuid(dfa98c1f-2f81-4115-8dd5-692d91ee7342),
version(1.0),
helpstring("This is a demo library")
]
library ATLProject1Lib
{
importlib("stdole2.tlb");
[object, uuid(8B82ACF5-2A77-4385-B254-4BC5C6F12FB5)]
interface IMessage : IUnknown
{
HRESULT Print([in] BSTR strMsg);
};
[uuid(B8922344-0FD5-4630-A4D7-DD9C9321BBB1)]
coclass Message
{
interface IMessage;
};
};
import "shobjidl.idl";
代码如下:
using namespace ATL;
struct CoInitHelper
{
CoInitHelper() { CoInitialize(NULL); }
~CoInitHelper() { CoUninitialize(); }
};
int main()
{
CoInitHelper coInitGuard; // 自动创建/释放套间
HRESULT hr = S_OK;
OLECHAR szPath[] = L"D:ATLProject1Lib.tlb";
CComPtr<ICreateTypeLib2> pCreateTypeLib2;
hr = CreateTypeLib2(SYS_WIN32, szPath, &pCreateTypeLib2);
OLECHAR szName[] = L"ATLProject1Lib";
OLECHAR szHelpString[] = L"This is a demo library";
/*
[
uuid(dfa98c1f-2f81-4115-8dd5-692d91ee7342),
version(1.0),
helpstring("This is a demo library")
]
library ATLProject1Lib
*/
GUID LIBID_ATLProject1Lib = { 0xdfa98c1f, 0x2f81, 0x4115, 0x8d, 0xd5, 0x69, 0x2d, 0x91, 0xee, 0x73, 0x42 };
hr = pCreateTypeLib2->SetGuid(LIBID_ATLProject1Lib);
hr = pCreateTypeLib2->SetName(szName);
hr = pCreateTypeLib2->SetVersion(1, 0);
hr = pCreateTypeLib2->SetDocString(szHelpString);
hr = pCreateTypeLib2->SetLcid(LANG_NEUTRAL); // 区域无关
/*
[object, uuid(8B82ACF5-2A77-4385-B254-4BC5C6F12FB5)]
interface IMessage : IUnknown
{
HRESULT Print([in] BSTR strMsg);
};
*/
OLECHAR szIMessage[] = L"IMessage";
CComPtr<ICreateTypeInfo> pIMessageCreateTypeInfo;
hr = pCreateTypeLib2->CreateTypeInfo(szIMessage, TKIND_INTERFACE, &pIMessageCreateTypeInfo);
GUID IID_IMessage = { 0x8B82ACF5,0x2A77,0x4385,0xB2,0x54,0x4B,0xC5,0xC6,0xF1,0x2F,0xB5 };
hr = pIMessageCreateTypeInfo->SetGuid(IID_IMessage);
// 下面的代码描述一个方法virtual HRESULT __stdcall Print([in] BSTR strMsg) = 0;
{
FUNCDESC funcDescPrint = { 0 };
funcDescPrint.funckind = FUNC_PUREVIRTUAL; // 纯虚函数
funcDescPrint.invkind = INVOKE_FUNC; // 普通方法(不是propertyget也不是propertyput)
funcDescPrint.callconv = CC_STDCALL; // stdcall调用约定
TYPEDESC tdescParams = { 0 }; // 参数类型
tdescParams.vt = VT_INT;
ELEMDESC paramDesc = { 0 };
paramDesc.tdesc.vt = VT_BSTR;
paramDesc.tdesc.lptdesc = &tdescParams;
paramDesc.paramdesc.wParamFlags = PARAMFLAG_FIN; // [in]
funcDescPrint.cParams = 1; // 接受1个参数
funcDescPrint.lprgelemdescParam = ¶mDesc; // 参数的描述
TYPEDESC tDesc = { 0 };
tDesc.vt = VT_INT;
funcDescPrint.elemdescFunc.tdesc.vt = VT_HRESULT; // 返回值描述
funcDescPrint.elemdescFunc.tdesc.lptdesc = &tDesc;
hr = pIMessageCreateTypeInfo->AddFuncDesc(0, &funcDescPrint); // 添加这个方法到IMessage
OLECHAR szFuncName[] = L"Print";
OLECHAR szParam1Name[] = L"strMsg";
LPOLESTR szParamNames[] = { szFuncName, szParam1Name };
hr = pIMessageCreateTypeInfo->SetFuncAndParamNames(0, szParamNames, _countof(szParamNames)); // 将Imessage中描述的方法名字、参数1(BSTR)命名
}
// 描述ISum继承自IUnknown
// ICreateTypeInfo::AddImplType可以表示一个类型实现了某一个接口,它接受一个HREFTYPE索引表示所实现的接口,通过ICreateTypeInfo::AddRefTypeInfo得到
// 我们的思路是拿到IUnknown接口的HREFTYPE,作为pIMessageCreateTypeInfo->AddImplType的参数,表明IMessage继承了IUnknown
CComPtr<ITypeLib> pStdOleLib;
hr = LoadRegTypeLib(IID_StdOle, STDOLE2_MAJORVERNUM, STDOLE2_MINORVERNUM, STDOLE2_LCID, &pStdOleLib);
CComPtr<ITypeInfo> pIUnknownInfo;
hr = pStdOleLib->GetTypeInfoOfGuid(IID_IUnknown, &pIUnknownInfo);
HREFTYPE refIUnknown;
hr = pIMessageCreateTypeInfo->AddRefTypeInfo(pIUnknownInfo, &refIUnknown);
hr = pIMessageCreateTypeInfo->AddImplType(0, refIUnknown);
/*
[uuid(B8922344-0FD5-4630-A4D7-DD9C9321BBB1)]
coclass Message
{
interface IMessage;
};
*/
CComPtr<ICreateTypeInfo> pCoClassMessageCreateTypeInfo;
OLECHAR szMessage[] = L"Message";
hr = pCreateTypeLib2->CreateTypeInfo(szMessage, TKIND_COCLASS, &pCoClassMessageCreateTypeInfo);
GUID CLSID_Message = { 0xB8922344,0x0FD5,0x4630,0xA4,0xD7,0xDD,0x9C,0x93,0x21,0xBB,0xB1 };
hr = pCoClassMessageCreateTypeInfo->SetGuid(CLSID_Message);
// 一定要设置下面这个标记,这样它能自动实现ITypeInfo::CreateInstance
// MIDL.exe中自动生成了这个标记,我们自己创建的时候,需要手动设置上。
hr = pCoClassMessageCreateTypeInfo->SetTypeFlags(TYPEFLAG_FCANCREATE);
// 在coclassMessage中加入interface IMessage。
// 和上面类似,使用ICreateTypeInfo::AddImplType表明实现关系
CComPtr<ITypeInfo> pIMessageTypeInfo;
pIMessageTypeInfo = pIMessageCreateTypeInfo;
HREFTYPE refIMessage;
hr = pCoClassMessageCreateTypeInfo->AddRefTypeInfo(pIMessageTypeInfo, &refIMessage);
hr = pCoClassMessageCreateTypeInfo->AddImplType(0, refIMessage); // 将interface IMessage添加到coclass Message,表明coclass Message实现了IMessage
// 如果这个interface前面带有[source, default, ...]等标记,则调用
// pCoClassMessageCreateTypeInfo->SetImplTypeFlags(0, IMPLTYPEFLAG_FDEFAULT | IMPLTYPEFLAG_FSOURCE);
/*
最终,设置虚函数索引,并保存
*/
hr = pIMessageCreateTypeInfo->LayOut();
hr = pCreateTypeLib2->SaveAllChanges();
return 0;
}
运行结束后,它在D盘生成了一个ATLProject1Lib.tlb。我们使用微软的工具oleview.exe,通过File->View TypeLib... 打开这个tlb,得到结果如下:
oleview.exe会为我们的tlb生成对应的idl,并且通过树形图展示层次关系。通过比对发现,它与midl.exe中解析的idl生成的tlb是完全一致的,说明我们的操作完全正确。
下一篇:
Froser:COM编程攻略(十八 组件类别)zhuanlan.zhihu.com