com类工厂中的clsid为1365a45f_COM编程攻略(十七 类型库与类型信息)

上一篇文章为:

Froser:COM编程攻略(十六 名字对象IMoniker与对象运行表ROT)​zhuanlan.zhihu.com
zhihu-card-default.svg

这一篇文章,我们将详细介绍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 apps​docs.microsoft.com
bb7001284bfe680bc80fd08697dfca57.png

一旦一个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:

5145d684865a8c243d0351ae26f9229c.png

如上图所示,一个uuid为{00020430-0000-0000-C000-000000000046}的库,它下面有2个子键——1.0和2.0,对应着它在idl中定义的版本。其中1.0的名字叫做OLE Automation,也就是用于做OLE自动化的库。在每个版本号的子键下,又有它的一些更多信息。例如在0/win32键下面,存放着它的tlb的位置——如果它没有内嵌tlb,那么它就表示tlb的文件路径,如果它内嵌了tlb,那么就是模块的位置:

a88ac1afd2d99f2f926dda7cd0dbb5d9.png
在我们的例子中,由于tlb被嵌入了资源文件,所以tlb的路径是整个dll的路径

a59d294af87758f6b70043acb3569cf5.png
stdole32单独导出了tlb,所以它的win32路径是tlb路径

除了在HKCRTypeLib中注册自己的TypeLib GUID外,我们应当将自己的CLSID和TypeLib关联起来,关联的方法是,在HKCRCLSID{自己的CLSID}下,新建一个TypeLib键,值为其TypeLib的GUID。如果它要指明某个TypeLib版本,那么建立一个Version键,里面值为TypeLib版本号,如1.0:

cff5b11a7a4a6efc2448a3fd2eb5f529.png
GUID为{B8922344-0FD5-4630-A4D7-DD9C9321BBB1}的类,其TypeLib的GUID为{dfa98c1f-2f81-4115-8dd5-692d91ee7342}

三、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.com
zhihu-card-default.svg

2、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 apps​docs.microsoft.com
bb7001284bfe680bc80fd08697dfca57.png

它对应的反注册函数为UnRegisterTypeLib:

UnRegisterTypeLib function (oleauto.h) - Win32 apps​docs.microsoft.com
bb7001284bfe680bc80fd08697dfca57.png

微软还提供了一个简单的注册和读取函数:LoadTypeLibEx

LoadTypeLibEx function (oleauto.h) - Win32 apps​docs.microsoft.com
bb7001284bfe680bc80fd08697dfca57.png
HRESULT 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会为我们自动完成。

总的来说,一个模块的注册流程是:

  1. 通过模块rgs来写注册表
  2. 注册AppID
  3. 如果是个服务模块,则注册服务
  4. 遍历每个暴露的工厂类,通过它们的rgs来写注册表
  5. 遍历每个暴露的工厂类,注册其组件类别。
  6. 注册类型库

服务、AppID和组件类别我会在今后的文章中介绍。 ATL在注册过程中还有一些细节,例如设置当前Locale,然后注册完毕后还原等,这里就不展开了。如果自己实现注册功能,可以参考上面的6步来做。

四、读取类型库

1、通过LoadTypeLib读取类型库

LoadTypeLib function (oleauto.h) - Win32 apps​docs.microsoft.com
bb7001284bfe680bc80fd08697dfca57.png

LoadTypeLib在上文有提到,是从某个文件(可能是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
            );

    // 省略
    // ......
}
https://docs.microsoft.com/en-us/windows/win32/api/oaidl/nn-oaidl-itypelib​docs.microsoft.com

这个接口可以枚举出所有的类型信息,以及库相关的信息。

0c4ddde01c7519b9887b7dc0a2cf9a3a.png

因为我们知道一个库中包含了很多接口、枚举等,因此可以通和ITypeLib::GetTypeInfo配合ITypeLib::GetTypeInfoCount,来遍历所有的类型信息,或者是用ITypeLib::GetTypeInfoOfGuid来检索某一个CLSID的类型信息。

一个类型所包含的元信息由ITypeInfo表示,它包含很多方法。它的强大程度如同QT中的QMetaObject:

ITypeInfo (oaidl.h) - Win32 apps​docs.microsoft.com
bb7001284bfe680bc80fd08697dfca57.png

比较常见的方法有ITypeInfo::GetIDsOfNames(通过名字拿id),ITypeInfo::Invoke(通过id调用对应的方法),这些在IDispatch中被运用。我们在

Froser:COM编程攻略(八 动态调用与IDispatch接口)​zhuanlan.zhihu.com
zhihu-card-default.svg

上面的文章中已经说到过,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
zhihu-card-default.svg

例如,我们假设有个回调接口_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 apps​docs.microsoft.com
bb7001284bfe680bc80fd08697dfca57.png
CreateTypeLib2 function (oleauto.h) - Win32 apps​docs.microsoft.com
bb7001284bfe680bc80fd08697dfca57.png

例如我们以下的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 apps​docs.microsoft.com
bb7001284bfe680bc80fd08697dfca57.png

ICreateTypeInfo的增强版本叫ICreateTypeInfo2,它提供了删除功能,从ICreateTypeInfo实例QueryInterface ICreateTypeInfo2的IID,便能得到它的接口。

通过ICreateTypeInfo::SetTypeFlags,可以给类型加上dual、oleautomation、hidden等idl中的关键字,并且定义是否能从ITypeInfo中调用CreateInstance创建自己。flags一览:

TYPEFLAGS (oaidl.h) - Win32 apps​docs.microsoft.com
bb7001284bfe680bc80fd08697dfca57.png

通过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-funcdesc​docs.microsoft.com

d57c0ce8511ea52e791c65dc33acdbc9.png

其中用到的结构体ELEMDESC,表示返回值或者参数类型的描述,详见MSDN:

https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-oaut/e14ff3cf-034a-4884-a498-fc7586f7160c​docs.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 = &paramDesc; // 参数的描述

		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,得到结果如下:

aeecb2ed06e83566f4e3caf5c50d65b5.png

oleview.exe会为我们的tlb生成对应的idl,并且通过树形图展示层次关系。通过比对发现,它与midl.exe中解析的idl生成的tlb是完全一致的,说明我们的操作完全正确。

下一篇:

Froser:COM编程攻略(十八 组件类别)​zhuanlan.zhihu.com
zhihu-card-default.svg
  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值