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

COM 纯 C 实现,第五部分

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.94/5 (34投票s)

2006 年 5 月 22 日

CPOL

18分钟阅读

viewsIcon

124552

downloadIcon

2305

添加可连接对象(源/接收)。

目录

引言

通常,我们调用的 DLL 函数“回调”到我们自己的一个函数中会很方便,这样我们可以在特定点执行一些额外任务,或者在某个事件发生时收到通知。例如,考虑标准的 C 库函数 qsort。传递给 qsort 的第四个参数是指向我们提供用于比较两个项的某个函数的指针。这样,当我们调用 qsort 时,qsort 允许我们确定项的排序顺序,而 qsort 本身负责实际重新排序这些项。

我们不能仅仅向 qsort 提供任意函数。编写 qsort 的人规定了会传递给我们的“比较”回调函数的参数、我们的函数返回什么,当然,还规定了我们的回调函数的具体用途。此外,qsort 决定了何时会实际调用我们的回调函数(因为正是 qsort 自己调用我们的函数)。设计 qsort 的人规定,我们的比较回调函数 **必须** 定义为如下形式

int (__cdecl *compare)(const void *elem1, const void *elem2);

让我们创建我们自己的 qsort 版本,我们称之为 Sort。(实际上,为了简化,我们会作弊,只实现冒泡排序。)让我们假设我们将其放入一个名为 *ISort.dll* 的 DLL 中。

而不是每次都向 Sort 传递一个指向比较函数的指针,让我们设计成这样:应用程序首先调用一个单独的 SetCompare 函数,允许我们的 DLL 将指针存储在某个全局变量中。我们还在 DLL 中放一个名为 UnsetCompare 的函数,应用程序可以调用它来清除该函数指针。所以这是我们的 DLL 源代码(我们将把它放在一个名为 *ISort.c* 的文件中)

// A global to store the pointer to the app's Compare function
int (STDMETHODCALLTYPE *CompareFunc)(const void *, const void *);

void STDMETHODCALLTYPE SetCompare(
     int (STDMETHODCALLTYPE *compare)(const void *, const void *))
{
   // Save the compare function ptr in a global
   CompareFunc = compare;
}

void STDMETHODCALLTYPE UnsetCompare(void)
{
   CompareFunc = 0;
}

HRESULT STDMETHODCALLTYPE Sort(void *base, 
        DWORD numElems, DWORD sizeElem)
{
   void   *hi;
   void      *p;
   void   *lo;
   void   *tmp;

   // Has the app set its Compare function pointer yet?
   if (!CompareFunc) return(E_FAIL);

   // Do the (bubble) sort
   if ((tmp = GlobalAlloc(GMEM_FIXED, sizeElem)))
   {
      hi = ((char *)base + ((numElems - 1) * sizeElem));
      lo = base;

      while (hi > base)
      {
         lo = base;
         p = ((char*)base + sizeElem);
         while (p <= hi)
         {
            if ((*CompareFunc)(p, lo) > 0) lo = p;
            (char *)p += sizeElem;
         }

         CopyMemory(tmp, lo, sizeElem);
         CopyMemory(lo, hi, sizeElem);
         CopyMemory(hi, tmp, sizeElem);
         (char *)hi -= sizeElem;
      }

      GlobalFree(tmp);
      return(S_OK);
   }

   return(E_OUTOFMEMORY);
}

现在,让我们编写一个使用我们的 DLL 来对五个 DWORD 数组进行排序的应用程序。首先,应用程序调用 SetCompare 来指定其比较回调函数(我们将命名为 Compare)。然后,应用程序调用 Sort。最后,当我们的应用程序完成使用 Sort 后,如果我们想确保任何对 Sort 的后续调用都不会意外地导致它调用我们的 Compare 函数,我们就调用 UnsetCompare

// An array of 5 DWORDs to be sorted
DWORD Array[5] = {2, 3, 1, 5, 4};

// Our Compare function
int STDMETHODCALLTYPE Compare(const void *elem1, const void *elem2)
{
   // Do a compare of the two elements. We know that
   // we passed an array of DWORD values to Sort()
   if (*((DWORD *)elem1) == *((DWORD *)elem2)) return 0;
   if (*((DWORD *)elem1) *lt; *((DWORD *)elem2)) return -1;
   return 1;
}

int main(int argc, char **argv)
{
   // Give ISort.dll our Compare function ptr
   SetCompare(Compare);

   // Sort of array of 5 DWORDs
   Sort(&Array[0], 5, sizeof(DWORD));

   UnsetCompare();

   return 0;
}

回调函数封装在 COM 对象中

COM 也提供了一种回调机制,但正如你可能担心的那样,它不像我们上面的示例那样直接。在名为 *ISort* 的目录中,你可以找到实现了我们上面排序 DLL 的 COM 对象的文件。

首先,我们必须将 Sort 函数封装在某个 COM 对象中。毕竟,我们将把 *ISort.dll* 变成一个 COM 组件。让我们使用 Microsoft 提供的 DECLARE_INTERFACE_ 宏来定义一个 ISort 对象。像所有 COM 对象一样,它的 VTable 以 QueryInterfaceAddRefRelease 函数开头。我们不会添加任何 IDispatch 函数。(我们的 ISort 不能被 VBScript 等脚本语言使用)。然后,我们将 Sort 函数作为第一个附加函数添加。这是我们将在 *ISort.h* 中放入的定义

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

所以我们的应用程序将获取我们 ISort 对象的一个实例并调用其 Sort 函数。现在你已经有足够的经验编写 C/C++ 应用程序来获取 COM 对象并调用其函数了。这没什么新鲜的。

当然,我们需要运行 *GUIDGEN.EXE* 来为我们的 ISort 对象及其 VTable 生成 GUID。我们将分别给它们命名为 CLSID_ISortIID_ISort,并将它们放入 *ISort.h*。

// ISort object's GUID
// {619321BA-4907-4596-874A-AEFF082F0014}
DEFINE_GUID(CLSID_ISort, 0x619321ba, 0x4907, 0x4596,
 0x87, 0x4a, 0xae, 0xff, 0x8, 0x2f, 0x0, 0x14);

// ISort VTable's GUID
// {4C9A7D40-D0ED-45ea-9520-1CB9095973F8}
DEFINE_GUID(IID_ISort, 0x4c9a7d40, 0xd0ed, 0x45ea,
 0x95, 0x20, 0x1c, 0xb9, 0x9, 0x59, 0x73, 0xf8);

像我们通常做的那样,我们需要在我们的 ISort 对象中添加至少一个额外的私有数据成员——一个引用计数成员。因此,我们在 *ISort.c* 中定义一个 MyRealISort,它向我们的 ISort 添加了这个额外的私有成员

typedef struct {
   ISortVtbl   *lpVtbl;   // ISort's VTable
   DWORD       count;     // ISort's reference count
} MyRealISort;

到目前为止,这一切都应该让你很熟悉了。

剩下的就是提供一种方式,让应用程序能够给我们一个指向其 Compare 函数的指针,以便我们的 Sort 函数可以调用它。那么,我们只需将 SetCompareUnsetCompare 函数添加到我们的 ISort VTable 中吗?不行。

这里开始变得复杂了。Microsoft 需要一种标准的方式来允许应用程序向 COM 对象提供其回调函数。Microsoft 决定应用程序应该将其回调函数封装在一个 COM 对象中。具体是哪个 COM 对象?这取决于。

还记得 qsort 的作者是如何决定传递给回调函数的参数以及它的返回值的吗?嗯,既然我们是编写我们 ISort 对象的人,我们就可以自己定义一个 COM 对象,应用程序必须使用它来封装我们所需的任何回调函数。所以,让我们定义一个我们称之为 ICompare 的 COM 对象。当然,我们必须以 QueryInterfaceAddRefRelease 这三个函数开始其 VTable。那么,我们接下来想要什么?我们来看看……我们想支持脚本语言提供比较函数(例如 VBScript 函数)吗?如果是,我们将在后面添加一些 IDispatch 函数。不,暂时跳过,只允许 C/C++ 应用程序提供比较回调。

让我们向 ICompare 添加一个附加函数。我们称之为 Compare。这将是应用程序的比较回调。(所以,现在你看到了应用程序如何将回调封装在我们自己定义的 COM 对象中。)这是我们 ICompare 的定义(我们将把它添加到 *ISort.h*)

#undef INTERFACE
#define INTERFACE  ICompare
DECLARE_INTERFACE_ (INTERFACE, IUnknown)
{
   STDMETHOD  (QueryInterface) (THIS_ REFIID, void **) PURE;
   STDMETHOD_ (ULONG, AddRef)  (THIS) PURE;
   STDMETHOD_ (ULONG, Release) (THIS) PURE;
   STDMETHOD_ (long, Compare)  (THIS_ const void *, 
                                const void *) PURE;
};

我们需要运行 *GUIDGEN.EXE* 来为我们 ICompare 的 VTable 创建一个 GUID。(我们不需要对象的 GUID)。我们将给它命名为 DIID_Compare。(传统上,Microsoft 会在 VTable 的 GUID 名称前加上一个 *D*,用于封装回调。我们只是遵循传统。)

// ICompare VTable's GUID
// {4115B8E2-1823-4bbc-B10D-3D33AAA12ACF}
DEFINE_GUID(DIID_ICompare, 0x4115b8e2, 0x1823, 0x4bbc,
 0xb1, 0xd, 0x3d, 0x33, 0xaa, 0xa1, 0x2a, 0xcf);

让我们回到我们的应用程序。现在,我们必须在应用程序代码中放置一个 COM 对象。具体来说,我们必须创建一个 ICompare 对象,并让我们的 Compare 回调成为它的附加函数。因为我们只需要一个 ICompare 对象,我们将简单地将其声明为静态。我们还必须 #include ISort.h,因为其中包含 ICompare 对象、其 VTable 和其 GUID 的定义。所以,这是我们插入以替换上述应用程序代码中 Compare 函数的代码

#include "ISort.h"

// Here's our app's ICompare object. We need only one of these,
// so we'll just declare it static. That way, we don't have to
// allocate it, free it, nor maintain any reference count.
static ICompare   MyCompare;

// This is ICompare's QueryInterface. It returns a pointer
// to our ICompare object.
HRESULT STDMETHODCALLTYPE QueryInterface(ICompare *this,
      REFIID vTableGuid, void **ppv)
{
   // Since this is an ICompare object, we must recognize
   // ICompare VTable's GUID, which is defined for us in
   // ISort.h. Our ICompare can also masquerade as an IUnknown.
   if (!IsEqualIID(vTableGuid, &IID_IUnknown) &&
      !IsEqualIID(vTableGuid, &DIID_ICompare))
   {
      *ppv = 0;
      return(E_NOINTERFACE);
   }

   *ppv = this;

   // Normally, we'd call our AddRef function here. But since
   // our ICompare isn't allocated, we don't need to bother
   // with reference counting.
   return(NOERROR);
}

ULONG STDMETHODCALLTYPE AddRef(ICompare *this)
{
   // Our one and only ICompare isn't allocated. Instead, it's
   // statically declared above (MyCompare). So we'll just
   // return 1.
   return(1);
}

ULONG STDMETHODCALLTYPE Release(ICompare *this)
{
   // Our one and only ICompare isn't allocated, so we
   // never need worry about freeing it.
   return(1);
}

// This is the extra function for ICompare. It is called
// Compare. The ISort object will call this function when
// we call ISort's Sort function.
long STDMETHODCALLTYPE Compare(ICompare *this,
      const void *elem1, const void *elem2)
{
   // Do a compare of the two elements. We know that
   // we passed an array of DWORD values to Sort().
   if (*((DWORD *)elem1) == *((DWORD *)elem2)) return 0;
   if (*((DWORD *)elem1) < *((DWORD *)elem2)) return -1;
   return 1;
}

// Our ICompare VTable. We need only one of these, so we can
// declare it static.
static const ICompareVtbl ICompare_Vtbl = {QueryInterface,
AddRef,
Release,
Compare};

请注意,我们的 Compare 函数就在那里。它现在只是被封装在 ICompare 对象中。(传递给它的第一个参数现在是我们 ICompare 对象的指针,在这种情况下,将是我们的静态声明的 MyCompare。)

现在,我们剩下真正棘手的工作了。我们的应用程序如何将它的 ICompare 对象交给我们的 ISort 对象(在 *ISort.dll* 中)?我真希望我能说这会很简单易懂,但不幸的是,由于 MS 程序员在设计这个方案时做出了一些值得商榷的设计决策,这将是一次痛苦而曲折的恐怖之旅。

IConnectionPointContainer 和 IConnectionPoint 对象

为了让应用程序能够将它的 ICompare 对象交给我们的 ISort 对象,我们必须向我们的 ISort 对象添加一些东西。具体来说,我们必须向它添加两个子对象。(希望你在上一章中已经阅读并理解了如何创建一个具有子对象的对象。)

一件好事是,Microsoft 已经在与你的编译器一起提供的某些头文件中定义了这两个子对象(以及它们的 VTable GUID)。这两个子对象称为 IConnectionPointContainerIConnectionPoint。我们只需要将它们添加到我们的 ISort 对象中。由于我们已经定义了一个 MyRealISort,我们将直接将这两个子对象嵌入其中。而且,由于我们知道应用程序将给我们一个 ICompare 对象的指针,所以让我们也在 MyRealISort 中添加一个地方来存储它。

typedef struct {
   ISortVtbl                 *lpVtbl;
   DWORD                     count;
   IConnectionPointContainer container;
   IConnectionPoint          point;
   ICompare                  *compare;
} MyRealISort;

请注意,我们的基对象是我们的 ISort 本身。我们可以这样表示

typedef struct {
   ISort                     iSort;
   DWORD                     count;
   IConnectionPointContainer container;
   IConnectionPoint          point;
   ICompare                  *compare;
} MyRealISort;

但是,由于 ISort 只有一个 lpVtbl 成员,所以这两个定义在本质上是相同的。

正如你在上一章中回忆的那样,基对象的 QueryInterface 必须识别其所有子对象 VTable 的 GUID 以及其自身的 VTable。所以我们的 ISort QueryInterface 必须是这样的

HRESULT STDMETHODCALLTYPE QueryInterface(ISort *this, 
        REFIID vTableGuid, void **ppv)
{
   // Because an IConnectionPointContainer
   // is a sub-object of our ISort, we
   // must return a pointer to this sub-object
   // if the app asks for it. Because
   // we've embedded our IConnectionPointContainer
   // object inside of our
   // MyRealISort, we can get that sub-object
   // very easily using pointer arithmetic.
   if (IsEqualIID(vTableGuid, &IID_IConnectionPointContainer))
      *ppv = ((unsigned char *)this + 
               offsetof(MyRealISort, container));

   else if (IsEqualIID(vTableGuid, &IID_IUnknown) || 
            IsEqualIID(vTableGuid, &IID_ISort))
      *ppv = this;

   else
   {
      *ppv = 0;
      return(E_NOINTERFACE);
   }

   this->lpVtbl->AddRef(this);

   return(NOERROR);
}

但是等等。我们是否忘记了也检查我们 IConnectionPoint 子对象的 VTable GUID,并返回指向它的指针?我们没有忘记。不幸的是,想出这个设计的 MS 程序员有点打破了规则。IConnectionPoint 子对象不是通过调用基对象的 QueryInterface(也不是通过我们 IConnectionPointContainer 子对象的 QueryInterface)获得的。稍后我们将看到它是如何获得的。

IConnectionPointContainer 子对象可以服务于两个目的。

首先,通过将 IConnectionPointContainer VTable GUID 传递给我们的 ISortQueryInterface 函数,应用程序可以确定我们的 ISort 是否接受回调函数。顺便说一句,在 COM 文档中,这样的对象(即提供 IConnectionPointContainer 的对象)被描述为*源事件*。如果 QueryInterface 返回指向 IConnectionPointContainer 的指针,那么该对象就可以源事件。(如果不是,则返回 0。)

其次,IConnectionPointContainer 有一个函数用于获取 IConnectionPoint 子对象。该函数名为 FindConnectionPoint

所以,让我们来编写我们 IConnectionPointContainer 子对象的功能。因为它是 ISort 的一个子对象,而 ISort 是基对象,所以我们的 IConnectionPointContainerQueryInterfaceAddRefRelease 函数只是委托给 ISortQueryInterfaceAddRefRelease

STDMETHODIMP QueryInterface_Connect(IConnectionPointContainer *this,
                                    REFIID vTableGuid, void **ppv)
{
   // Because this is a sub-object of our ISort (ie, MyRealISort) object,
   // we delegate to ISort's QueryInterface. And because we embedded the
   // IConnectionPointContainer directly inside of MyRealISort, all we need
   // is a little pointer arithmetic to get our ISort.
   return(QueryInterface((ISort *)((char *)this - 
          offsetof(MyRealISort, container)), vTableGuid, ppv));
}

STDMETHODIMP_(ULONG) AddRef_Connect(IConnectionPointContainer *this)
{
   // Because we're a sub-object of ISort, delegate to its AddRef()
   // in order to increment ISort's reference count.
   return(AddRef((ISort *)((char *)this - offsetof(MyRealISort, container))));
}

STDMETHODIMP_(ULONG) Release_Connect(IConnectionPointContainer *this)
{
   // Because we're a sub-object of ISort, delegate to its Release()
   // in order to decrement ISort's reference count.
   return(Release((ISort *)((char *)this - offsetof(MyRealISort, container))));
}

目前,我们将使用 EnumConnectionPoints 函数的存根。

STDMETHODIMP EnumConnectionPoints(IConnectionPointContainer *this,
             IEnumConnectionPoints **enumPoints)
{
   *enumPoints = 0;
   return(E_NOTIMPL);
}

真正的工作在于 FindConnectionPoint。应用程序将一个句柄传递给我们,告诉我们它希望我们返回 ISortIConnectionPoint 子对象的位置。由于我们已将其直接嵌入 MyRealISort 中,因此很容易找到它。

应用程序还传递了我们为 ICompare(回调)对象定义的 VTable GUID。这就是我们知道应用程序确实打算提供我们所需对象的正确方式。如果我们不准备提供 ICompare 对象,我们就不想返回 IConnectionPoint

STDMETHODIMP FindConnectionPoint(IConnectionPointContainer *this,
             REFIID vTableGuid, IConnectionPoint **ppv) 
{
   // Is the app asking us to return an IConnectionPoint object it can use
   // to give us its ICompare object? The app asks this by passing us
   // ICompare VTable's GUID (which we defined in ISort.h).
   if (IsEqualIID(vTableGuid, &DIID_ICompare))
   {
      MyRealISort  *iSort;

      // The app obviously wants to give its ICompare object to our
      // ISort. In order to do that, we need to give the app a standard
      // IConnectionPoint. This is easy to do since we embedded both
      // our IConnectionPointContainer and IConnectionPoint inside of our 
      // ISort. All we need is a little pointer arithmetic.
      iSort = (MyRealISort *)((char *)this - offsetof(MyRealISort, container));
      *ppv = &iSort->point;

      // Because we're giving the app a pointer to our IConnectionPoint,
      // and our IConnectionPoint is a sub-object of ISort, we need to
      // increment ISort's reference count. The easiest way to do this is
      // to call our IConnectionPointContainer's AddRef, because all we do
      // there is delegate to our ISort's AddRef.
      AddRef_Connect(this);

      return(S_OK);
   }

   // We don't support any other app callback objects for our ISort.
   // All we've defined, and support, is an ICompare object. Tell
   // the app we don't know anything about the GUID he passed to us, and
   // do not give him any IConnectionPoint object.
   *ppv = 0;
   return(E_NOINTERFACE);
}

这完成了我们 IConnectionPointContainer 的功能。现在我们需要编写我们 IConnectionPoint 的功能。

由于 IConnectionPoint 也是 ISort 的子对象(就像 IConnectionPointContainer 一样),其 QueryInterfaceAddRefRelease 将委托给 ISortQueryInterfaceAddRefRelease。我不打算在这里重复这些函数,因为它们与 IConnectionPointContainer 的函数几乎相同。

目前,我们将使用 EnumConnections 函数的存根。

GetConnectionInterface 函数只是将 ICompare 的 VTable GUID 复制到应用程序传递的缓冲区中。稍后我们将看到它的用途。

GetConnectionPointContainer 函数返回指向创建此 IConnectionPoint 对象的 IConnectionPointContainer 子对象的指针。这意味着,只要应用程序仍然持有 IConnectionPointContainer 提供的某个 IConnectionPoint 子对象,我们的 IConnectionPointContainer 就必须保留。这对我们来说不是问题,因为我们将 IConnectionPointContainerIConnectionPoint 都嵌入了我们的 MyRealISort 中(并且我们的 MyRealISort 将一直存在,直到其所有子对象和基对象都被 Release。)

上述三个函数相当简单,我将只让你参考 *Sort.c* 中的源代码。

真正的工作在于 AdviseUnadvise 函数。这就是应用程序如何实际给我们它的 ICompareAdvise 基本上执行了我们 SetCompare 函数所做的相同任务。而 Unadvise 则执行了我们 UnsetCompare 所做的事情。

让我们看看 Advise

STDMETHODIMP Advise(IConnectionPoint *this, IUnknown *obj, DWORD *cookie) 
{
   HRESULT     hr;
   MyRealISort *iSort;

   // Get our MyRealISort that this IConnectionPoint sub-object belongs
   // to. Because this IConnectPoint sub-object is embedded directly
   // inside its MyRealISort, all we need is a little pointer arithmetic.
   iSort = (MyRealISort *)((char *)this - offsetof(MyRealISort, point));

   // We allow only one ICompare for our ISort, so see if the app already
   // called our Advise(), and we got one. If so, let the app know that it
   // is trying to give us more ICompares than we allow.
   if (iSort->compare) return(CONNECT_E_ADVISELIMIT);
 
   // Ok, we haven't yet gotten the one ICompare we allow from the app. Get
   // the app's ICompare object. We do this by calling the QueryInterface
   // function of the app object passed to us. We pass ICompare VTable's
   // GUID (which we defined in ISort.h).
   //
   // Save the app's ICompare pointer in our ISort compare member, so we
   // can get it when we need it.
   hr = obj->lpVtbl->QueryInterface(obj, &DIID_ICompare, &iSort->compare);

   // We need to return (to the app) some value that will clue our Unadvise()
   // function below how to locate this app ICompare. The simpliest thing is
   // to just use the app's ICompare pointer as that return value.
   *cookie = (DWORD)iSort->compare;

   return(hr);
}

实际上,应用程序并不传递它的 ICompare 对象的指针。那样太简单、太直接、太容易理解了,显然这并不是 MS 程序员设计这个东西的目标。相反,应用程序传递一个应用程序对象给我们,我们可以从该对象请求应用程序给我们它的 ICompare。我们的 Advise 函数预计会调用该对象的 QueryInterface 函数,传递 ICompare VTable 的 GUID 来请求应用程序的 ICompare。我猜 MS 认为这是验证应用程序是否确实给我们一个 ICompare 的好方法(除了,难道你不知道,如果一个应用程序写得太差,无法弄清楚它应该做什么 Advise,那么它的 QueryInterface 也会返回错误的对象吗?)。当然,应用程序的 QueryInterface 会自动对它给我们的 ICompare 执行 AddRef,所以我们被期望在完成后 Release 应用程序的 ICompare

我们的 Advise 函数将应用程序的 ICompare 指针存储在我们 MyRealISortcompare 成员中,以便我们可以在需要时访问它,并最终 Release 它。

Advise 必须返回一个由我们自己选择的 DWORD 值给应用程序。应用程序被期望存储这个 DWORD 值,并在以后将其传递给我们的 Unadvise 函数。我们可以选择任何我们想要的 DWORD 值,但这个值应该以某种方式帮助我们的 Unadvise 函数,当应用程序稍后想让我们释放(即 Release)它的 ICompare 时。实际上,应用程序后来调用我们的 Unadvise 是因为应用程序不再希望我们调用其回调函数,而是希望我们 Release 它的 ICompare。所以,让我们看看 Unadvise

STDMETHODIMP Unadvise(IConnectionPoint *this, DWORD cookie) 
{
   MyRealISort *iSort;

   // Get the MyRealISort that this IConnectionPoint sub-object belongs
   // to. Because this IConnectPoint sub-object is embedded directly
   // inside its MyRealISort, all we need is a little pointer arithmetic.
   iSort = (MyRealISort *)((char *)this - offsetof(MyRealISort, point));

   // Use the passed value to find wherever we stored his ICompare pointer.
   // Well, since we allow only one ICompare for our ISort, we already
   // know we stored it in our ISort->compare member. And Advise()
   // returned that pointer as the "cookie" value. So we already got the
   // ICompare right now.
   //        
   // Let's just make sure the cookie he passed is really the pointer we
   // expect.
   if (cookie && (ICompare *)cookie == iSort->compare)
   {
      // Release the app's ICompare
      ((ICompare *)cookie)->lpVtbl->Release((ICompare *)cookie);

      // We no longer have the app's ICompare, so clear the ISort
      // compare member.
      iSort->compare = 0;
      
      return(S_OK);
   }
   return(CONNECT_E_NOCONNECTION);
}

我们 *Sort.c* 文件中的其余代码与我们第一章的 *IExample.c* 中的代码几乎完全相同。唯一的区别是,在我们的 IClassFactoryCreateInstance 分配了我们的 MyRealISort 之后,我们必须初始化其中的子对象,并初始化任何其他私有数据成员。

你可以将我们的 COM 对象编译成 *ISORT.DLL*。要注册它,只需使用 *RegIExample* 目录中的 IExample 的注册实用程序,并将所有 IExample 实例替换为 ISort

一个 C 应用程序示例

在 *ISortApp* 目录中有一个使用我们的 ISort DLL 的 C 应用程序示例。之前我们讨论了 ICompare 函数,所以这里唯一真正新的是应用程序如何调用我们的 Advise 函数来给我们它的 ICompare 对象,然后稍后调用 Unadvice 来请求我们释放它。这是一个相当简单的示例,所以你可以查阅 *ISortApp.c* 中的注释。

因为 COM 事件连接可能相当复杂,所以我提供了一个使用回调对象的第二个示例。在 *IExampleEvts* 目录中是另一个 COM DLL 的源代码。这个 DLL 实现了一个对象(IExampleEvts),它有一个名为 DoSomething 的附加函数。该函数能够调用多达五个不同应用程序回调中的任何一个。所以我们定义了一个 IFeedback 对象来封装这五个回调函数。它的定义在 *IExampleEvts.h* 中。(回调对象可以包含许多额外的回调函数。每个函数可以有不同的目的、不同的参数和不同的返回值。)

花点时间仔细阅读这些示例,并完全理解正在发生的事情。随着我们继续前进,事情将变得更加复杂。

添加对脚本语言的支持

为了支持脚本语言,我们需要添加一些 IDispatch 函数。让我们以我们的 IExampleEvts COM 对象为例,并将其改编为脚本语言使用。

首先,让我们创建一个名为 *IExampleEvts2* 的新目录,并将原始源文件复制到那里。然后,我们将重命名文件,将所有 IExampleEvts 实例替换为 IExampleEvts2,并将所有 IFeedback 实例替换为 IFeedback2。我已经为你做好了。我还运行了 *GUIDGEN.EXE* 并为对象及其 VTable 生成了新的 GUID。因为我们还需要一个类型库,所以我还为此创建了一个新的 GUID。

我们需要向我们的 IExampleEvts2 VTable 和 IFeedback2 VTable 添加 IDispatch 函数。你可以将原始的 *IExampleEvts.h* 与新的 *IExampleEvts2.h* 进行比较,以查看更改。请注意,两个 VTable 都添加了五个标准的 IDispatch 函数,宏现在也指定两个 VTable 都基于 IDispatch(而不是 IUnknown)。

在 *IExampleEvts2.c* 中,我向我们的 IExampleEvts2 对象及其静态声明的 VTable 添加了五个 IDispatch 函数。我还添加了一个静态变量 MyTypeInfo,用于存储 Microsoft 的 GetTypeInfoOfGuid 为我们创建的 ITypeInfo(我们用它来 DispInvoke)。这与我们在第二章中为 IExample2 对象添加支持时所做的相同。到目前为止,所有这些更改都没有什么新东西。

我们必须更改我们的 DoSomething 函数。我们不再能够直接调用任何回调函数(通过对象的 lpVtbl 引用它)。这是因为脚本引擎实际上不会在其 VTable 中为那些附加回调函数设置函数指针。相反,我们必须通过回调对象(IFeedback)的 Invoke 函数间接调用它。我们必须传递我们想要调用的函数的相应 DISPID。

唯一的新东西出现在我们的 *IExampleEvts2.IDL* 文件中(即,*MIDL.EXE* 编译成我们的 *.TLB* 类型库文件的文件)。请注意,我们如何定义 IFeedback2 的 VTable(我们任意将其标记为 IFeedback2VTbl)。这与我们定义 IExampleEvts2 VTable 的方式类似,只是我们没有在 IFeedback2 的 VTable 上使用 dual 关键字。脚本引擎无法为回调对象使用双重 VTable。我任意选择使用 DISPID 1 来表示第一个回调函数(Callback1),2 表示第二个回调函数,依此类推。

另一个新东西是我们如何定义我们自己的 IExampleEvts2 对象,如下所示

coclass IExampleEvts2
{
   [default] interface IExampleEvts2VTbl;
   [source, default] interface IFeedback2VTbl;
}

第一行接口应该看起来很熟悉。它与我们在任何 IDL 文件中标记对象 VTable 的方式相同。但第二行接口告诉任何使用我们类型库的人,IExampleEvts2 支持一个回调对象,其 VTable 由 IFeedback2Vtbl 描述。“source”关键字表示这是一个回调 VTable。“default”关键字意味着这个 VTable 是脚本引擎应为与我们的 IExampleEvts2 对象关联的任何回调对象使用的。

你可以将我们的 COM 对象编译成 *IExampleEvts2.DLL*。要注册它,只需使用 *RegIExample2* 目录中的 IExample2 的注册实用程序,并将所有 IExample2 实例替换为 IExampleEvts2

另一个 C 应用程序示例

在 *IExampleEvts2App* 目录中有一个使用我们的 IExampleEvts2 DLL 的 C 应用程序示例。将其与 *IExampleEvtsApp* 中的原始源进行比较。请注意,我们已将五个标准的 IDispatch 函数添加到我们的 IFeedback2 对象中。

虽然 *IExampleEvts2.IDL* 没有将 IFeedback2 VTable 定义为“dual”,但我们仍然在我们实际的静态声明的 VTable(IFeedback2_Vtbl)中包含了附加函数(Callback1Callback5)。这样,我们可以欺骗 DispInvoke 使用此 VTable,就好像它确实被声明为 dual 一样。此外,请注意我们加载的类型库是 IExampleEvts2 的类型库,因为 IFeedback2 VTable 在那里被描述。我们要求 GetTypeInfoOfGuid 创建的 ITypeInfo 是为 IFeedback2 VTable 创建的。我们使用此 ITypeInfo 来处理我们 IFeedback2Invoke 函数。

一个 VBScript 示例

在 *IExampleEvts2* 目录中有一个名为 *IExampleEvts2.vbs* 的 VBScript 示例,如下所示

Set myobj = WScript.CreateObject("IExampleEvts2.object", "label_")
myobj.DoSomething()

sub label_Callback1()
   Wscript.echo "Callback1 is called"
end sub

第一行使用 Windows Scripting Shell 的 CreateObject 来获取我们 IExampleEvts2 对象的一个实例(它应该已经注册了 ProdID 为 *IExampleEvts2.object*)。我们使用 Script Shell 的 CreateObject 而不是 VB 引擎的 CreateObject 的原因,是因为前者允许我们传递第二个参数——一个字符串,它会加到任何回调函数名的前面。在这里,我们指定了字符串 *label*。例如,当我们的 IExampleEvts2 DoSomething 调用脚本引擎的 IFeedback2Invoke 并传递 DISPID 1(即,用于 Callback1)时,被调用的 VBScript 函数是 label_Callback1

事实上,我们提供了这样一个名称的 VB 函数,它只是显示一个消息框。

多种类型回调对象

一个对象可以支持多种类型回调对象。例如,如果我们愿意,我们可以让我们的 ISort 对象支持同时接收 ICompareIFeedback2 回调对象。在这种情况下,我们需要为每个对象提供一个单独的 IConnectionPoint 对象。在这种情况下,我们需要在 IDL 文件中同时放置 ICompareIFeedback2 VTable,并在对象中同时引用它们,如下所示

coclass IExampleEvts2
{
   [default] interface IExampleEvts2VTbl;
   [source, default] interface IFeedback2VTbl;
   [source] interface ICompareVtbl;
}

但请注意,只有一个源接口可以被标记为默认接口,并且脚本引擎只能使用默认接口(即,脚本不能为我们提供任何 ICompare 回调函数)。

此外,我们必须为我们的 IConnectionPointContainerEnumConnectionPoints 函数提供实际的代码。此函数应返回另一个对象,该对象可以枚举我们支持的每种不同类型的 IConnectPoint。(它返回的对象是一个标准的枚举器对象,例如我们在“集合”章节中使用的枚举器。)

因为回调对象通常与脚本一起使用(并且脚本引擎无法为单个 coclass 对象支持多种类型回调对象),所以我将不再深入探讨多种类型回调对象。

多个回调对象

在我们的示例对象中,我们只允许应用程序(或脚本引擎)给我们一个 IFeedback2 对象(我们将其存储在我们的 MyRealIExampleEvts2feedback 成员中)。但理论上,一个应用程序可以给我们任意数量的 IFeedback2 回调对象。在我们的 IConnectionPointAdvise 中,我们只是拒绝接受应用程序/引擎提供的多个 IFeedback2。如果你想允许应用程序/引擎提供更多 IFeedback2,那么你可能需要定义另一个可以链接到列表中的结构,而不是一个 feedback 成员,并且该结构还包含一个用于存储 IFeedback2 指针的成员。你将在 AdviseGlobalAlloc 其中一个结构,并将它们链接到存储在某个 MyRealIExampleEvts2 成员中的列表的头部。

你还需要修改 DoSomething 函数,使其循环遍历整个列表,并调用每个 IFeedback2Invoke

但是,同样,由于回调对象的主要用途是用于脚本,并且大多数用途将只涉及一个回调对象,因此我们将跳过开发一个示例来进一步说明。

接下来呢?

在下一篇文章中,我们将探讨如何将 C 应用程序转换为 ActiveX 脚本主机(即,如何从你的 C 应用程序运行脚本,脚本如何调用你应用程序中的 C 函数,以及你的应用程序如何调用脚本中的函数,以控制你的应用程序并与脚本交换数据)。

© . All rights reserved.