用标准C编写COM一COM in plain CPart1.docx
《用标准C编写COM一COM in plain CPart1.docx》由会员分享,可在线阅读,更多相关《用标准C编写COM一COM in plain CPart1.docx(27页珍藏版)》请在冰豆网上搜索。
![用标准C编写COM一COM in plain CPart1.docx](https://file1.bdocx.com/fileroot1/2023-1/5/a643bde8-17fb-42cb-89a0-c8fee556c70e/a643bde8-17fb-42cb-89a0-c8fee556c70e1.gif)
用标准C编写COM一COMinplainCPart1
内容:
∙ 简介
∙ COM对象和虚表
∙ GUID
∙ QueryInterface(),AddRef(),andRelease()
∙ IClassFactory对象
∙ 打包到DLL中
∙ C++/C包含文件
∙ 定义文件(DEF)
∙ 安装DLL,注册对象
∙ C实例程序
∙ C++实例程序
∙ 修改代码
∙ 接下来是什么?
简介:
有大量例子教大家如何使用和创建COM/OLE/ActiveX组件,但这些例子大都使用了微软基础类(MFC)、.NET、C#、WTL,至少会使用了ATL,因为这些框架会提供给你一些已经“封装” 好了的模板代码。
不幸的是,这些框架对程序员隐藏了所有底层细节,所以你永远不会真正明白使用COM组件的本质。
更好的掌握如何使用一个特定的框架是建立在你熟练掌握COM的基础上。
如果你正尝试不使用MFC、WTL、.NET、ATL、C#、或者甚至一些C++代码,只使用标准的C来处理COM对象,则这方面的例子和资料非常匮乏。
本文就是介绍在不使用其他框架,只使用标准C创建COM组件文章系列的第一部分。
对于标准的Win32控件,例如Static、Edit、ListBox、ComboBox等,你可以获得一个控件的句柄(也就是一个HWND)并通过发消息(通过SendMessage)给它来操纵它。
同时当这个控件要通知你一些事情或给你一些数据时,它也通过消息的形式返回给你(也就是通过把它们放入你自己的消息队列中,你再通过GetMessage获取他们)。
对于一个OLE/COM对象而言则完全不是这样。
你不能来回发消息,取而代之的是,COM对象给你一些可以调用来操纵这个对象的一些函数指针。
例如,一个IE浏览器对象会给你一个函数指针,通过这个指针你可以调用来引发浏览器在你的一个窗口中去加载并显示Web页面。
一个Office的对象会给你一个函数指针,你可以通过它加载一个文档。
如果COM对象要通知你一些事情或发给你一些数据,那么你需要在你的程序中写特殊的函数,提供这些函数的指针(给COM对象)以便对象在需要的时候调用它们。
换句话说你需要在你的程序中创建你自己的COM对象。
其中在C中真正麻烦是怎么定义你自己的COM对象。
为了这样做,你需要知道一个COM对象的每个细节-这些原始的东西在预制的框架中对你而言则是隐藏的,在接下来的一系列文章中我将展示它。
总之,你通过调用COM对象中的函数来操纵它,同时它在你的程序中也是通过函数调用来通知你一些事情或传给你一些数据或通过其他方式与你的程序交互。
这个方法类似于DLL中的函数调用,就像在你的C程序中DLL也要调用函数一样-有几分像“回调”。
但是与DLL不同的是,你不能用LoadLibrary()和GetProcAddress()去获得这个COM对象的函数指针。
我们马上就会揭示它,你需要使用一个与之不同的操作系统函数来获得一个对象指针,然后用这个对象去获得指向它的函数的指针。
COM对象和虚表
在学习怎样使用COM对象之前,我们首先需要了解一下COM对象是什么。
认识它的最好的方式是创建我们自己的COM对象。
但在我们这样做之前,让我们给出一个C结构数据类型。
作为一个C程序员,你应该对它相当熟悉。
这是一个例子的定义,一个简单的结构(叫“IExample”),它包含两个成员-一个DWORD(通过“count” 成员名来存取)和一个80个字符长度的数组(通过“buffer” 成员名来存取)。
[cpp] viewplaincopyprint?
1.struct IExample {
2. DWORD count;
3. char buffer[80];
4.};
让我们用typedef来使它可以提前使用:
[cpp] viewplaincopyprint?
1.typedef struct {
2. DWORD count;
3. char buffer[80];
4.}IExample;
接下来是一个对于这个结构分配一个实例的例子(忽略了错误检查),同时初始化它的成员:
[cpp] viewplaincopyprint?
1.IExample * example;
2.example = (IExample*)GlobalAlloc(GMEM_FIXED, sizeof(IExample));
3.example->count = 1;
4.example->buffer[0] =0;
你知道一个结构可以存储一个指向函数的指针嘛?
希望你知道,这是个例子。
我们有一个参数是char*的函数,返回值是个long。
这是我们的函数:
[cpp] viewplaincopyprint?
1.long SetString(char *str)
2.{
3. return(0);
4.}
现在我们需要把这个指向这个函数的指针存储在IExample中。
在这里我们定义IExample,添加一个成员(“SetString”)来存储指向上面的函数的指针(并且我也用了一个typedef来使它提前可用):
[cpp] viewplaincopyprint?
1.typedef long SetStringPtr(char *);
2.typedef struct {
3. SetStringPtr * SetString;
4. DWORD count;
5. char buffer[80];
6.} IExample;
7.
接下来是我们在分配的IExample中给SetString指针赋值,然后用这个指针调用来调用SetString:
[cpp] viewplaincopyprint?
1.example->SetString =SetString;
2.long value =example->SetString("Some text");
好,可能我们需要存储两个函数指针。
这是第二个函数:
[cpp] viewplaincopyprint?
1.long GetString(char *buffer, long length)
2.{
3. return(0);
4.}
让我们重新定义IExample,添加另一个函数成员(“GetString”)来存储指向第二个函数的指针:
[cpp] viewplaincopyprint?
1.typedef long GetStringPtr(char *, long);
2.typedef struct {
3. SetStringPtr * SetString;
4. GetStringPtr * GetString;
5. DWORD count;
6. char buffer[80];
7.} IExample;
8.
接下来我们初始化这个成员:
[cpp] viewplaincopyprint?
1.example->GetString= GetString;
但你可能会说我不想把函数指针直接存储在IExample中。
相反的,我们更愿意使用一个函数指针数组。
例如,让我们定义第二个结构来达到存储我们的两个函数指针的目的。
我们将叫它IExampleVtbl结构,它的定义是这样的:
[cpp] viewplaincopyprint?
1.typedef struct {
2. SetStringPtr * SetString;
3. GetStringPtr * GetString;
4.} IExampleVtbl;
5.
现在,我们把指向上面的数组的指针存储在IExample中。
我们要添加一个叫“lpVtbl”的新成员来达到这个目的(当然,我们得删除SetString和GetString成员,因为他们已经挪到IExampleVtbl结构中了)
[cpp] viewplaincopyprint?
1.typedef struct {
2. IExampleVtbl * lpVtbl;
3. DWORD count;
4. char buffer[80];
5.} IExample;
6.
所以下面是一个分配内存并初始化IExample的例子(当然,包括IExampleVtbl):
[cpp] viewplaincopyprint?
1.// 由于IExample_Vtbl的内容永远不会改变,
2.// 所以我把它定义为静态的并且用以下方法初始化它。
3.// 它可以被大量的IExample实例复制。
4.static const IExampleVtbl IExample_Vtbl = {SetString, GetString};
5.IExample * example;
6.// 创建 (分配内存) 一个IExample结构.
7.example = (IExample*)GlobalAlloc(GMEM_FIXED, sizeof(IExample));
8.// 初始化IExample(也就是把指向IExample_Vtbl赋值给它).
9.example->lpVtbl =&IExample_Vtbl;
10.example->count = 1;
11.example->buffer[0] =0;
接着可以这样调用我们的函数:
[cpp] viewplaincopyprint?
1.char buffer[80];
2.example->lpVtbl->SetString("Sometext");
3.example->lpVtbl->GetString(buffer,sizeof(buffer));
此外需要说明的是,在我们的函数中可能需要通过访问结构中的“count”和“buffer”成员来调用他们。
所以我们要做的是总要把指向这个结构的指针作为第一个参数传入。
让我们重写我们的函数来达到这一点:
[cpp] viewplaincopyprint?
1.typedef long SetStringPtr(IExample *, char *);
2.typedef long GetStringPtr(IExample *, char *, long);
3.long SetString(IExample *this, char * str)
4.{
5. DWORD i;
6. // 把传入的str拷贝到IExample的buffer中
7. i = lstrlen(str);
8. if (i > 79) i = 79;
9. CopyMemory(this->buffer, str, i);
10. this->buffer[i] = 0;
11. return(0);
12.}
13.long GetString(IExample*this, char *buffer, long length)
14.{
15. DWORD i;
16. // 拷贝IExample的buffer到传入的buffer中
17. i = lstrlen(this->buffer);
18. --length;
19. if (i > length) i = length;
20. CopyMemory(buffer, this->buffer, i);
21. buffer[i] = 0;
22. return(0);
23.}
当调用IExample结构的函数时把它的结构指针传入:
[cpp] viewplaincopyprint?
1.example->lpVtbl->SetString(example,"Some text");
2.example->lpVtbl->GetString(example,buffer, sizeof(buffer));
如果你曾经用过C++,你可能认为:
等一下,它好像很眼熟啊。
是的,我们上边做的就是用标准C来创建一个C++类。
IExample结构实际上是一个C++类(一个不继承于其他任何类的类)。
一个C++类实际上除了第一个成员总是一个数组指针,这个数组包含所有类成员函数的指针,与结构没什么差别。
并且每个函数的第一个参数总是类(也就是结构)本身的指针。
(它也就是隐藏的this指针)
简单说来,一个COM对象实际上就是一个C++类。
你现在可能会认为:
“哇噻!
IExample现在就是一个COM对象嘛?
这就是它的全部嘛?
?
它就这么简单!
”打住!
IExample正在接近这一点,但对于它还有很多,它不会这么容易。
如果它是这样,它就不会是微软技术了,现在做什么?
首先,让我先来介绍一下COM术语。
你看到上面的指针数组-IExampleVtbl结构了嘛?
COM文档中把它定义为接口或虚表。
一个COM对象在虚表(也就是我们的IExampleVtbl结构)中首先需要有三个被命名为QueryInterface、AddRef和Release的函数。
当然,我们也必须写这三个函数。
微软已经把这三个函数的调用参数,返回值和调用约定指定好了。
我们需要#include一些微软的包含文件(他们在你的C编译器包中,或者你下载的微软的SDK中)。
我们这样重新定义我们的IExampleVtbl结构:
[cpp] viewplaincopyprint?
1.#include
2.#include
3.#include
4.typedef HRESULT STDMETHODCALLTYPE QueryInterfacePtr(IExample *, REFIID, void **);
5.typedef ULONG STDMETHODCALLTYPE AddRefPtr(IExample *);
6.typedef ULONG STDMETHODCALLTYPE ReleasePtr(IExample *);
7.typedef struct {
8. // 前3个成员必须叫是QuryInterface、AddRef和Release
9. QueryInterfacePtr *QueryInterface;
10. AddRefPtr *AddRef;
11. ReleasePtr *Release;
12. SetStringPtr *SetString;
13. GetStringPtr *GetString;
14.} IExampleVtbl;
让我们查看typedef过的QueryInterface。
首先,这个函数返回一个HRESULT,它被简单定义为LONG。
接着,它用了STDMETHODCALLTYPE。
这意味参数不通过寄存器传递,而是通过栈。
并且也约定了谁来平栈。
事实上,对于COM对象,我们应该确保所有我们的函数都被定义为STDMETHODCALLTYPE,并返回一个LONG(HRESULT)。
QueryInterface的第一个参数是用于函数调用的对象指针。
我们难道不是在把IExample转化为一个COM对象嘛?
是的,这也是我们要传递的参数的原因。
(记住确保传递给我们函数的第一个参数是一个用于调用这些函数的结构指针?
COM完全强制依赖以上的定义)
稍后,我们展示一个REFIID是什么,并且也提到QueryInterface的第三个参数,注意AddRef和Release也传递同样的我们用于调用他们的结构指针。
好,在我们没有忘记前,让我们添加HRESULTSTDMETHODCALLTYPE到SetString和GetString:
[cpp] viewplaincopyprint?
1.typedef HRESULT STDMETHODCALLTYPE SetStringPtr(IExample *, char *);
2.typedef HRESULT STDMETHODCALLTYPE GetStringPtr(IExample *, char *, long);
3.HRESULT STDMETHODCALLTYPE SetString(IExample *this, char * str)
4.{ ...
5. return(0);
6.}
7.HRESULT STDMETHODCALLTYPE GetString(IExample *this, char *buffer, long value)
8.{
9. ...
10. return(0);
11.}
总之,一个COM对象基本上是一个C++类。
这个C++类是一个总是以它的虚表指针(一个函数指针数组)为起点的结构。
并且在虚表中最开始的三个函数总是被命名为QueryInterface、AddRef和Release。
额外的函数也可以出现在虚表中,它们的名字依赖对象它自身的定义。
(你决定要加入你的COM对象中的函数)。
例如,IE的Browser对象勿庸置疑有与播放音乐对象不同的函数。
但是所有的COM对象都以它们的虚表指针开始,最开始的三个虚表指针指向对象的QueryInterface、AddRef、和Release函数。
一个对象的函数的第一个参数是一个指向对象(结构)自身的指针。
这是一个约定,一定要遵守。
GUID
让我们继续我们的构造IExample为一个真正的COM对象之旅。
现在要写我们的QueryInterface、AddRef和Release函数。
但在我们动手之前,我们必须谈谈一个叫全局唯一表示符(GUID)的东东。
哦,它是什么?
它是一个用特殊的一连串字节填充的16字节数组。
当我说它是特殊的时候,我的意思是唯一。
一个GUID(也就是16字节数组)不能与另一个GUID有同样的字节序列,无论何时何地。
每个GUID在任何时候被创建都有唯一的16位序列数。
那么你怎样创建这个唯一的16位序列呢?
你可以用一个微软的GUIDGEN.EXE工具。
它打包在你的编译器中,或者你也可以在SDK找到它。
运行它你会看到这个窗口:
当你一运行GUIDGEN时,它自动生成一个新的GUID给你,显示在Result框中。
注意在你的Result框中看到的会与上面的有所不同。
毕竟,每个单一的GUID生成与其他的是不同的。
所以你最好看到一些与我看到的不同的东东。
继续单击“NewGUID”按钮会看到一些不同的数字出现在Result框中。
单击一整天,看看是否会生成同一个序列数超过一次,不会。
同时,也没人会生成一些与你生成的序列相同的数。
你可以单击“Copy”按钮来把这个信息传输到剪切板上,然后把它粘贴到其它地方(像你的源代码中)。
这是我这样做,粘贴完的东东:
[cpp] viewplaincopyprint?
1.//{0B5B3D8E-574C-4fa3-9010-25B8E4CE24C2}
2.DEFINE_GUID(<>,0xb5b3d8e, 0x574c, 0x4fa3,
3. 0x90, 0x10, 0x25, 0xb8, 0xe4, 0xce,0x24, 0xc2);
上面是一个宏,一个#define在微软的包含文件中,它会告诉你的编译器把上面的内容编译成一个16位数组。
但是有一个事情我们必须做。
我们必须用一些我们要用的这个GUID的C变量名来替换<>。
我们叫它CLSID_IExample.
[cpp] viewplaincopyprint?
1.//{0B5B3D8E-574C-4fa3-9010-25B8E4CE24C2}
2.DEFINE_GUID(CLSID_IExample,0xb5b3d8e, 0x574c, 0x4fa3,
3. 0x90, 0x10, 0x25, 0xb8, 0xe4, 0xce, 0x24,0xc2);
现在我们有了一个可以用于IExample的GUID。
我们还需要一个GUID给IExample的虚表(“接口”),也就是,我们的IExampleVtble结构。
所以继续单击GUIDGEN.EXE的“NewGUID”按钮,并拷贝、粘贴到其他地方。
这次,我们将用一个命名为IID_IExample的C变量名来替换<>。
下面是我粘贴、编辑过的结果:
[cpp] viewplaincopyprint?
1.//{74666CAC-C2B1-4fa8-A049-97F3214802F0}
2.DEFINE_GUID(IID_IExample,0x74666cac, 0xc2b1, 0x4fa8,
3.0xa0, 0x49, 0x97, 0xf3,0x21, 0x48, 0x2, 0xf0);
总之,每个COM对象有它自己GUID,每个GUID是由不同的16位字节数组组成。
一个GUID可以通过GUIDGEN.EXE工具生成。
一个COM对象的虚表(也就是接口)也得有一个GUID。
QueryInterface(),AddRef(),andRelease()
当然我们要允许其他程序来获得我们创建、初始化的IExample结构(也就是一个COM对象),那么这个程序就可以调用我们的函数了。
(我们先不给出另一个程序怎样来获得我们的IExample。
我们将在后面讨论它)。
除我们自己的COM对象以外,可能有很多其他COM组件安装在一个特定的计算机上。
(再次,我们将推后讨论怎样安装我们的COM组件。
)不同的计算机可能安装了不同的COM组件。
一个程序怎样确定我们的IExampl