大家好,今天小编来为大家解答深入解析:COM技术基础入门篇(第一部分)这个问题,很多人还不知道,现在让我们一起来看看吧!
介绍
COM(组件对象模型)组件对象模型在这段时间的Windows世界中随处可见。每天都会出现大量基于COM 的文章。这些文章抛出了很多术语,如COM 对象、接口、服务器等,但这些文章假设您已经了解COM 并知道如何使用它。
本文针对初学者,由浅入深地介绍COM的底层机制,并教你如何在程序中使用第三方COM对象(以Windows Shell为例)。了解了本文的内容后,您将能够使用Windows内置的COM对象和第三方提供的COM对象。
本文假设您熟悉C++。我在示例代码中使用了一些MFC和ATL代码。如果你不了解这两种技术也没关系。我会在这些地方详细解释。本文分为以下几章:
什么是 COM简单介绍一下COM标准以及COM的出现解决了哪些问题。您无需了解这一部分即可使用COM。但我仍然建议您阅读本章,以便您能够理解为什么COM 中的事物是这样编写的。
基本概念COM 术语及其相应含义。
使用 COM 对象简单介绍如何创建、使用和销毁COM对象。
基本接口-IUnknown介绍基本接口IUnknown,以及该接口中的功能。
注意-字符串处理描述如何处理COM 代码中的字符串。
知识点整合-实例代码使用两个代码示例来说明本文提到的每个概念。
处理 HRESULT介绍HRESULT 类型以及如何测试错误代码。
引用介绍一些值得一读的书籍。
什么是 COM
简单地说,COM 是一种在不同程序和编程语言之间共享二进制代码的方式。这与C++提倡源代码级共享不同。 ATL 是一个很好的例子,源代码共享很好,但仅限于C++。它还带来了命名空间冲突的可能性,更不用说不断复制和重用代码而导致的项目膨胀。
Windows 使用DLL 进行二进制代码共享。 Windows应用程序通过重用kernel32.dll、user32.dll来运行。但这些DLL都是用C编写的,只有符合C调用规则的语言才能使用它们。这给其他语言重用dll造成了负担。
MFC 提供了一种称为MFC 扩展DLL 的机制来共享二进制代码,但它的限制性更强。通过这种机制创建的DLL只能在MFC程序之间共享。
COM通过定义二进制标准解决了上述问题。也就是说,COM规定其二进制模块(dll和exe)必须编译成与指定结构相匹配的格式。该标准还指定了COM 对象在内存中的组织方式。并且它独立于任何编程语言功能(例如C++ 命名空间)。一旦建立了上述规定,任何编程语言都可以轻松访问二进制模块。该标准将二进制共享所需的额外工作放在编译器上(而不是dll 本身)。只要编程语言的编译器产生的二进制代码与标准兼容,其他人就可以轻松使用。
内存中COM 对象的结构“恰好”与C++ 虚函数使用的结构相似。这就是为什么很多COM 代码都是用C++ 编写的。但请记住,COM 组件用哪种语言编写并不重要,因为任何语言都可以使用它。
顺便说一下,COM并不局限于Windows平台。理论上它可以移植到Unix 或其他操作系统。但我从未见过在其他系统上讨论COM。
基本概念
我们将从下往上,一一介绍COM中的基本概念。
接口代表COM中的一组功能。这些函数称为方法。接口以I 开头,例如IShellLink。在C++中,接口通常被编写为仅具有纯虚函数的抽象基类。与C++类似,COM接口也可以从其他接口继承。 COM 接口继承的工作方式与C++ 单继承类似。它不允许多重继承。
coclass(组件对象类组件对象类)包含在DLL 或EXE 中。它包含一个或多个接口的代码。这些接口的组件类也称为实现。这里再次提醒大家,COM中的“类”与C++中的“类”并不相同,尽管在实际工作中,通常使用C++类来编写COM类代码。
COM Object(COM对象) 在内存中,COM对象是一个coclass实例。 COM 对象中可能有一个或多个接口。
COM 服务器(COM 服务)是一个包含一个或多个组件类的二进制模块(DLL 或EXE)。
注册是创建注册表项的过程,注册表项告诉Windows 如何定位COM 服务器。另一方面,取消注册会从Windows 中删除注册表项。
GUID(全局唯一标识符)是一个128位的数字。 GUID是COM提供的一种独立于编程语言的标识方法。每个接口和组件类都对应一个GUID。由于GUID是全局唯一的,因此可以避免名称冲突(只要是使用COM API创建的,就不会出现名称冲突)。有时你会看到另一个UUID(通用唯一标识符),两者具有相同的功能。
类ID 或CLSID 表示组件类的GUID。接口ID或IID表示接口的GUID。
GUID 在COM 中如此广泛使用的原因有两个:
GUID 只是一串数字,任何编程语言都可以处理它。
GUID 一旦创建,对于任何人和任何机器来说都是唯一的。因此,COM 开发人员可以创建自己的GUID,而不必担心与其他人的GUID 发生冲突。这避免了集中发布GUID 的麻烦。
HRESULT 是COM 中用于返回错误代码的整数值。虽然它以“H”开头,但它不是任何对象的“句柄”。 HRESULT 将在以下章节中更详细地描述。
COM 库是您在执行COM 相关操作时使用的操作系统的一部分。通常COM库被称为“COM”,但为了避免混淆,这里不使用这个名称。
使用 COM 对象
每种编程语言都有其处理对象的方式。例如,在C++ 中,您可以在堆栈上创建对象或使用new 在堆上动态分配对象。由于COM 必须与语言无关,因此COM 库提供了自己的对象管理方法。下面列出了COM 和C++ 对象管理之间的差异:
创建对象C++,使用new操作符或者直接在栈上创建; COM,调用COM库中的API;销毁对象C++,使用删除运算符或超出范围时自动销毁堆栈上的对象; COM中,所有对象都保存自己的引用计数,当用户使用完COM对象时,必须告诉COM对象递减引用计数。当COM 对象引用计数递减至0 时,COM 对象将从内存中销毁。使用对象创建COM对象后,需要使用该对象。 COM 对象中可能有一个或多个方法。当你想使用一个方法时,你必须告诉COM库你需要哪个接口。如果COM 对象已成功创建,COM 库将返回指向所需接口的指针。你可以使用这个指针来调用这个方法,就像调用C++对象的接口一样。
创建 COM 对象
当需要创建COM对象并获取对象中的接口指针时,可以调用COM API CoCreateInstance()。该函数的原型如下:
HRESULT CoCreateInstance(
REFCLSID rclsid,
LPUNKNOWN punkOuter,
DWORD dwClsContext,
瑞德瑞德,
LPVOID* ppv );参数含义如下:
rclsid:
组件类的CLSID。例如,您可以将CLSID_ShellLink 传递给此参数来创建可用于创建快捷键的COM 对象。
朋克外衣:
该参数仅用于COM对象的聚合。它可用于向现有组件类添加新方法。我们这里只需要传入NULL即可表示不需要聚合。
dwClsContext
指定您需要哪种类型的COM 服务器。本文仅使用最简单的服务器,即进程内DLL,因此传入CLSCTX_INPROC_SERVER。注意,这里不要使用CLSCTX_ALL(这是ATL 的默认值),因为它会在未安装DCOM 的Windows 95 上导致错误。
里德:
您希望返回的接口的IID。例如,您可以将IID_IShellLink 传递给此参数以获取IShellLink 接口。
ppv:
接口指针的地址。 COM库将通过该参数返回所请求的接口。
当您调用CoCreateInstance() 函数时,它会在注册表中查找CLSID,找到COM 服务器的位置,将服务器加载到内存中,并创建您请求的组件类的实例。
下面的代码给出了一个简单的例子。它实例化一个CLSID_ShellLink 对象并请求一个指向COM 对象的IShellLink 接口:
HRESULT 小时;
IShellLink* pISL;
hr=CoCreateInstance( CLSID_ShellLink,
无效的,
CLSCTX_INPROC_SERVER,
IID_IShellLink,
(无效**)pISL);
如果(成功(小时))
{
//创建COM对象成功,使用pISL调用接口
}
别的
{
//无法创建COM对象,错误代码存在于hr中
}
销毁 COM 对象
正如前面提到的,您不需要手动从内存中销毁COM对象,您只需告诉它您不再需要它。所有COM 类都继承自IUnKnown 接口。该接口提供了一个名为Release()的函数。调用此函数告诉COM 对象您不再需要它。一旦调用Release(),就无法继续使用相应的接口,因为COM对象可能已从内存中销毁。
如果您的程序使用许多不同的COM 对象,那么当您不再需要使用该接口时调用Release() 非常重要。如果不释放这些接口,COM对象及其对应的dll将一直存在于内存中,这会增加不必要的开销。如果您的程序运行时间较长,您可以在空闲期间调用CoFreeUnusedLibraries() API。此API 将卸载任何未显式调用的COM 服务器。这样做可以在一定程度上减少内存的使用。
以下示例代码演示了如何使用Release():
if(成功(小时))
{
//使用pISL接口执行一些操作
//使用后,告诉COM对象不再需要这个接口。
pISL-释放();
}IUnKnown接口将在下一节详细介绍。
基本接口-IUnknown
每个COM 接口都继承自IUnknown 接口。 IUnknown 这个名字有点误导,它并不意味着“未知”的接口。之所以这样命名,是因为如果有一个IUnknown 指针指向COM 对象,则您不知道底层对象是什么,因为每个COM 对象都实现了IUnknown 接口。
IUnknown接口具有三个功能:
AddRef() 该接口告诉COM 对象增加其引用计数。 Release() 该接口告诉COM 对象减少引用计数。 QueryInterface() 向COM 对象请求接口指针。当一个组件类实现了多个接口时,需要使用该函数获取指定的接口。 QueryInterface()的函数原型如下:
HRESULT IUnknown:QueryInterface {
瑞菲德iid,
void** ppv );参数含义如下:
独立同分布
您请求的接口的IID
PPV
接口指针地址。如果QueryInterface()调用成功,接口将通过该参数传出。
在前面的例子中,我们通过CoCreateInstance()获得了IShellLink接口的指针pISL。我们还可以使用pISL 和QueryInterface() 获取COM 对象中的其他接口指针:
HRESULT 小时;
IPersistFile* pIPF;
hr=pISL-QueryInterface(IID_IPersistFile, (void**)pIPF);可以使用SUCCEEDED宏来检测接口指针是否成功获取。使用pIPF 时,还必须像pISL 一样调用Release() 来减少引用计数。
注意-字符串处理
本节将绕道讨论如何处理COM 代码中的字符串。如果您熟悉Unicode 和ANSI 字符串的工作原理,并且知道如何在两种编码之间进行转换,则可以跳过本章。
每当COM 函数返回一个字符串时,它都会以Unicode 进行编码。 Unicode 是一种字符编码集,与ANSI 类似,只不过Unicode 中的所有字符都是两个字节。如果您想更好地操作字符串,可以将其转换为TCHAR 类型。
TCHAR 和以_t 开头的函数(例如_tcscpy)旨在使用相同的代码处理Unicode 和ANSI 字符串。在大多数情况下,您将使用ANSI 字符串和ANSI API,因此为了简单起见,我将在以后的文章中使用char 而不是TCHAR。但你还是应该精通TCHAR 类型。
从COM 函数获取字符串后,可以使用以下函数将其转换为char 类型:
调用WideCharToMultiByte();调用CRT 函数wcstombs();可以使用MFC中的CString构造函数进行转换;使用ATL字符串转换宏;下面对这些方法进行详细介绍:
WideCharToMultiByte()
该函数的原型如下:
int WideCharToMultiByte(
UINT 代码页,
DWORD dwFlags,
LPCWSTR lpWideCharStr,
int cchWideChar,
LPSTR lpMultiByteStr,
int cbMultiByte,
LPCSTR lpDefaultChar,
LPBOOL lpUserDefaultChar );代码页
Unicode 字符转换为的代码页。您可以传入CP_ACP 将Unicode 转换为当前系统使用的ANSI 代码页。代码页是256个字符集,其中0~127与ANSI相同,128~255不同。它可以包含图形字符或语音符号。每种语言都有自己的代码页,因此使用正确的代码页非常重要,这样才能正确显示字符。
dw标志
该标志决定该函数如何处理“复合”Unicode 字符串。该复合字符串后面会跟一个发音符号,例如。如果这个符号存在于指定的代码页中,则没有问题,但如果不存在,Windows必须将这个符号转换成其他形式来显示。
将WC_COMPOSITECHECK 传递给dwFlags,然后API 将检测“复合字符串”;
将WC_SEPCHARS 传入dwFlags,则API 会将字符划分为“字符+发音符号”的形式,如 --e`
如果WC_DISCARDNS 传递给dwFlags,API 将丢弃发音符号;
将WC_DEFAULTCHAR 传递给dwFlags,则API 会将发音符号替换为默认符号,默认符号可以在lpDefaultChar 中指定。
dwFlags 的默认值为WC_SEPCHARS。
lpWideCharStr
要转换的Unicode 字符串。
宽字符
lpWideCharStr 字符串的长度。如果传入-1,则会自动检查00结尾,以确认长度。
lpMultiByteStr
char类型的字符串缓冲区,用于接收转换后的ANSI字符串;
cb多字节
lpMultiByteStr 缓冲区的长度(以字节为单位)。
lp默认字符
可选参数,当WC_COMPOSITECHECK 中传递dwFlags 时| WC_DEFAULTCHAR,如果API 检查目标代码页中不存在某个字符,则将使用默认字符来替换该字符。如果该参数传入NULL,API将使用系统默认字符(通常是问号)来替换它。
lpUserDefaultChar
可选参数是指向BOOL 值的指针。如果lpDefaultChar 被插入到目标字符串中,则BOOL 值将被设置为TRUE 以对其进行标记。
这个函数比较复杂,举个例子:
char szANSIString [MAX_PATH];
WideCharToMultiByte( CP_ACP, //使用系统当前代码页
WC_COMPOSITECHECK, //检查复合字符
wszSomeString, //要转换的Unicode 字符串
-1, //自动检查Unicode字符串长度
szANSIString, //ANSI 字符串缓冲区
sizeof(szANSIString), //缓冲区长度
NULL, //使用系统默认字符替换复合字符
无效的); //不检查是否替换
wcstombs()
CRT函数wcstombs() 就简单多了,但最终还是调用了WideCharToMultiByte()。
size_t wcstombs(
字符* mbstr,
常量wchar_t* wcstr,
size_t 计数);mbstr
转换后的ANSI字符串存储在该缓冲区中;
西斯特
要转换的Unicode 字符串;
数数
mbstr 的字符串长度,以字节为单位。
wcstombs() 使用WC_COMPOSITECHECK | WC_SEPCHARS 标志。您可以按如下方式调用wcstombs():
wcstombs(szANSIString, wszSomeString, sizeof(szANSIString));
CString
MFC中的CString在构造函数或赋值运算符中可以接受Unicode字符串,可用于转换:
CString str1(wszSomeString);
CString str2;
str2=wszSomeString;
ATL 字符串转换宏
ATL 提供了一组用于转换字符串的宏:
W2A()(Wide To ANSI)用于将Unicode转换为ANSI; OLE2A()(OLE或COM String To ANSI)与上面的宏功能相同,但描述更精确。 "OLE" 清楚地表明它是COM 字符串。 W2T() (Wide To TCHAR) 将Unicode 转换为TCHAR;W2CT() (Wide To const TCHAR)OLE2CA() (OLE String To const char String) 以下是示例:
char szANSIString[MAX_PATH];
使用转换; //声明OLE2A 宏所需的局部变量
lstrcpy(szANSIString, OLE2A(wszSomeString); 之所以需要调用lstrcpy将OLE2A()返回的结果复制到szANSIString是因为OLE2A()返回的结果是暂时存放在栈中的,只有在之后才能永久使用被复制。
知识点整合-实例代码
下面用两个例子来说明本文提到的每个概念。
使用单接口的 COM 对象
以下示例在shell 中使用Active Desktop 组件类来获取当前壁纸的路径。
#include#include#include#includeint main()
{
setlocale(LC_ALL, "chs");
HRESULT 小时;
IActiveDesktop* pIAD;
WCHAR wszWallpaper[MAX_PATH];
//1. 初始化COM库
CoInitialize(NULL);
//2. 创建COM对象实例
hr=CoCreateInstance(CLSID_ActiveDesktop,
NULL,CLSCTX_INPROC_SERVER,IID_IActiveDesktop,(void **)pIAD);
如果(成功(小时))
{
//3.调用GetWallpaper函数
hr=pIAD-GetWallpaper(wszWallpaper, MAX_PATH, 0);
如果(成功(小时))
{
wprintf(L"壁纸路径=%sn", wszWallpaper);
}
别的
{
wprintf(L"GetWallpaper Er
ror! n"); } // 4. 释放接口 pIAD->Release(); } else { wprintf(L"CoCreateInstance Error! n"); } // 5. 反初始化 COM library CoUninitialize(); return 0; }上面代码中的 setlocale() 是为了让 wprintf() 能够在控制台中正确输出中文。使用多接口的 COM 对象
下面的例子展示了使用 QueryInterface() 获取接口的方法。它先创建 ShellLink COM 对象,拿到它的 IShellLink 接口,然后调用 QueryInterface() 获取 IPersistFile 接口。 这个例子的功能是创建壁纸文件的快捷方式。 #include#include#includeint main() { WCHAR wszWallpaper[MAX_PATH] = L"C:\Users\Dongyu\Pictures\桌面黑.png"; // 1. 初始化 COM library CoInitialize(NULL); HRESULT hr; IShellLink* pISL; IPersistFile* pIPF; // 2. 创建 ShellLink COM 对象实例 hr = CoCreateInstance(CLSID_ShellLink, NULL, CLSCTX_INPROC_SERVER, IID_IShellLink, (void**)&pISL); if ( SUCCEEDED(hr) ) { // 3. 设定目标文件的路径 hr = pISL->SetPath(wszWallpaper); if ( SUCCEEDED(hr) ) { // 4. 获取第二个 COM 接口 hr = pISL->QueryInterface(IID_IPersistFile, (void**)&pIPF); if ( SUCCEEDED(hr) ) { // 5. 调用 Save 方法保存壁纸的快捷方式 hr = pIPF->Save(L"E:\wallpaper.lnk", FALSE); // 6. 用完了,释放 pIPF->Release(); } } // 7. 用完了,释放 pISL->Release(); } // 8. 反初始化 COM library CoUninitialize(); return 0; }处理 HRESULT
之前的例子中,用 SUCCEEDED, FAILED 宏简单处理了 HRESULT. 下面我要介绍一些关于 HRESULT 更多的细节。 HRESULT 返回值是一个 32 位有符号整形。非负数表示成功,负数表示失败。 HRESULT 有三个域,[结果位],[功能码],[状态码]。结果位表示结果是成功还是失败;功能码表示错误来自于哪个组件,如 COM, 任务调度程序都有对应的功能码。功能码是一个 16 位的值,没有其它内在含义,这个数字和意义之间是没有关联的,类似于 GetLastError() 的返回值。 如果你去查看 winerror.h, 你会看到一堆 HRESULT 的定义。它们命名的规则是: [功能][结果][描述],如果 HRESULT 不属于任何特定组件,那么 [功能] 这一项不写: REGDB_E_READREGDB: 功能=REGDB(registry database), 结果=Error, 描述=READREGDB(表示无法读取数据库);S_OK: 功能=通用, 结果=Success, 描述=OK(表示没啥问题);除了直接查看 winerror.h,你可以用 Error lookup tool 来了解 HRESULT 的具体意义。 你可以在调试时的监视窗口中使用 @err.hr 来查看 HRESULT 所代表的具体意义。【深入解析:COM技术基础入门篇(第一部分)】相关文章:
2.米颠拜石
3.王羲之临池学书
8.郑板桥轶事十则
用户评论
终于看到这个了!我一直想了解一下 COM 是什么
有20位网友表示赞同!
期待学习更多关于COM 的知识,Part1 能让我打个基础
有19位网友表示赞同!
之前一直不知道怎么使用 COM,这篇文章能解决我的疑惑吗?
有18位网友表示赞同!
希望这篇文章详细讲解 COM 的原理和应用场景
有9位网友表示赞同!
了解一下 COM 可以拓展我的编程能力,期待学习!
有20位网友表示赞同!
Part1 是什么内容呢?是不是介绍了基础概念?
有6位网友表示赞同!
希望能用通俗易懂的语言解释 COM
有17位网友表示赞同!
希望这篇文章包含一些实际例子,方便理解
有9位网友表示赞同!
COM 在软件开发中扮演什么角色? Part1 能告诉我吗?
有16位网友表示赞同!
我对 COM 领域不太了解,这篇文章能给我一个入门指南吗?
有9位网友表示赞同!
什么时候可以看 Part2 呢?期待继续学习 COM
有16位网友表示赞同!
以前听说过 COM,但一直没认真了解过,这次正好趁机学习一下
有18位网友表示赞同!
COM 在不同的编程语言中都有运用吗?这篇文章会介绍吗?
有9位网友表示赞同!
Part1 会介绍 COM 的历史发展吗?我很想了解它的由来
有5位网友表示赞同!
如果理解了 Part1,能做什么样的开发项目呢?
有11位网友表示赞同!
COM 能解决哪些实际问题?希望能在这篇文章中找到答案
有18位网友表示赞同!
期待这篇文章提供一些学习资料和资源
有12位网友表示赞同!
希望作者以后能够继续推出 COM 的后续文章
有13位网友表示赞同!
对于初学者来说,Part1 是一个很好的起点
有15位网友表示赞同!