COM 纯 C 实现,第五部分






4.94/5 (34投票s)
添加可连接对象(源/接收)。
目录
- 将回调函数封装在 COM 对象中
- IConnectionPointContainer 和 IConnectionPoint 对象
- 一个 C 应用程序示例
- 添加对脚本语言的支持
- 另一个 C 应用程序示例
- VBScript 示例
- 多种类型回调对象
- 多个回调对象
引言
通常,我们调用的 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 以 QueryInterface
、AddRef
和 Release
函数开头。我们不会添加任何 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_ISort
和 IID_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
函数可以调用它。那么,我们只需将 SetCompare
和 UnsetCompare
函数添加到我们的 ISort
VTable 中吗?不行。
这里开始变得复杂了。Microsoft 需要一种标准的方式来允许应用程序向 COM 对象提供其回调函数。Microsoft 决定应用程序应该将其回调函数封装在一个 COM 对象中。具体是哪个 COM 对象?这取决于。
还记得 qsort
的作者是如何决定传递给回调函数的参数以及它的返回值的吗?嗯,既然我们是编写我们 ISort
对象的人,我们就可以自己定义一个 COM 对象,应用程序必须使用它来封装我们所需的任何回调函数。所以,让我们定义一个我们称之为 ICompare
的 COM 对象。当然,我们必须以 QueryInterface
、AddRef
和 Release
这三个函数开始其 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)。这两个子对象称为 IConnectionPointContainer
和 IConnectionPoint
。我们只需要将它们添加到我们的 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 传递给我们的 ISort
的 QueryInterface
函数,应用程序可以确定我们的 ISort
是否接受回调函数。顺便说一句,在 COM 文档中,这样的对象(即提供 IConnectionPointContainer
的对象)被描述为*源事件*。如果 QueryInterface
返回指向 IConnectionPointContainer
的指针,那么该对象就可以源事件。(如果不是,则返回 0。)
其次,IConnectionPointContainer
有一个函数用于获取 IConnectionPoint
子对象。该函数名为 FindConnectionPoint
。
所以,让我们来编写我们 IConnectionPointContainer
子对象的功能。因为它是 ISort
的一个子对象,而 ISort
是基对象,所以我们的 IConnectionPointContainer
的 QueryInterface
、AddRef
和 Release
函数只是委托给 ISort
的 QueryInterface
、AddRef
和 Release
。
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
。应用程序将一个句柄传递给我们,告诉我们它希望我们返回 ISort
的 IConnectionPoint
子对象的位置。由于我们已将其直接嵌入 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
一样),其 QueryInterface
、AddRef
和 Release
将委托给 ISort
的 QueryInterface
、AddRef
和 Release
。我不打算在这里重复这些函数,因为它们与 IConnectionPointContainer
的函数几乎相同。
目前,我们将使用 EnumConnections
函数的存根。
GetConnectionInterface
函数只是将 ICompare
的 VTable GUID 复制到应用程序传递的缓冲区中。稍后我们将看到它的用途。
GetConnectionPointContainer
函数返回指向创建此 IConnectionPoint
对象的 IConnectionPointContainer
子对象的指针。这意味着,只要应用程序仍然持有 IConnectionPointContainer
提供的某个 IConnectionPoint
子对象,我们的 IConnectionPointContainer
就必须保留。这对我们来说不是问题,因为我们将 IConnectionPointContainer
和 IConnectionPoint
都嵌入了我们的 MyRealISort
中(并且我们的 MyRealISort
将一直存在,直到其所有子对象和基对象都被 Release
。)
上述三个函数相当简单,我将只让你参考 *Sort.c* 中的源代码。
真正的工作在于 Advise
和 Unadvise
函数。这就是应用程序如何实际给我们它的 ICompare
。Advise
基本上执行了我们 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
指针存储在我们 MyRealISort
的 compare
成员中,以便我们可以在需要时访问它,并最终 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* 中的代码几乎完全相同。唯一的区别是,在我们的 IClassFactory
的 CreateInstance
分配了我们的 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
)中包含了附加函数(Callback1
到 Callback5
)。这样,我们可以欺骗 DispInvoke
使用此 VTable,就好像它确实被声明为 dual 一样。此外,请注意我们加载的类型库是 IExampleEvts2
的类型库,因为 IFeedback2
VTable 在那里被描述。我们要求 GetTypeInfoOfGuid
创建的 ITypeInfo
是为 IFeedback2
VTable 创建的。我们使用此 ITypeInfo
来处理我们 IFeedback2
的 Invoke
函数。
一个 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
调用脚本引擎的 IFeedback2
的 Invoke
并传递 DISPID 1(即,用于 Callback1
)时,被调用的 VBScript 函数是 label_Callback1
。
事实上,我们提供了这样一个名称的 VB 函数,它只是显示一个消息框。
多种类型回调对象
一个对象可以支持多种类型回调对象。例如,如果我们愿意,我们可以让我们的 ISort
对象支持同时接收 ICompare
和 IFeedback2
回调对象。在这种情况下,我们需要为每个对象提供一个单独的 IConnectionPoint
对象。在这种情况下,我们需要在 IDL 文件中同时放置 ICompare
和 IFeedback2
VTable,并在对象中同时引用它们,如下所示
coclass IExampleEvts2 { [default] interface IExampleEvts2VTbl; [source, default] interface IFeedback2VTbl; [source] interface ICompareVtbl; }
但请注意,只有一个源接口可以被标记为默认接口,并且脚本引擎只能使用默认接口(即,脚本不能为我们提供任何 ICompare
回调函数)。
此外,我们必须为我们的 IConnectionPointContainer
的 EnumConnectionPoints
函数提供实际的代码。此函数应返回另一个对象,该对象可以枚举我们支持的每种不同类型的 IConnectPoint
。(它返回的对象是一个标准的枚举器对象,例如我们在“集合”章节中使用的枚举器。)
因为回调对象通常与脚本一起使用(并且脚本引擎无法为单个 coclass 对象支持多种类型回调对象),所以我将不再深入探讨多种类型回调对象。
多个回调对象
在我们的示例对象中,我们只允许应用程序(或脚本引擎)给我们一个 IFeedback2
对象(我们将其存储在我们的 MyRealIExampleEvts2
的 feedback
成员中)。但理论上,一个应用程序可以给我们任意数量的 IFeedback2
回调对象。在我们的 IConnectionPoint
的 Advise
中,我们只是拒绝接受应用程序/引擎提供的多个 IFeedback2
。如果你想允许应用程序/引擎提供更多 IFeedback2
,那么你可能需要定义另一个可以链接到列表中的结构,而不是一个 feedback
成员,并且该结构还包含一个用于存储 IFeedback2
指针的成员。你将在 Advise
中 GlobalAlloc
其中一个结构,并将它们链接到存储在某个 MyRealIExampleEvts2
成员中的列表的头部。
你还需要修改 DoSomething
函数,使其循环遍历整个列表,并调用每个 IFeedback2
的 Invoke
。
但是,同样,由于回调对象的主要用途是用于脚本,并且大多数用途将只涉及一个回调对象,因此我们将跳过开发一个示例来进一步说明。
接下来呢?
在下一篇文章中,我们将探讨如何将 C 应用程序转换为 ActiveX 脚本主机(即,如何从你的 C 应用程序运行脚本,脚本如何调用你应用程序中的 C 函数,以及你的应用程序如何调用脚本中的函数,以控制你的应用程序并与脚本交换数据)。