65.9K
CodeProject 正在变化。 阅读更多。
Home

纯 C 语言中的 COM

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.98/5 (495投票s)

2006年3月29日

CPOL

34分钟阅读

viewsIcon

1252742

downloadIcon

10832

如何在纯 C 中创建/使用 COM 组件,无需 MFC、ATL、WTL 或任何其他框架。

目录

引言

有大量的示例演示了如何使用/创建 COM/OLE/ActiveX 组件。但这些示例通常使用 Microsoft 基础类 (MFC)、.NET、C#、WTL,或者至少是 ATL,因为这些框架提供了预制的“包装器”来为您提供一些样板代码。不幸的是,这些框架往往会隐藏程序员的所有底层细节,所以您永远不会真正学会如何使用 COM 组件本身。相反,您学会的是如何使用一个建立在 COM 之上的特定框架。

如果您尝试使用纯 C,不使用 MFC、WTL、.NET、ATL、C#,甚至不使用任何 C++ 代码,那么关于如何处理 COM 对象的信息和示例就非常稀缺。这是本系列文章的第一篇,将探讨如何在纯 C 中利用 COM,而不使用任何框架。

对于标准的 Win32 控件,如 Static、Edit、Listbox、Combobox 等,您会获得控件的句柄(即 HWND),然后通过消息(通过 SendMessage)传递给它以进行操作。同样,当控件需要通知您某事或提供某些数据时,它会向您传递消息(即将其放入您自己的消息队列中,您通过 GetMessage 获取)。

OLE/COM 对象则不同。您不会来回传递消息。相反,COM 对象会提供一些指向特定函数的指针,您可以调用这些函数来操作对象。例如,Internet Explorer 的一个对象会提供一个指向函数的指针,您可以调用它来让浏览器在您的某个窗口中加载和显示网页。Office 的一个对象会提供一个指向函数的指针,您可以调用它来加载文档。如果 COM 对象需要通知您某事或向您传递数据,那么您需要编写程序中的特定函数,并将这些函数的指针提供给 COM 对象,以便在需要时对象可以调用这些函数。换句话说,您需要在程序中创建自己的 COM 对象。在 C 中,大部分麻烦将涉及定义自己的 COM 对象。要做到这一点,您需要了解 COM 对象的所有细节——这些细节是大多数预制框架会隐藏起来的内容,而我们将在本系列中进行探讨。

总而言之,您调用 COM 对象中的函数来操作它,它调用您程序中的函数来通知您事情或向您传递数据或以某种方式与您的程序交互。这种机制类似于调用 DLL 中的函数,但就像 DLL 也能调用您 C 程序中的函数一样——有点像“回调”。但与 DLL 不同的是,您不使用 LoadLibrary()GetProcAddress() 来获取 COM 对象函数的指针。正如我们很快将发现的,您将使用另一个操作系统函数来获取指向对象的指针,然后使用该对象来获取指向其函数的指针。

COM 对象及其 VTable

在我们学习如何使用 COM 对象之前,我们首先需要了解它是什么。而做到这一点的最好方法就是创建我们自己的 COM 对象。

但在我们这样做之前,让我们先看看 C 语言的 struct 数据类型。作为一名 C 程序员,您应该非常熟悉 struct。这是一个简单结构(称为“IExample”)的示例定义,它包含两个成员——一个 DWORD(通过成员名“count”访问),以及一个 80 个 char 的数组(通过成员名“buffer”访问)。

struct IExample {
   DWORD   count;
   char    buffer[80];
};

让我们使用 typedef 来使其更容易使用

typedef struct {
   DWORD   count;
   char    buffer[80];
} IExample;

下面是分配上述结构实例(省略了错误检查)并初始化其成员的示例

IExample * example;

example = (IExample *)GlobalAlloc(GMEM_FIXED, sizeof(IExample));
example->count = 1;
example->buffer[0] = 0;

您是否知道结构可以存储指向某个函数的指针?希望您知道,但这里有一个例子。假设我们有一个函数,它接收一个 char 指针作为参数,并返回一个 long。这是我们的函数

long SetString(char * str)
{
   return(0);
}

现在,我们想将此函数的指针存储在 IExample 中。这是我们定义 IExample 的方式,添加了一个成员(“SetString”)来存储指向上述函数的指针(我将使用 typedef 使其更易读)

typedef long SetStringPtr(char *);

typedef struct {
   SetStringPtr * SetString;
   DWORD          count;
   char           buffer[80];
} IExample;

下面是我们如何将 SetString 的指针存储在我们分配的 IExample 中,然后使用该指针调用 SetString

example->SetString = SetString;

long value = example->SetString("Some text");

好的,也许我们想存储两个函数的指针。这是第二个函数

long GetString(char *buffer, long length)
{
   return(0);
}

让我们重新定义 IExample,添加另一个成员(“GetString”)来存储指向第二个函数的指针

typedef long GetStringPtr(char *, long);

typedef struct {
   SetStringPtr * SetString;
   GetStringPtr * GetString;
   DWORD          count;
   char           buffer[80];
} IExample;

在这里,我们初始化了这个成员

example->GetString = GetString;

但假设我们不想将函数指针直接存储在 IExample 中。相反,我们宁愿有一个函数指针数组。例如,让我们定义第二个结构,其唯一目的是存储我们的两个函数指针。我们将此称为 IExampleVtbl 结构,并将其定义如下

typedef struct {
   SetStringPtr * SetString;
   GetStringPtr * GetString;
} IExampleVtbl;

现在,我们将指向上述数组的指针存储在 IExample 中。我们将为此添加一个名为“lpVtbl”的新成员(当然,我们将删除 SetStringGetString 成员,因为它们已被移至 IExampleVtbl 结构)

typedef struct {
   IExampleVtbl * lpVtbl;
   DWORD          count;
   char           buffer[80];
} IExample;

所以,这是一个分配和初始化 IExample(当然,还有一个 IExampleVtbl)的例子

// Since the contents of IExample_Vtbl will never change, we'll
// just declare it static and initialize it that way. It can
// be reused for lots of instances of IExample.
static const IExampleVtbl IExample_Vtbl = {SetString, GetString};

IExample * example;

// Create (allocate) a IExample struct.
example = (IExample *)GlobalAlloc(GMEM_FIXED, sizeof(IExample));

// Initialize the IExample (ie, store a pointer to
// IExample_Vtbl in it).
example->lpVtbl = &IExample_Vtbl;
example->count = 1;
example->buffer[0] = 0;

要调用我们的函数,我们这样做

char buffer[80];

example->lpVtbl->SetString("Some text");
example->lpVtbl->GetString(buffer, sizeof(buffer));

还有一件事。假设我们已决定我们的函数可能需要访问用于调用它们的结构体中的“count”和“buffer”成员。所以,我们将始终将指向该结构体的指针作为第一个参数传递。让我们重写我们的函数以适应这一点

typedef long SetStringPtr(IExample *, char *);
typedef long GetStringPtr(IExample *, char *, long);

long SetString(IExample *this, char * str)
{
   DWORD  i;

   // Let's copy the passed str to IExample's buffer
   i = lstrlen(str);
   if (i > 79) i = 79;
   CopyMemory(this->buffer, str, i);
   this->buffer[i] = 0;

   return(0);
}

long GetString(IExample *this, char *buffer, long length)
{
   DWORD  i;

   // Let's copy IExample's buffer to the passed buffer
   i = lstrlen(this->buffer);
   --length;
   if (i > length) i = length;
   CopyMemory(buffer, this->buffer, i);
   buffer[i] = 0;

   return(0);
}

在调用其函数时,我们传递 IExample 结构体的指针

example->lpVtbl->SetString(example, "Some text");
example->lpVtbl->GetString(example, buffer, sizeof(buffer));

如果您曾经使用过 C++,您可能会想“等等。这看起来有点熟悉。”是的。我们上面所做的就是使用纯 C 语言重新创建了一个 C++ 类。IExample 结构体实际上是一个 C++ 类(一个不继承自任何其他类的类)。C++ 类实际上只是一个结构体,其第一个成员始终是指向数组的指针——该数组包含指向该类中所有函数的指针。并且传递给每个函数的第一个参数始终是指向类(即结构体)本身的指针。(这被称为隐藏的“this”指针。)

最简单地说,COM 对象实际上只是一个 C++ 类。您可能会想“哇!IExample 现在是一个 COM 对象了吗?就这么简单吗?这很容易!”等等。IExample 越来越接近了,但还有更多内容。并非如此简单。如果真是这样,那就不是“微软技术”了,不是吗?

首先,让我们引入一些 COM 术语。您看到的上面的指针数组——IExampleVtbl 结构体?COM 文档将其称为接口VTable

COM 对象的一个要求是,我们的 VTable(即 IExampleVtbl 结构体)的前三个成员必须命名为 QueryInterfaceAddRefRelease。当然,我们必须编写这三个函数。微软已经确定了这些函数必须传递什么参数、它们必须返回什么以及它们使用的调用约定。我们将需要 #include 一些微软的头文件(这些头文件随您的 C 编译器一起提供,或者您可以下载 Microsoft SDK)。我们将重新定义我们的 IExampleVtbl 结构如下

#include <windows.h>
#include <objbase.h>
#include <INITGUID.H>

typedef HRESULT STDMETHODCALLTYPE QueryInterfacePtr(IExample *, REFIID, void **);
typedef ULONG STDMETHODCALLTYPE AddRefPtr(IExample *);
typedef ULONG STDMETHODCALLTYPE ReleasePtr(IExample *);

typedef struct {
   // First 3 members must be called QueryInterface, AddRef, and Release
   QueryInterfacePtr  *QueryInterface;
   AddRefPtr          *AddRef;
   ReleasePtr         *Release;
   SetStringPtr       *SetString;
   GetStringPtr       *GetString;
} IExampleVtbl;

让我们来分析一下 QueryInterfacetypedef。首先,该函数返回一个 HRESULT。这被简单地定义为 long。接下来,它使用 STDMETHODCALLTYPE。这意味着参数不是通过寄存器传递,而是通过堆栈传递。这还决定了谁负责清理堆栈。实际上,对于 COM 对象,我们应该确保所有函数都使用 STDMETHODCALLTYPE 声明,并返回一个 longHRESULT)。传递给 QueryInterface 的第一个参数是指向用于调用函数的对象的指针。我们不是正在将 IExample 变成一个 COM 对象吗?是的,这就是我们将为此参数传递的内容。(还记得我们决定传递给我们的任何函数的第一个参数是指向用于调用该函数的结构体的指针吗?COM 只是强制执行并依赖于此设计。)

稍后,我们将探讨 REFIID 是什么,还将讨论 QueryInterface 的第三个参数有什么用。但现在,请注意 AddRefRelease 也被传递了我们用于调用它们的结构体的相同指针。

好的,在我们忘记之前,让我们为 SetStringGetString 添加 HRESULT STDMETHODCALLTYPE

typedef HRESULT STDMETHODCALLTYPE SetStringPtr(IExample *, char *);
typedef HRESULT STDMETHODCALLTYPE GetStringPtr(IExample *, char *, long);

HRESULT STDMETHODCALLTYPE SetString(IExample *this, char * str)
{
   ...

   return(0);
}

HRESULT STDMETHODCALLTYPE GetString(IExample *this, char *buffer, long value)
{
   ...

   return(0);
}

总而言之,COM 对象基本上是一个 C++ 类。C++ 类只是一个总是以指向其 VTable(函数指针数组)的指针开头的结构体。VTable 中的前三个指针将始终命名为 QueryInterfaceAddRefRelease。VTable 中可能包含哪些其他函数,以及它们的指针名称是什么,取决于对象的类型。(您决定要向 COM 对象添加哪些其他函数。)例如,Internet Explorer 的浏览器对象无疑会有与播放音乐的某些对象不同的函数。但所有 COM 对象都以指向其 VTable 的指针开头,并且前三个 VTable 指针是指向对象的 QueryInterfaceAddRefRelease 函数。传递给对象函数的第一个参数是指向对象(结构体)本身的指针。这是定律。遵守它。

GUID

让我们继续我们将 IExample 变成一个真正的 COM 对象之旅。我们还没有真正编写我们的 QueryInterfaceAddRefRelease 函数。但在我们这样做之前,我们必须谈谈一个叫做全局唯一标识符(GUID)的东西。咳。那是什么?它是一个 16 字节的数组,其中填充了一系列唯一的字节。当我说是唯一时,我的意思是唯一。一个 GUID(即 16 字节数组)不能与另一个 GUID 具有相同的字节序列……无论是在世界的哪个地方。每一个曾经创建过的 GUID 都拥有一个 16 字节的唯一字节序列。

您如何创建这 16 个唯一字节的序列?您使用一个名为 GUIDGEN.EXE 的微软实用程序。它随您的编译器一起提供,或者您可以从 SDK 中获取。运行它,您将看到这个窗口

一旦您运行 GUIDGEN,它就会自动为您生成一个新的 GUID 并将其显示在“结果”框中。请注意,您在“结果”框中看到的内容将与上面不同。毕竟,每一个生成的 GUID 都将与任何其他 GUID 不同。所以您最好看到一些与我不同的东西。继续单击“新建 GUID”按钮,看看不同的数字出现在“结果”框中。随意点击,自娱自乐,看看您是否会生成相同的数字序列不止一次。您不会。更重要的是,其他人永远不会生成您生成的**任何**数字序列。

您可以单击“复制”按钮将文本传输到剪贴板,然后粘贴到其他地方(例如您的源代码中)。这是我这样做时粘贴的内容

// {0B5B3D8E-574C-4fa3-9010-25B8E4CE24C2}
DEFINE_GUID(<<name>>, 0xb5b3d8e, 0x574c, 0x4fa3, 
            0x90, 0x10, 0x25, 0xb8, 0xe4, 0xce, 0x24, 0xc2);

上面的内容是一个宏。微软头文件中的一个 #define 允许您的编译器将上面的内容编译成一个 16 字节的数组。

但有一件事我们必须做。我们必须将 <<name>> 替换为我们想要用于此 GUID 的 C 变量名。我们称之为 CLSID_IExample

// {0B5B3D8E-574C-4fa3-9010-25B8E4CE24C2}
DEFINE_GUID(CLSID_IExample, 0xb5b3d8e, 0x574c, 0x4fa3, 
      0x90, 0x10, 0x25, 0xb8, 0xe4, 0xce, 0x24, 0xc2);

现在我们有了可用于 IExample 的 GUID。

我们还需要一个 GUID 来标识 IExample 的 VTable(“接口”),即我们的 IExampleVtbl 结构体。所以,请继续单击 GUIDGEN.EXE 的“新建 GUID”按钮,并将其复制/粘贴到某个地方。这次,我们将用 C 变量名 IID_IExample 替换 <<name>>。这是我粘贴/编辑的内容

// {74666CAC-C2B1-4fa8-A049-97F3214802F0}
DEFINE_GUID(IID_IExample, 0x74666cac, 0xc2b1, 0x4fa8, 
      0xa0, 0x49, 0x97, 0xf3, 0x21, 0x48, 0x2, 0xf0);

总而言之,每个 COM 对象都有自己的 GUID,它是一个 16 字节的数组,与任何其他 GUID 都不同。GUID 是使用 GUIDGEN.EXE 实用程序创建的。COM 对象的 VTable(即接口)也有一个 GUID。

QueryInterface()、AddRef() 和 Release()

假设我们想允许另一个程序获取我们创建/初始化的 IExample 结构体(即 COM 对象)的句柄,以便该程序可以调用我们的函数。(我们暂时不探讨其他程序如何获取我们 IExample 的详细信息。我们稍后会讨论。)

除了我们自己的 COM 对象外,一台给定的计算机上可能安装了许多其他 COM 组件。(同样,我们将推迟讨论如何安装我们的 COM 组件。)并且不同的计算机可能安装了不同的 COM 组件。该程序如何确定我们的 IExample COM 对象是否已安装,并将其与其他所有 COM 对象区分开来?

请记住,每个 COM 对象都有一个完全唯一的 GUID,我们的 IExample 对象也是如此。我们的 IExample 的 VTable 也有一个 GUID。我们需要做的是告诉编写该程序的开发者我们的 IExample 对象及其 VTable 的 GUID 是什么。通常,您通过给它一个包含从 GUIDGEN.EXE 获得的上述两个 GUID 宏的头文件(.H)来做到这一点。好的,那么其他程序就知道了 IExample 及其 VTable 的 GUID。它用它们做什么?

这就是我们的 QueryInterface 函数的作用。请记住,每个 COM 对象都必须有一个 QueryInterface 函数(以及 AddRefRelease)。其他程序将把我们的 IExample VTable GUID 传递给我们的 QueryInterface 函数,我们将检查它以确保它确实是 IExample VTable 的 GUID。如果是,我们将返回一些东西让程序知道它确实拥有一个 IExample 对象。如果传递了错误的 GUID,我们将返回一些错误,并告知它所拥有的不是 IExample 对象。因此,计算机上的所有 COM 对象在 QueryInterface 被传递 IExample VTable 的 GUID 时都会返回错误,除了我们自己的 QueryInterface

传递给 QueryInterface 的第二个参数是我们用于检查的 GUID。第三个参数是(句柄),如果我们传递给它的 GUID 与 IExample VTable 的 GUID 匹配,我们将把传递给我们的同一个对象指针返回到此处。否则,我们将该句柄置零。此外,如果 GUID 匹配,QueryInterface 返回 longNOERROR(即,#define 为 0),否则返回某个非零错误值(E_NOINTERFACE)。所以,让我们看看 IExampleQueryInterface

HRESULT STDMETHODCALLTYPE QueryInterface(IExample *this, 
                          REFIID vTableGuid, void **ppv)
{
   // Check if the GUID matches IExample
   // VTable's GUID. Remember that we gave the
   // C variable name IID_IExample to our
   // VTable GUID. We can use an OLE function called
   // IsEqualIID to do the comparison for us.
   if (!IsEqualIID(riid, &IID_IExample))
   {
      // We don't recognize the GUID passed
      // to us. Let the caller know this,
      // by clearing his handle, 
      // and returning E_NOINTERFACE.
      *ppv = 0;
      return(E_NOINTERFACE);
   }

   // It's a match!

   // First, we fill in his handle with
   // the same object pointer he passed us. That's
   // our IExample we created/initialized,
   // and he obtained from us.
   *ppv = this;

   // Now we call our own AddRef function,
   // passing the IExample. 
   this->lpVtbl->AddRef(this);

   // Let him know he indeed has a IExample. 
   return(NOERROR);
}

现在让我们来谈谈我们的 AddRefRelease 函数。您会注意到我们在 QueryInterface 中调用了 AddRef……如果我们真的有一个 IExample

请记住,我们是代表其他程序分配 IExample 的。它只是获得了它的访问权限。我们负责在其他程序使用完毕后将其释放。我们如何知道何时完成?

我们将使用一种称为“引用计数”的东西。如果您回顾一下 IExample 的定义,您会发现我在其中放入了一个 DWORD 成员(count)。我们将利用这个成员。当我们创建一个 IExample 时,我们将将其初始化为 0。然后,每当调用 AddRef 时,我们将增加此成员(加 1),每当调用 Release 时,我们将减少此成员(减 1)。

因此,当我们的 IExample 被传递给 QueryInterface 时,我们调用 AddRef 来增加其 count 成员。当其他程序使用完毕后,该程序将把我们的 IExample 传递给我们的 Release 函数,在那里我们将减少该成员。如果它为 0,我们将在那时释放 IExample

这是 COM 的另一个重要规则。 如果您获得了其他人创建的 COM 对象,在您使用完毕后必须调用其 Release 函数。我们当然期望其他程序在使用完我们的 IExample 对象后调用我们的 Release 函数。

因此,这是我们的 AddRefRelease 函数

ULONG STDMETHODCALLTYPE AddRef(IExample *this)
{
   // Increment the reference count (count member).
   ++this->count;

   // We're supposed to return the updated count.
   return(this->count);
}

ULONG STDMETHODCALLTYPE Release(IExample *this)
{
   // Decrement the reference count.
   --this->count;

   // If it's now zero, we can free IExample.
   if (this->count == 0)
   {
      GlobalFree(this);
      return(0);
   }

   // We're supposed to return the updated count.
   return(this->count);
}

还有一件事我们要做的。微软定义了一个称为 IUnknown 的 COM 对象。那是什么?IUnknown 对象就像 IExample 一样,只是它的 VTable **只** 包含 QueryInterfaceAddRefRelease 函数(即它不包含我们 IExample VTable 具有 SetStringGetString 等其他函数)。换句话说,IUnknown 是最基本的 COM 对象。微软为 IUnknown 对象创建了一个特殊的 GUID。但您知道吗?我们的 IExample 对象也可以伪装成 IUnknown 对象。毕竟,它具有 QueryInterfaceAddRefRelease 函数。如果他们只关心前三个函数,那么没有人需要知道它实际上是一个 IExample 对象。我们将更改一行代码,以便在其他程序向我们传递我们 IExample 的 GUID 或 IUnknown 的 GUID 时报告成功。顺便说一句,微软的头文件给 IUnknown GUID 起了 C 变量名 IID_IUnknown

// Check if the GUID matches IExample's GUID or IUnknown's GUID.
if (!IsEqualIID(vTableGuid, &IID_IExample) && 
        !IsEqualIID(vTableGuid, &IID_IUnknown))

总而言之,对于我们自己的 COM 对象,我们代表另一个程序为其分配(该程序获取对象的访问权限并使用它来调用我们的函数)。我们负责释放对象。我们将引用计数与我们的 AddRefRelease 函数结合使用来实现此目的。我们的 QueryInterface 允许其他程序验证它们是否拥有它们想要的正确对象,同时也允许我们增加引用计数。(实际上,QueryInterface 主要服务于我们稍后将要研究的另一个目的。但目前,将它的用途这样看待就足够了。)

那么,IExample 现在是真正的 COM 对象了吗?是的!太棒了!不是很困难!我们完成了!

错误!我们仍然需要将这个东西打包成另一种程序可以使用的形式(即动态链接库),并编写代码来执行特殊的安装例程,并检查其他程序如何获取我们创建的 IExample(这会涉及到我们编写更多代码)。

IClassFactory 对象

现在我们需要看看程序如何获取我们的一个 IExample 对象,最终,我们必须编写更多代码来实现这一点。微软已经为此设计了一种标准化的方法。这涉及到我们在 DLL 中放入第二个 COM 对象(及其函数)。这个 COM 对象称为 IClassFactory,它在微软的头文件中有一组特定的已定义函数。它也有自己的已定义 GUID,并被赋予 C 变量名 IID_IClassFactory

我们的 IClassFactory 的 VTable 包含五个特定函数,它们是 QueryInterfaceAddRefReleaseCreateInstanceLockServer。请注意,IClassFactory 拥有自己的 QueryInterfaceAddRefRelease 函数,就像我们的 IExample 对象一样。毕竟,我们的 IClassFactory 也是一个 COM 对象,所有 COM 对象的 VTable 都必须以这三个函数开头。(但为了避免与 IExample 的函数发生名称冲突,我们将 IClassFactory 的函数名加上前缀“class”,例如 classQueryInterfaceclassAddRefclassRelease。只要 IClassFactory 的 VTable 将其前三个成员定义为 QueryInterfaceAddRefRelease,那就可以了。)

真正重要的函数是 CreateInstance。当程序希望我们创建一个 IExample 对象、初始化并返回它时,程序会调用我们的 IClassFactoryCreateInstance。实际上,如果程序想要多个 IExample 对象,它可以多次调用 CreateInstance。好的,这就是程序获取我们一个 IExample 对象的方式。“但程序如何获取我们一个 IClassFactory 对象呢?”,您可能会问。我们稍后会讲到。现在,让我们简单地编写我们的 IClassFactory 的五个函数,并创建它的 VTable。

创建 VTable 很容易。与我们 IExample 对象的 IExampleVtbl 不同,我们不必定义我们的 IClassFactory 的 VTable 结构体。微软已经在某个头文件中为我们定义了一个 IClassFactoryVtbl 结构体。我们只需要声明我们的 VTable 并用指向我们五个 IClassFactory 函数的指针填充它。让我们使用变量名 IClassFactory_Vtbl 创建一个静态 VTable 并进行填充

static const IClassFactoryVtbl IClassFactory_Vtbl = {classQueryInterface,
classAddRef,
classRelease,
classCreateInstance,
classLockServer};

同样,创建实际的 IClassFactory 对象也很容易,因为微软也为我们定义了该结构体。我们只需要一个,所以让我们使用变量名 MyIClassFactoryObj 声明一个静态 IClassFactory,并将其 lpVtbl 成员初始化为指向我们上面的 VTable

static IClassFactory MyIClassFactoryObj = {&IClassFactory_Vtbl};

现在,我们只需要编写上面的五个函数。我们的 classAddRefclassRelease 函数非常简单。因为我们实际上没有分配我们的 IClassFactory(即,我们只是将其声明为静态的),所以我们不需要释放任何东西。因此,classAddRef 将简单地返回 1(表示始终有一个 IClassFactory 在周围)。classRelease 也将做同样的事情。由于我们不必释放它,所以我们不需要为我们的 IClassFactory 进行任何引用计数。

ULONG STDMETHODCALLTYPE classAddRef(IClassFactory *this)
{
   return(1);
}

ULONG STDMETHODCALLTYPE classRelease(IClassFactory *this)
{
   return(1);
}

现在,让我们看看我们的 QueryInterface。它需要检查传递给它的 GUID 是 IUnknown 的 GUID(因为我们的 IClassFactory 具有 QueryInterfaceAddRefRelease 函数,所以它也可以伪装成 IUnknown 对象)还是 IClassFactory 的 GUID。否则,我们将做与我们在 IExampleQueryInterface 中所做的相同的事情。

HRESULT STDMETHODCALLTYPE classQueryInterface(IClassFactory *this, 
                          REFIID factoryGuid, void **ppv)
{
   // Check if the GUID matches an IClassFactory or IUnknown GUID.
   if (!IsEqualIID(factoryGuid, &IID_IUnknown) && 
       !IsEqualIID(factoryGuid, &IID_IClassFactory))
   {
      // It doesn't. Clear his handle, and return E_NOINTERFACE.
      *ppv = 0;
      return(E_NOINTERFACE);
   }

   // It's a match!

   // First, we fill in his handle with the same object pointer he passed us.
   // That's our IClassFactory (MyIClassFactoryObj) he obtained from us.
   *ppv = this;

   // Call our IClassFactory's AddRef, passing the IClassFactory. 
   this->lpVtbl->AddRef(this);

   // Let him know he indeed has an IClassFactory. 
   return(NOERROR);
}

现在,我们的 IClassFactoryLockServer 可以只是一个存根

HRESULT STDMETHODCALLTYPE classLockServer(IClassFactory *this, BOOL flock)
{
   return(NOERROR);
}

还有一个函数要写——CreateInstance。它定义如下

HRESULT STDMETHODCALLTYPE classCreateInstance(IClassFactory *, 
                          IUnknown *, REFIID, void **);

像往常一样,第一个参数将是指向用于调用 CreateInstanceIClassFactory 对象(MyIClassFactoryObj)的指针。

我们仅在实现称为聚合的东西时才使用第二个参数。我们现在不深入讨论。如果这个参数不为零,那么有人希望我们支持聚合,而我们不想这样做,我们将通过返回错误来表示这一点。

第三个参数将是 IExample VTable 的 GUID(如果有人确实希望我们分配、初始化并返回一个 IExample 对象)。

第四个参数是一个句柄,我们将在此处返回我们创建的 IExample 对象。

所以,让我们深入了解我们的 CreateInstance 函数(名为 classCreateInstance

HRESULT STDMETHODCALLTYPE classCreateInstance(IClassFactory *this, 
        IUnknown *punkOuter, REFIID vTableGuid, void **ppv)
{
   HRESULT          hr;
   struct IExample *thisobj;

   // Assume an error by clearing caller's handle.
   *ppv = 0;

   // We don't support aggregation in IExample.
   if (punkOuter)
      hr = CLASS_E_NOAGGREGATION;
   else
   {
      // Create our IExample object, and initialize it.
      if (!(thisobj = GlobalAlloc(GMEM_FIXED, 
                      sizeof(struct IExample))))
         hr = E_OUTOFMEMORY;
      else
      {
         // Store IExample's VTable. We declared it
         // as a static variable IExample_Vtbl.
         thisobj->lpVtbl = &IExample_Vtbl;

         // Increment reference count so we
         // can call Release() below and it will
         // deallocate only if there
         // is an error with QueryInterface().
         thisobj->count = 1;

         // Fill in the caller's handle
         // with a pointer to the IExample we just
         // allocated above. We'll let IExample's
         // QueryInterface do that, because
         // it also checks the GUID the caller
         // passed, and also increments the
         // reference count (to 2) if all goes well.
         hr = IExample_Vtbl.QueryInterface(thisobj, vTableGuid, ppv);

         // Decrement reference count.
         // NOTE: If there was an error in QueryInterface()
         // then Release() will be decrementing
         // the count back to 0 and will free the
         // IExample for us. One error that may
         // occur is that the caller is asking for
         // some sort of object that we don't
         // support (ie, it's a GUID we don't recognize).
         IExample_Vtbl.Release(thisobj);
      }
   }

   return(hr);
}

这就完成了实现我们的 IClassFactory 对象。

打包成 DLL

为了方便另一个程序获取我们的 IClassFactory(并调用其 CreateInstance 函数来获取一些 IExample 对象),我们将上面的源代码打包成一个动态链接库 (DLL)。本教程不讨论如何创建 DLL 本身,所以如果您不熟悉,请先阅读有关 DLL 的教程。

上面,我们已经编写了所有关于我们的 IExampleIClassFactory 对象的代码。我们只需要将其粘贴到我们的 DLL 源代码中。

但还有更多工作要做。微软还规定我们必须向 DLL 添加一个名为 DllGetClassObject 的函数。微软已经定义了它的参数、它的作用以及它应该返回什么。程序将调用我们的 DllGetClassObject 来获取指向我们 IClassFactory 对象的指针。(实际上,我们稍后会看到,程序将调用一个名为 CoGetClassObject 的 OLE 函数,该函数反过来会调用我们的 DllGetClassObject。)所以,这就是程序获取我们 IClassFactory 对象的方式——通过调用我们的 DllGetClassObject。我们的 DllGetClassObject 函数必须执行此任务。这是它的定义

HRESULT PASCAL DllGetClassObject(REFCLSID objGuid, 
        REFIID factoryGuid, void **factoryHandle);

传递的第一个参数将是我们 IExample 对象(不是其 VTable 的 GUID)的 GUID。我们需要检查它以确保调用者确实打算调用我们 DLL 的 DllGetClassObject。请注意,每个 COM DLL 都有一个 DllGetClassObject 函数,所以同样,我们需要该 GUID 来区分我们的 DllGetClassObject 与所有其他 COM DLL 的 DllGetClassObject

第二个参数将是 IClassFactory 的 GUID。

第三个参数是一个句柄,我们将在此处返回指向我们 IClassFactory 的指针(如果程序确实传递了 IExample 的 GUID,而不是其他 COM 对象的 GUID)。

HRESULT PASCAL DllGetClassObject(REFCLSID objGuid, 
        REFIID factoryGuid, void **factoryHandle)
{
   HRESULT  hr;

   // Check that the caller is passing
   // our IExample GUID. That's the COM
   // object our DLL implements.
   if (IsEqualCLSID(objGuid, &CLSID_IExample))
   {
      // Fill in the caller's handle
      // with a pointer to our IClassFactory object.
      // We'll let our IClassFactory's
      // QueryInterface do that, because it also
      // checks the IClassFactory GUID and does other book-keeping.
      hr = classQueryInterface(&MyIClassFactoryObj, 
                          factoryGuid, factoryHandle);
   }
   else
   {
      // We don't understand this GUID.
      // It's obviously not for our DLL.
      // Let the caller know this by
      // clearing his handle and returning
      // CLASS_E_CLASSNOTAVAILABLE.
      *factoryHandle = 0;
      hr = CLASS_E_CLASSNOTAVAILABLE;
   }

   return(hr);
}

我们快完成创建 DLL 所需的一切了。只剩最后一件事。实际加载我们 DLL 的不是程序。相反,当程序调用 CoGetDllClassObject(即 CoGetClassObject 找到我们的 DLL 文件,对其执行 LoadLibrary,使用 GetProcAddress 获取我们上面的 DllGetClassObject,并代表程序调用它)时,操作系统会代表程序加载它。不幸的是,微软没有提供任何方法让程序告诉操作系统程序已完成使用我们的 DLL,以及操作系统应该卸载(FreeLibrary)我们的 DLL。所以我们必须帮助操作系统,让它知道何时可以安全地卸载我们的 DLL。我们必须提供一个名为 DllCanUnloadNow 的函数,该函数将在可以安全卸载我们的 DLL 时返回 S_OK,否则返回 S_FALSE

我们如何知道何时可以安全卸载?

我们将不得不进行更多的引用计数。具体来说,每次我们为一个程序分配一个对象时,我们都必须增加一个计数。每次程序调用该对象的 Release 函数,并且我们释放该对象时,我们将减少同一个计数。只有当计数为零时,我们才会告诉操作系统我们的 DLL 可以安全卸载,因为那时我们确切地知道程序没有使用我们的任何对象。所以,我们将声明一个名为 OutstandingObjects 的静态 DWORD 变量来维护此计数。(当然,当我们的 DLL 首次加载时,需要将其初始化为 0。)

那么,在哪里可以最方便地递增这个变量?在我们的 IClassFactoryCreateInstance 函数中,在我们实际 GlobalAlloc 对象并确保一切正常之后。因此,我们将在该函数中添加一行,紧随 Release 的调用之后

static DWORD OutstandingObjects = 0;

HRESULT STDMETHODCALLTYPE classCreateInstance(IClassFactory *this, 
        IUnknown *punkOuter, REFIID vTableGuid, void **ppv)
{
   ...

         IExampleVtbl.Release(thisobj);

         // Increment our count of outstanding objects if all
         // went well.
         if (!hr) InterlockedIncrement(&OutstandingObjects);;
      }
   }

   return(hr);
}

在哪里可以最方便地递减这个变量?在我们的 IExampleRelease 函数中,在我们 GlobalFree 对象之后。所以我们在 GlobalFree 之后添加一行

InterlockedDecrement(&OutstandingObjects);

但还有更多。(微软的麻烦细节永无止境吗?)微软决定应该有一种方法可以让程序锁定我们的 DLL 以便使用。为此,它可以调用我们的 IClassFactoryLockServer 函数,传递 1 表示我们希望增加对我们 DLL 的锁定计数,或 0 表示我们希望减少对我们 DLL 的锁定计数。所以,我们还需要第二个静态 DWORD 引用计数,我们称之为 LockCount。(当然,这也需要在我们的 DLL 加载时初始化为 0。)我们的 LockServer 函数现在变成

static DWORD LockCount = 0;

HRESULT STDMETHODCALLTYPE 
        classLockServer(IClassFactory *this, BOOL flock)
{
   if (flock) InterlockedIncrement(&LockCount);
   else InterlockedDecrement(&LockCount);

   return(NOERROR);
}

现在我们准备编写 DllCanUnloadNow 函数

HRESULT PASCAL DllCanUnloadNow(void)
{
   // If someone has retrieved pointers to any of our objects, and
   // not yet Release()'ed them, then we return S_FALSE to indicate
   // not to unload this DLL. Also, if someone has us locked, return
   // S_FALSE
   return((OutstandingObjects | LockCount) ? S_FALSE : S_OK);
}

如果您下载了示例项目,我们 DLL 的源文件(IExample.c)位于 IExample 目录中。还提供了使用此源代码创建 DLL(IExample.dll)的 Microsoft Visual C++ 项目文件。

我们的 C++/C 头文件

如前所述,为了让用 C++/C 编写的程序使用我们的 IExample DLL,我们需要将我们 IExample 对象及其 VTable 的 GUID 提供给该程序的用户。我们将把这些 GUID 宏放在一个头文件(.H)中,我们可以分发给他人,也可以包含在我们 DLL 的源代码中。我们还需要将我们的 IExampleVtblIExample 结构体的定义放在这个头文件中,以便程序可以通过我们提供的 IExample 来调用我们的函数。

到目前为止,我们定义了我们的 IExampleVtblIExample 结构体如下

typedef HRESULT STDMETHODCALLTYPE QueryInterfacePtr(IExample *, REFIID, void **);
typedef ULONG STDMETHODCALLTYPE AddRefPtr(IExample *);
typedef ULONG STDMETHODCALLTYPE ReleasePtr(IExample *);
typedef HRESULT STDMETHODCALLTYPE SetStringPtr(IExample *, char *);
typedef HRESULT STDMETHODCALLTYPE GetStringPtr(IExample *, char *, long);

typedef struct {
   QueryInterfacePtr  *QueryInterface;
   AddRefPtr          *AddRef;
   ReleasePtr         *Release;
   SetStringPtr       *SetString;
   GetStringPtr       *GetString;
} IExampleVtbl;

typedef struct {
   IExampleVtbl *lpVtbl;
   DWORD         count;
   char          buffer[80];
} IExample;

上面的定义有一个问题。我们不希望让其他程序知道我们的“count”和“buffer”成员。我们想将它们隐藏起来,不让程序访问。程序永远不应该直接访问我们对象的成员。它应该只知道“lpVtbl”成员,以便能够调用我们的函数。所以,就程序而言,我们希望将我们的 IExample 定义为如下

typedef struct {
   IExampleVtbl *lpVtbl;
} IExample;

此外,虽然函数定义中的 typedef 使事情更容易阅读,但如果您的对象中有许多函数,这可能会变得冗长且容易出错。

最后,存在一个问题,即上面的定义是 C 语言的。对于想要使用我们 COM 对象的 C++ 程序来说,它实际上并没有使事情变得容易。毕竟,即使我们用 C 语言编写了 IExample,我们的 IExample 结构体实际上是一个 C++ 类。对于 C++ 程序来说,将其定义为 C++ 类比定义为 C 结构体要容易得多。

与上述定义不同,微软提供了一个宏,我们可以使用它来定义 VTable 和对象,使其同时适用于 C 和 C++,并隐藏额外的成员。要使用此宏,我们必须首先定义符号 INTERFACE 为我们的对象名称(在本例中为 IExample)。在此之前,我们必须 undef 该符号以避免编译器警告。然后,我们使用 DECLARE_INTERFACE_ 宏。在宏内部,我们列出了我们的 IExample 函数。下面是它的样子

#undef  INTERFACE
#define INTERFACE   IExample
DECLARE_INTERFACE_ (INTERFACE, IUnknown)
{
   STDMETHOD  (QueryInterface)  (THIS_ REFIID, void **) PURE;
   STDMETHOD_ (ULONG, AddRef)   (THIS) PURE;
   STDMETHOD_ (ULONG, Release)  (THIS) PURE;
   STDMETHOD  (SetString)       (THIS_ char *) PURE;
   STDMETHOD  (GetString)       (THIS_ char *, DWORD) PURE;
};

这看起来可能有点奇怪。

在定义函数时,当函数返回 HRESULT 时使用 STDMETHOD。我们的 QueryInterfaceSetStringGetString 函数返回 HRESULT。而 AddRefRelease 不返回。后两个返回 ULONG。所以这就是为什么我们为这两个函数使用 STDMETHOD_(带有下划线结尾)的原因。然后,我们把函数名放在括号里。如果函数不返回 HRESULT,我们需要将返回类型放在函数名前面,然后是一个逗号。在函数名之后,我们列出函数参数。THIS 指的是指向我们对象的指针(即 IExample)。如果传递给函数的唯一内容是指针,那么您只需将 THIS 放在括号里。AddRefRelease 函数就是这种情况。但其他函数有额外的参数。所以,我们必须使用 THIS_(带有下划线结尾)。然后我们列出剩余的参数。请注意,THIS_ 和剩余参数之间**没有**逗号。但剩余参数之间用逗号分隔。最后,我们放入单词 PURE 和一个分号。

可以肯定的是,这是一个奇怪的宏,它主要是为了定义一个 COM 对象,使其能够同时适用于纯 C 编译器和 C++ 编译器。

您可能会问“但是我们的 IExample 结构体的定义在哪里?”这个宏确实很奇怪。它会导致 C 编译器自动生成一个 IExample 结构体的定义,该结构体只包含“lpVtbl”成员。所以,仅仅通过这样定义我们的 VTable,我们就自动得到了一个适合其他程序员的 IExample 定义。

将我们的两个 GUID 宏粘贴到这个头文件中,我们就准备好了。我这样做创建了文件 IExample.h

但正如您所知,我们的 IExample 实际上还有两个数据成员。所以,我们必须做的是在一个“IExample”的“变体”中定义一个“MyRealIExample”,并在我们的 DLL 源文件中。我们将称之为“MyRealIExample”,它将是我们 IExample 的真实定义

typedef struct {
   IExampleVtbl *lpVtbl;
   DWORD         count;
   char          buffer[80];
} MyRealIExample;

我们将更改我们 IClassFactoryCreateInstance 函数中的一行代码,以便我们分配一个 MyRealIExample 结构体

if (!(thisobj = GlobalAlloc(GMEM_FIXED, sizeof(struct MyRealIExample))))

程序不需要知道我们实际上给了它一个包含一些额外数据成员的对象的(这些数据成员对于该程序来说实际上是隐藏的)。毕竟,这两个结构体都有相同的“lpVtbl”成员,指向相同的函数指针数组。但是现在,我们的 DLL 函数可以通过将 IExample 指针转换为 MyRealIExample 指针来访问这些“隐藏”的成员。

定义 (DEF) 文件

我们还需要一个 DEF 文件来暴露 DllCanUnloadNowDllGetClassObject 这两个函数。微软的编译器还要求它们被定义为 PRIVATE。这是我们的 DEF 文件,必须将其提供给链接器

LIBRARY IExample
EXPORTS
DllCanUnloadNow   PRIVATE
DllGetClassObject PRIVATE

安装 DLL 并注册对象

我们现在已经完成了创建 IExample.dll 所需的一切。我们可以继续编译 IExample.dll

但这还不是我们工作的结束。在任何其他程序能够使用我们的 IExample 对象(即 DLL)之前,我们需要做两件事

  1. 将我们的 DLL 安装到能够被运行该程序的计算机找到的位置。
  2. 将我们的 DLL 注册为 COM 组件。

我们需要创建一个安装程序,该程序会将 IExample.DLL 复制到一个精心选择的位置。例如,也许我们在 Program Files 目录中创建一个“IExample”目录,并将 DLL 复制到那里。(当然,我们的安装程序应该进行版本检查,以便如果该位置已安装了我们 DLL 的较早版本,我们不会用一个早期版本覆盖它。)

然后我们需要注册这个 DLL。这涉及到创建几个注册表项。

我们首先需要在 HKEY_LOCAL_MACHINE\Software\Classes\CLSID 下创建一个键。对于这个新键的名称,我们必须使用我们 IExample 对象的 GUID,但它必须以特定的文本字符串格式化。

如果您下载了示例项目,RegIExample 目录包含 IExample.dll 的示例安装程序。stringFromCLSID 函数演示了如何将我们的 IExample GUID 格式化为适合使用它创建注册表键名的文本字符串。

注意:此示例安装程序不会将 DLL 复制到某个精心选择的位置,然后再进行注册。相反,它允许您选择编译 IExample.dll 的任何位置,并在该位置进行注册。这只是为了方便开发/测试。生产质量的安装程序应将 DLL 复制到精心选择的位置,并进行版本检查。这些需要的增强功能留给您自己的安装程序来完成。

在我们的“GUID 键”下,我们必须创建一个名为 InprocServer32 的子键。然后将此子键的默认值设置为我们 DLL 已安装的完整路径。

如果不需要限制程序仅从单个线程调用我们 DLL 的函数,我们还必须将一个名为 ThreadingModel 的值设置为字符串值“both”。由于我们在 IExample 函数中不使用全局数据,因此它是线程安全的。

在运行我们的安装程序后,IExample.dll 现在已作为 COM 组件在我们的计算机上注册,并且某个程序现在可以使用它。

注意:UnregIExample 目录包含 IExample.dll 的示例卸载程序。它基本上删除了 RegIExample 创建的注册表项。生产质量的卸载程序还应该删除 IExample.dll 和安装程序创建的任何目录。

C 语言示例程序

现在我们准备编写一个使用我们 IExample COM 对象的 C 程序。如果您下载了示例项目,IExampleApp 目录包含一个 C 语言示例程序。

首先,C 程序 #include 了我们的 IExample.h 头文件,以便它可以引用我们 IExample 对象及其 VTable 的 GUID。

在程序使用任何 COM 对象之前,它必须初始化 COM,这通过调用 CoInitialize 函数来完成。这只需要执行一次,所以一个好的地方是在程序的开头执行。

接下来,程序调用 CoGetClassObject 来获取指向 IExample.dllIClassFactory 对象的指针。请注意,我们将 IExample 对象的 GUID 作为第一个参数传递。我们还将传递一个指向我们的变量 classFactory 的指针,如果一切顺利,IClassFactory 的指针将在此处返回给我们。

一旦我们获得了 IClassFactory 对象,我们就可以调用其 CreateInstance 函数来获取一个 IExample 对象。请注意我们如何使用 IClassFactory 来调用其 CreateInstance 函数。我们通过 IClassFactory 的 VTable(即其 lpVtbl 成员)获取函数。还要注意,我们将 IClassFactory 指针作为第一个参数传递。请记住,这是标准的 COM。

请注意,我们将 IExample 的 VTable GUID 作为第三个参数传递。对于第四个参数,我们传递一个指向我们变量 exampleObj 的指针,如果一切顺利,IExample 对象的指针将在此处返回给我们。

一旦我们有了一个 IExample 对象,我们就可以 Release IClassFactory 对象。请记住,程序在完成使用某个对象后必须调用该对象的 Release 函数。IClassFactory 是一个对象,就像 IExample 是一个对象一样。每个对象都有自己的 Release 函数,当您使用完该对象后必须调用它。我们不再需要 IClassFactory 了。我们不想获取更多的 IExample 对象,也不想调用 IClassFactory 的任何其他函数。所以,我们现在可以 Release 它。请注意,这对我们的 IExample 对象没有影响。

所以接下来,我们调用 IClassFactoryRelease 函数。一旦我们这样做,我们的 classFactory 变量就不再包含有效的指针了。它现在是垃圾。

但我们仍然拥有我们的 IExample 指针。我们还没有 Release 它。所以接下来,我们决定调用 IExample 的一些函数。我们调用 SetString。然后我们接着调用 GetString。请注意,我们如何使用 IExample 指针来调用其 SetString 函数。我们通过 IExample 的 VTable 获取函数。同时,请注意我们将 IExample 指针作为第一个参数传递。再次,这是标准 COM。

当我们最终完成 IExample 后,我们 Release 它。一旦我们这样做,我们的 exampleObj 变量就不再包含有效的指针了。

最后,我们必须调用 CoUninitialize 来允许 COM 清理一些内部东西。这只需要执行一次,所以最好在程序结束时执行(但前提是 CoInitialize 成功)。

还有一个名为 CoCreateInstance 的函数,可以用来替换调用 CoGetClassObject(获取 DLL 的 IClassFactory)以及随后调用 IClassFactoryCreateInstanceCoCreateInstance 本身调用 CoGetClassObject,然后调用 IClassFactoryCreateInstanceCoCreateInstance 直接返回我们的 IExample,使我们无需获取 IClassFactory。这是一个示例用法

if ((hr = CoCreateInstance(&CLSID_IExample, 0, 
        CLSCTX_INPROC_SERVER, &IID_IExample, &exampleObj)))
   MessageBox(0, "Can't create IExample object", 
              "CoCreateInstance error", 
              MB_OK|MB_ICONEXCLAMATION);

C++ 语言示例程序

IExampleCPlusApp 目录包含一个 C++ 语言示例程序。它执行与 C 语言示例完全相同的操作。但是,您会注意到一些重要的区别。首先,因为 IExample.h 中的宏将 IExample 定义为 C++ 类(而不是结构体),并且因为 C++ 以特殊方式处理类,所以 C++ 程序以不同的格式调用我们的 IExample 函数。

在 C 语言中,我们通过直接访问 VTable(通过 lpVtbl 成员)来获取 IExample 函数,并且我们始终将 IExample 作为第一个参数传递。

C++ 编译器知道类以 VTable 作为其第一个成员,并自动访问其 lpVtbl 成员以获取其中的函数。所以,我们不必指定 lpVtbl 部分。此外,C++ 编译器会自动将对象作为第一个参数传递。

所以,在 C 语言中,我们编写

classFactory->lpVtbl->CreateInstance(classFactory, 0, 
                      &IID_IExample, &exampleObj);

而在 C++ 中,我们编写

classFactory->CreateInstance(0, IID_IExample, &exampleObj);

注意:我们也省略了 IID_IExample GUID 上的 &。C++ 的 GUID 宏不需要指定它。

修改代码

要创建您自己的对象,请复制 IExample 目录。删除 DebugRelease 子目录,以及以下文件

IExample.dsp
IExample.dsw
IExample.ncb
IExample.opt
IExample.plg

在剩余的文件(IExample.cIExample.hIExample.def)中,搜索并用您的对象名称替换字符串 IExample,例如 IMyObject。根据您新的对象名称重命名这些文件(即 IMyObject.c 等)。

在此目录中创建一个名为您新对象名称的新 Visual C++ 项目。对于项目类型,选择“Win32 动态链接库”。创建一个空项目。然后将上述三个文件添加到其中。

确保使用 GUIDGEN.EXE 为您的对象及其 VTable 生成您自己的 GUID。不要使用我生成的 GUID。替换 .H 文件中的 GUID 宏(并记住替换 GUID 宏的 <<name>> 部分)。

.C.H 文件中删除 SetStringGetString 函数,并用您自己的函数替换它们。修改 .H 文件中的 INTERFACE 宏以定义您添加的函数。

更改 MyRealIExample 的数据成员(即 MyRealIMyObject, whatever)为您想要的。

修改安装程序以更改源代码中的前三个字符串。

在示例程序中,搜索并用您对象名称替换字符串 IExample

接下来做什么?

虽然 C 或 C++ 程序,或用大多数编译语言编写的程序都可以使用我们的 COM 对象,但我们还没有添加一些支持来允许大多数解释型语言使用我们的对象,例如 Visual Basic、VBscript、JScript、Python 等。这将是本系列第二部分的主题。

© . All rights reserved.