本篇文章为翻译文章,适合像我一样,之前从来没有接触过COM编程的人,如果翻译的有什么不足之处,希望大家多多指出。
原文链接:
https://www.codeproject.com/Articles/633/Introduction-to-COM-What-It-Is-and-How-to-Use-It
以下为译文部分:
这是一篇面向COM编程新手程序员的教程,解释了COM服务器的内部原理,以及如何使用C++编写你自己的接口。
我写这篇文章的目的是为了那些开始使用COM并且需要一些帮助来理解基础知识的程序员。本文简要介绍了COM规范,然后解释了一些COM术语,并描述了如何重用现有的COM组件。本文不讨论如何编写自己的COM对象或接口。
COM(Component Object Model)是最近WIndows世界中最流行的TLA(three-letter acronym)。一些新技术的出现都是基于COM的。并且这些技术文档中抛出了很多术语, 比如 COM对象 、 接口 、 服务器 等等,但都假设您已熟悉COM的基本工作原理和使用方法。
本文由浅入深描述COM的内在运行机制,并展示了如何使用其他人提供的COM对象(特别是Windows sheel)。阅读完本片文章,您将具备能够使用Windows和第三方提供的COM对象的能力。
本文假设您熟练使用C++。在我的示例代码中使用了一点MFC和ATL,但我会详细解释这些代码,因此如果您不熟悉MFC或者ATL也能够跟得上。本篇文章将包括如下内容:
简单地说,COM是一种跨不同应用程序和语言共享二进制代码的方法。这与c++方法不同,后者促进了源代码的重用。ATL就是一个很好的例子。虽然源代码级重用可以很好地工作,但它只适用于c++。它还引入了名称冲突的可能性,更不用说在项目中拥有多个代码副本而导致的工程膨胀和臃肿。
Windows允许使用dll在二进制级别上共享代码。毕竟,这就是Windows应用程序的功能——重用kernel32.dll, user32.dll,等等。但是由于dll是写在C接口上的,所以它们只能由C或理解C调用约定的语言使用。这就把共享的重担放在了编程语言实现者身上,而不是DLL本身。
MFC通过MFC扩展dll引入了另一种二进制共享机制。但是这些限制更加严格——你只能从MFC应用程序中使用它们。
COM通过定义二进制标准解决了所有这些问题,这意味着COM指定二进制模块(dll和exe)必须被编译以匹配特定的结构。该标准还精确地指定了在内存中必须如何组织COM对象。二进制文件还必须不依赖于任何编程语言的任何特性(比如c++中的名称装饰)。一旦完成了这些,就可以很容易地从任何编程语言访问模块。二进制标准将兼容性的重担压在了生成二进制文件的编译器上,这使得以后需要使用这些二进制文件的人更加容易。
内存中COM对象的结构恰好使用与c++虚函数相同的结构,这就是为什么许多COM代码使用c++的原因。但是请记住,编写模块所用的语言是不相关的,因为生成的二进制文件可以被所有语言使用。
顺便说一句,COM不是特定于win32的。理论上,它可以移植到Unix或任何其他操作系统。然而,我似乎从来没有提到过COM以外的Windows世界。
让我们从下往上。接口只是一组函数。这些函数被称为方法。接口名以I开头,例如IShellLink。在c++中,接口被编写为只有纯虚函数的抽象基类。
接口可以从其他接口继承。继承的工作方式就像c++中的单继承。接口不允许多重继承。
coclass (component object class的缩写)包含在DLL或EXE中,背后包含了一个或多个接口的代码。coclass被称为实现这些接口。COM对象是coclass在内存中的实例。请注意,COM“类”与c++“类”是不同的,尽管COM类的实现通常是c++类。
COM服务器 是一个二进制文件(DLL或EXE),其中包含一个或多个coclass。
注册 (Registration)是创建注册表项的过程,这些注册表项告诉Windows COM服务器的位置。
取消注册 (Unregistration) 是相反的——删除那些注册表项。
GUID (与“fluid”押韵,表示全球唯一标示符 – globally unique identifier)是一个128位的数字。 guid是COM的独立于语言的识别事物的方式。每个接口和coclass都有一个GUID。由于GUID在全世界都是唯一的,因此可以避免名称冲突 (只要您使用COM API创建它们)。您还会不时看到术语 UUID (它代表通用唯一识别符 – universally unique identifier)。实际上, uuid和guid是相同的。
类 ID 或 CLSID 是一个coclass的GUID名。接口 ID 或 IID 是一个接口(interface)的GUID的名字。
在COM中GUID使用如此的广泛,有两个原因:
HRESULT 是一个整数类型,是COM用来返回错误和成功码的。它不是任何东西的“句柄”,尽管有H前缀。稍后我将对HRESULT以及如何测试它们进行更多的说明。
最后,COM库(COM library) 是操作系统的一部分,当你做与COM相关的事情时,它会与你交互。 通常, COM库(COM library) 被称为“COM”,但是为了避免混淆,我在这里不这么做。
每种语言都有自己处理对象的方式。例如,在c++中,您可以在栈上创建它们,或者使用new动态地分配它们。因为COM必须是语言无关的,所以COM库提供了自己的对象管理例程。COM和c++对象管理的比较如下:
由此可见,对象的创建和销毁这两个阶段缺一不可。当你创建一个COM对象时,你告诉COM库你需要什么接口。如果对象被成功创建,COM库返回一个指向所请求接口的指针。然后,您可以通过该指针调用方法,就像它是一个普通c++对象的指针一样。
要创建COM对象并从该对象获取接口,需要调用COM库API CoCreateInstance() , 函数原型如下:
HRESULT CoCreateInstance (
REFCLSID rclsid,
LPUNKNOWN pUnkOuter,
DWORD dwClsContext,
REFIID riid,
LPVOID* ppv );
参数:
当您调用 CoCreateInstance() 时,它将在注册表中查找CLSID、读取服务器的位置、将服务器加载到内存中以及创建您请求的coclass的实例。
下面是一个示例调用,它实例化一个CLSID_ShellLink对象并请求一个指向该COM对象的IShellLink接口指针。
HRESULT hr;
IShellLink* pISL;
hr = CoCreateInstance ( CLSID_ShellLink, // CLSID of coclass
NULL, // not used - aggregation
CLSCTX_INPROC_SERVER, // type of server
IID_IShellLink, // IID of interface
(void**) &pISL ); // Pointer to our interface pointer
if ( SUCCEEDED ( hr ) )
{
// Call methods using pISL here.
}
else
{
// Couldn't create the COM object. hr holds the error code.
}
首先,我们声明一个 HRESULT 来保存来自 CoCreateInstance() 的返回值和一个 IShellLink 指针。我们调用 CoCreateInstance() 来创建一个新的COM对象。 如果 hr 持有指示成功的代码,则宏 SUCCEEDED 返回TRUE;如果 hr 持有指示失败的代码,则返回FALSE。有一个相应的宏 FAILED 用于测试失败代码。
如前所述,你不能释放COM对象,你只是告诉它们你已经用完了。每个COM对象实现的 IUnknown 接口都有一个 Release() 方法。调用此方法是为了告诉COM对象您不再需要它。一旦调用 Release() ,就不能再使用接口指针了,因为COM对象随时可能从内存中消失。
如果你的应用程序使用了很多不同的COM对象,那么在你使用完接口的时候调用 Release() 是非常重要的。 如果你不释放(release)接口,COM对象(和包含代码的dll)将保持在内存中,这会增加不必要的开销。如果您的应用程序将运行在很长一段时间里, 在你的程序空闲的时候你应该调用 CoFreeUnusedLibraries() API。这个API会卸载任何没有未完成引用的COM服务器,因此这也会减少应用程序的内存使用。
继续上面的例子,下面是使用Release()的方法:
// Create COM object as above. Then...
if ( SUCCEEDED ( hr ) )
{
// Call methods using pISL here.
// Tell the COM object that we're done with it.
pISL->Release();
}
IUnknown 接口将在下一节中详细解释。
每个COM接口都是从 IUnknown 派生出来的。这个名称有点误导人,因为它不是一个未知的接口。 这个名字意味着如果你有一个指向COM对象的 IUnknown 指针,你不知道底层的对象是什么, 因为每个COM对象都实现 IUnknown 。
IUnknown 有三个方法:
我们已经看到了 Release() 的作用,但是QueryInterface()呢?当您使用 CoCreateInstance() 创建COM对象时,您将返回一个接口指针。如果COM对象实现了多个接口(不包括IUnknown), 您可以使用 QueryInterface() 来获得您需要的任何其他接口指针。QueryInterface()的原型为:
HRESULT IUnknown::QueryInterface (
REFIID iid,
void** ppv );
参数说明:
让我们继续我们的shell链接示例。生成shell links的coclass实现了 IShellLink 和 IPersistFile 。如果你已经有了一个 IShellLink 指针 pISL ,你可以用如下代码从COM对象请求 IPersistFile 接口:
HRESULT hr;
IPersistFile* pIPF;
hr = pISL->QueryInterface ( IID_IPersistFile, (void**) &pIPF );
然后使用宏 SUCCEEDED 测试 hr ,以确定 QueryInterface() 是否工作。如果成功,就可以像使用其他接口一样使用新的接口指针 pIPF 。您还必须调用 pIPF->Release() 来告诉COM对象您已经使用完了这个接口。
我需要做一个暂时的弯路,并讨论如何处理字符串在COM代码。如果您熟悉Unicode和ANSI字符串的工作方式,并且知道如何在两者之间进行转换,那么您可以跳过这一节。否则,请继续阅读。
每当COM方法返回一个字符串时,该字符串将使用Unicode格式。(也就是说,所有写进COM规范的方法都是这样的)Unicode是一个像ASCII一样的字符编码方案,它所有字符只有2字节长。如果您希望将字符串转换为更易于管理的状态,那么应该将其转换为 TCHAR 。
TCHAR 和 _t 函数(例如, _tcscpy() )被设计用来让您使用相同的源代码处理Unicode和ANSI字符串。在大多数情况下, 您将编写使用ANSI字符串和ANSI Windows API的代码,因此在本文的其余部分中,为了简单起见,我将使用 chars 而不是 TCHARs 。不过,您一定要了解TCHAR类型, 以免在其他人编写的代码中遇到它们。
当你从一个COM方法得到一个Unicode字符串时,你可以用以下几种方法将它转换成一个 char 字符串:
您可以将Unicode字符串转化为ANSI字符串使用 WideCharToMultiByte() API。这个API原型如下:
int WideCharToMultiByte (
UINT CodePage,
DWORD dwFlags,
LPCWSTR lpWideCharStr,
int cchWideChar,
LPSTR lpMultiByteStr,
int cbMultiByte,
LPCSTR lpDefaultChar,
LPBOOL lpUsedDefaultChar );
参数说明如下:
有很多无聊的细节!像往常一样,文档让它看起来比实际要复杂得多。下面是一个如何使用API的例子:
// Assuming we already have a Unicode string wszSomeString...
char szANSIString [MAX_PATH];
WideCharToMultiByte ( CP_ACP, // ANSI code page
WC_COMPOSITECHECK, // Check for accented characters
wszSomeString, // Source Unicode string
-1, // -1 means string is zero-terminated
szANSIString, // Destination char string
sizeof(szANSIString), // Size of buffer
NULL, // No default character
NULL ); // Don't care about this flag
在此调用之后,szANSIString将包含Unicode字符串的ANSI版本。
CRT函数wcstombs()稍微简单一些,但它最终只是调用了WideCharToMultiByte(),因此最终结果是相同的。wcstombs()的原型为:
size_t wcstombs (
char* mbstr,
const wchar_t* wcstr,
size_t count );
参数如下:
wcstombs()在它对WideCharToMultiByte()的调用中使用WC_COMPOSITECHECK | WC_SEPCHARS标志。为了重用前面的例子,你可以用如下代码转换Unicode字符串:
wcstombs ( szANSIString, wszSomeString, sizeof(szANSIString) );
MFC CString类包含可以接受Unicode字符串的构造函数和赋值操作符,所以您可以让CString为您做转换工作。例如:
// Assuming we already have wszSomeString...
CString str1 ( wszSomeString ); // Convert with a constructor.
CString str2;
str2 = wszSomeString; // Convert with an assignment operator.
ATL有一组方便的宏来转换字符串。要将Unicode字符串转换为ANSI,可以使用W2A()宏(“wide To ANSI”的助记符)。实际上,更准确地说,应该使用OLE2A(),其中的“OLE”表示字符串来自COM或OLE源。无论如何,下面是如何使用这些宏的示例。
#include <atlconv.h>
// Again assuming we have wszSomeString...
{
char szANSIString [MAX_PATH];
USES_CONVERSION; // Declare local variable used by the macros.
lstrcpy ( szANSIString, OLE2A(wszSomeString) );
}
OLE2A()宏“返回”一个指向转换后字符串的指针,但是转换后的字符串存储在一个临时堆栈变量中,因此我们需要使用lstrcpy()对它进行自己的复制。您应该研究的其他宏有W2T() (Unicode to TCHAR)和W2CT() (Unicode string to const TCHAR string)。
有一个OLE2CA()宏(Unicode字符串到const char字符串),我们可以在上面的代码片段中使用它。对于这种情况,OLE2CA()实际上是正确的宏,因为lstrcpy()的第二个参数是const char*,但我不想一次抛出太多。
【这个我不知道怎么翻译,应该是控制台输出相关的部分内容】
另一方面,如果您不需要对字符串做任何复杂的操作,那么您可以在Unicode中保留该字符串。如果你在写一个控制台应用程序,你可以打印Unicode字符串与 std::wcout 的全局变量,例如:
wcout << wszSomeString;
但是请记住,wcout要求所有字符串都是Unicode的,所以如果您有任何“正常”字符串,您仍然需要使用std::cout输出它们。如果你有字符串,在它们前面加上L使它们成为Unicode,例如:
wcout << L"The Oracle says..." << endl << wszOracleResponse;
如果你在Unicode中保存一个字符串,有几个限制:
【因为我个人更加擅长使用Qt开发,因此充了这个部分】
下面是两个示例,演示了本文中涉及的COM概念。代码也包含在本文的示例项目中。
第一个示例展示了如何使用公开单个接口的COM对象。这是将是你遇到的最简单的例子。该代码使用包含在sheel中的Active Desktop coclass来检索当前壁纸的文件名。要使此代码工作,您需要安装Active Desktop。
涉及的步骤为:
WCHAR wszWallpaper [MAX_PATH];
CString strPath;
HRESULT hr;
IActiveDesktop* pIAD;
// 1. Initialize the COM library (make Windows load the DLLs). Normally you would
// call this in your InitInstance() or other startup code. In MFC apps, use
// AfxOleInit() instead.</FONT>
CoInitialize ( NULL );
// 2. Create a COM object, using the Active Desktop coclass provided by the shell.
// The 4th parameter tells COM what interface we want (IActiveDesktop).
hr = CoCreateInstance ( CLSID_ActiveDesktop,
NULL,
CLSCTX_INPROC_SERVER,
IID_IActiveDesktop,
(void**) &pIAD );
if ( SUCCEEDED(hr) )
{
// 3. If the COM object was created, call its GetWallpaper() method.
hr = pIAD->GetWallpaper ( wszWallpaper, MAX_PATH, 0 );
if ( SUCCEEDED(hr) )
{
// 4. If GetWallpaper() succeeded, print the filename it returned.
// Note that I'm using wcout to display the Unicode string wszWallpaper.
// wcout is the Unicode equivalent of cout.
wcout << L"Wallpaper path is:\n " << wszWallpaper << endl << endl;
}
else
{
cout << _T("GetWallpaper() failed.") << endl << endl;
}
// 5. Release the interface.
pIAD->Release();
}
else
{
cout << _T("CoCreateInstance() failed.") << endl << endl;
}
// 6. Uninit the COM library. In MFC apps, this is not necessary since MFC does
// it for us.
CoUninitialize();
在这个示例中,我使用 std::wcout 来显示Unicode字符串 wszWallpaper 。
第二个示例展示了如何对公开单个接口的COM对象使用 QueryInterface() 。代码使用包含在Shell中的Shell Link coclass为我们在上一个示例中检索到的墙纸文件创建快捷方式。
涉及的步骤有:
CString sWallpaper = wszWallpaper; // Convert the wallpaper path to ANSI
IShellLink* pISL;
IPersistFile* pIPF;
// 1. Initialize the COM library (make Windows load the DLLs). Normally you would
// call this in your InitInstance() or other startup code. In MFC apps, use
// AfxOleInit() instead.
CoInitialize ( NULL );
2. Create a COM object, using the Shell Link coclass provided by the shell.
// The 4th parameter tells COM what interface we want (IShellLink).
hr = CoCreateInstance ( CLSID_ShellLink,
NULL,
CLSCTX_INPROC_SERVER,
IID_IShellLink,
(void**) &pISL );
if ( SUCCEEDED(hr) )
{
// 3. Set the path of the shortcut's target (the wallpaper file).
hr = pISL->SetPath ( sWallpaper );
if ( SUCCEEDED(hr) )
{
// 4. Get a second interface (IPersistFile) from the COM object.
hr = pISL->QueryInterface ( IID_IPersistFile, (void**) &pIPF );
if ( SUCCEEDED(hr) )
{
// 5. Call the Save() method to save the shortcut to a file. The
// first parameter is a Unicode string.
hr = pIPF->Save ( L"C:\\wallpaper.lnk", FALSE );
// 6a. Release the IPersistFile interface.
pIPF->Release();
}
}
// 6b. Release the IShellLink interface.
pISL->Release();
}
// Printing of error messages omitted here.
// 7. Uninit the COM library. In MFC apps, this is not necessary since MFC
// does it for us.
CoUninitialize();
我已经展示了一些使用 SUCCEEDED 和 FAILED 宏的简单错误处理。现在, 我将提供一些关于如何处理从COM方法返回的 HRESULT s的更多细节。
HRESULT 是一个32位有符号整数,非负值表示成功,负值表示失败。 HRESULT 有三个位域:程度位(指示成功或失败)、功能码和状态码。“功能码”指示 HRESULT 来自哪个组件或程序。Microsoft将功能代码分配给各个组件,例如COM有一个,任务调度器有一个,等等。 “代码”是一个没有内在含义的16位字段;这些代码只是数字和含义之间的任意关联,就像 GetLastError() 返回的值一样。
如果您在 winerror.h 文件中查找错误代码,您会看到列出了许多 HRESULTs ,命名约定为[功能][程度][描述]。任何组件(如E_OUTOFMEMORY)都可以返回通用的 HRESULTs ,但在它们的名称中没有任何功能。例如:
幸运的是,有比查看 winerror.h 更容易的方法来确定 HRESULT 的含义。 内置工具的 HRESULTs 可以用错误查找工具查找。例如,假设您忘记在 CoCreateInstance() 之前调用 CoInitialize() 。 CoCreateInstance() 将返回一个值0x800401F0。您可以在错误查找中输入该值,然后您将看到描述: “ CoInitialize 未被调用。”
您还可以在调试器中查找 HRESULT 描述。如果您有一个名为hres的 HRESULT 变量,那么您可以通过输入“hres,hr”作为要监视的值来查看监视窗口中的描述。“,hr”告诉VC将值显示为 HRESULT 描述。