纯 C 语言 COM 组件系列,第 4 部分






4.95/5 (30投票s)
用 C 语言创建一个包含多个接口的 COM 对象。
目录
- 引言
- 在我们的对象中嵌入子对象
- 应用程序如何获取基对象
- 应用程序如何从基对象获取子对象
- 应用程序如何从另一个子对象获取对象
- 委托
- 我们的基对象的 QueryInterface、AddRef 和 Release
- 我们的子对象的 QueryInterface、AddRef 和 Release
- 向我们的对象添加子对象的另一种方法
- 具有多个接口的示例对象
- 使用我们对象的 C 语言应用程序示例
- 接下来呢?
引言
有时,COM 对象可能拥有所谓的“多个接口”。说一个 COM 对象拥有多个接口只是另一种说法,即该对象由几个“子对象”组成。每个子对象本身就是一个完整的 COM 对象,拥有自己的 lpVtbl
成员(指向其自身 VTable 的指针),以及一组 VTable 函数(包括其自身的 QueryInterface
、AddRef
和 Release
函数)。
一个对象可以有多种子对象。例如,假设我们的对象管理一个声卡。声卡上可以有几个输入和输出插孔。假设我们想为每个输入/输出插孔设置一个子对象。例如,假设我们有一个线路输入插孔、一个麦克风输入插孔和一个扬声器输出插孔。因此,我们可能有三个子对象,称为 ILineIn
、IMicIn
和 ISpeakerOut
。每个单独的对象可能都有专门控制其个体插孔的函数。例如,麦克风输入插孔可能有一个设置,可以将其阻抗在低阻抗和高阻抗之间切换。所以,我们的 IMicIn
对象可能有一个 SetImpedance
函数。但是 ILineIn
和 ISpeakerOut
对象不需要这样的函数,因为该设置与那些插孔无关。也许 ILineIn
和/或 ISpeakerOut
对象可能需要特定于那些插孔的其他函数。假设 ILineIn
有一个名为 Mute
的函数来静音信号。我们的 ISpeakerOut
有一个名为 SetVolume
的函数来设置其音量。以下是我们如何使用 Microsoft 提供的宏来定义这三个对象,以定义一个 COM 对象:
#undef INTERFACE #define INTERFACE IMicIn DECLARE_INTERFACE_ (INTERFACE, IUnknown) { STDMETHOD (QueryInterface) (THIS_ REFIID, void **) PURE; STDMETHOD_ (ULONG, AddRef) (THIS) PURE; STDMETHOD_ (ULONG, Release) (THIS) PURE; STDMETHOD (SetImpedance) (THIS_ long) PURE; }; #undef INTERFACE #define INTERFACE ILineIn DECLARE_INTERFACE_ (INTERFACE, IUnknown) { STDMETHOD (QueryInterface) (THIS_ REFIID, void **) PURE; STDMETHOD_ (ULONG, AddRef) (THIS) PURE; STDMETHOD_ (ULONG, Release) (THIS) PURE; STDMETHOD (Mute) (THIS_ long) PURE; }; #undef INTERFACE #define INTERFACE ISpeakerOut DECLARE_INTERFACE_ (INTERFACE, IUnknown) { STDMETHOD (QueryInterface) (THIS_ REFIID, void **) PURE; STDMETHOD_ (ULONG, AddRef) (THIS) PURE; STDMETHOD_ (ULONG, Release) (THIS) PURE; STDMETHOD (SetVolume) (THIS_ long) PURE; };
请注意,每个子对象的 VTable 都以其自身的 QueryInterface
、AddRef
和 Release
函数开头,所有 COM 对象都必须如此。在此之后,我们列出了对象 VTable 中的任何额外函数。因此,IMicIn
具有其 SetImpedance
函数,ILineIn
具有其 Mute
函数,ISpeakerOut
具有其 SetVolume
函数。(为了演示,我已定义这些函数中的每一个都接收一个参数——一个 long
值。在 SetImpedance
的情况下,这可能是阻抗值。对于 Mute
,这可能是 1 表示静音或 0 表示取消静音。对于 SetVolume
,这可能是音量级别。)
注意:我没有向这些子对象添加任何IDispatch
函数(也没有指定它们的 VTable 基于IDispatch
)。相反,我省略了IDispatch
函数,并指定 VTable 基于IUnknown
。因此,这些子对象的函数都不能被 VBScript 或 JScript 等脚本语言直接调用。没关系,因为那些脚本语言本来就无法使用多接口对象。
另外,请记住,上述宏还自动将对象本身定义为一个成员,即 lpVtbl
— 指向其 VTable 的指针。
typedef struct { IMicInVtbl *lpVtbl; } IMicIn; typedef struct { ILineInVtbl *lpVtbl; } ILineIn; typedef struct { ISpeakerOutVtbl *lpVtbl; } ISpeakerOut;
在我们的对象中嵌入子对象
关于子对象有一些规则。其中一个子对象被认为是“基对象”,其 VTable 指针必须是我们对象本身的第一个成员。例如,假设我们想让 IMicIn
子对象成为基对象。我们可以这样定义我们的对象(我们称之为 IAudioCard
),首先嵌入 IMicIn
子对象,然后嵌入其他子对象:
typedef struct { IMicIn mic; // Our IMicIn (base) sub-object. ILineIn line; // Our ILineIn sub-object. ISpeakerOut speaker; // Our ISpeakerOut sub-object. } IAudioCard;
请记住,IMicIn
的第一个成员是其 lpVtbl
(即指向其 VTable 的指针)。由于我们的 IMicIn
直接嵌入在 IAudioCard
对象内,位于结构的开头,这意味着 IAudioCard
对象内部的第一个成员实际上是指向 IMicIn
VTable 的指针。这就是 IMicIn
成为基对象的方式。因此,IAudioCard
确实以 VTable 指针开头(因为 IMicIn
以 VTable 指针开头),其 VTable 确实以 QueryInterface
、AddRef
和 Release
三个函数开头。
另一条规则是,每个子对象的 VTable 都必须有自己的 GUID。因此,我们需要运行 GUIDGEN.EXE 为 IMicIn
、ILineIn
和 ISpeakerOut
VTable 创建 GUID。而且,我们还需要为我们的 IAudioCard
对象本身创建一个 GUID。
应用程序如何获取基对象
通常,应用程序可以通过调用我们的 IClassFactory
的 CreateInstance
来获取基对象(在此示例中为 IMicIn
),并传递 IAudioCard
的 GUID。(或者,要获取我们的基对象,也许应用程序会调用我们添加到其他对象中的某个额外函数,就像我们在本系列上一部分中返回集合对象或 IEnumVARIANT
对象一样。)然后,我们的 IClassFactory
的 CreateInstance
将返回指向基对象的指针。
// How an app gets IAudioCard's base object. Error-checking omitted! // Include the .H file that defines the VTables of // our IAudioCard sub-objects, and has all of the // needed GUIDs. #include "IAudioCard.h" // First, we need to get IAudioCard's base object. Let's assume we // do this by calling CoCreateInstance. We must pass the GUID for the // DLL containing the IAudioCard object. Let's assume that this GUID // has been given the name CLSID_IAudioCard in IAudioCard.h. We must // also pass the IAudioCard object's GUID which we'll assume is // given the name IID_IAudioCard. CoCreateInstance returns a pointer // to IAudioCard's base object, which we store in our audioCard variable. // Note that we've declared our audioCard variable as an IUknown *. This // is because we don't really know what kind of object that base object // is. (Well, it's going to be an IMicIn object. But if this DLL was // written by someone else, we may not have been told what kind of // object the base object really is). All we know is that it definitely // has a QueryInterface function, since all COM objects start with that one. IUnknown *audioCard; CoCreateInstance(&CLSID_IAudioCard, 0, CLSCTX_ALL, &IID_IAudioCard, (void **)&audioCard); // "audioCard" now contains a pointer to the base object (whatever it // is -- in this case, it will be an IMicIn, but we'd typically // follow up with a call to QueryInterface, passing IID_IMicIn // if we specifically want the IMicIn).
应用程序如何从基对象获取子对象
应用程序通过调用基对象(IMicIn
)的 QueryInterface
函数来获取我们的 IAudioCard
的子对象之一,并传递它想要的子对象的 VTable GUID。(现在,您可以看到 QueryInterface
函数的另一个用途,除了允许应用程序验证其对象的类型之外。QueryInterface
还用于获取多接口对象的子对象。)这意味着基对象的 QueryInterface
函数应能够识别应用程序何时传递了其他子对象 VTable 的 GUID,找到该子对象,并将指向该子对象的指针返回给应用程序。
因此,如果应用程序想要获取我们的 ISpeakerOut
对象,它必须首先获取我们的 IAudioCard
基对象。然后,应用程序必须调用该基对象的 QueryInterface
,并传递我们 ISpeakerOut
VTable 的 GUID。QueryInterface
将返回指向我们 ISpeakerOut
对象的指针。
// How an app gets IAudioCard's ISpeakerOut sub-object. // Error-checking omitted! IUnknown *audioCard; ISpeakerOut *speakerOut; // Get IAudioCard's base object. CoCreateInstance(&CLSID_IAudioCard, 0, CLSCTX_ALL, &IID_IAudioCard, (void **)&audioCard); // Now that we've got the base object (whatever it is) in "audioCard", let's // call its QueryInterface, asking for its ISpeakerOut sub-object. We call the // base object's QueryInterface, passing the GUID of ISpeakerOut's VTable, // which we'll assume has the name IID_ISpeakerOut. QueryInterface will // store this pointer in "speakerOut". audioCard->lpVtbl->QueryInterface(audioCard, &IID_ISpeakerOut, &speakerOut); // Now that we've got the ISpeakerOut, we can Release the base object. // NOTE: If the base object happened to be the ISpeakerOut, then both // "speakerOut" and "audioCard" are the same pointer (object). But // CoCreateInstance has done an AddRef on it for us. And QueryInterface // has also done an AddRef on it for us. So the Release below undoes // only one of the AddRef's. Our ISpeakerOut isn't going away, because // we still need to do one more Release on it. audioCard->lpVtbl->Release(audioCard);
应用程序如何从另一个子对象获取对象
另一条规则是,在应用程序释放了所有其他子对象之前,基对象不能被删除。如果应用程序仍然持有指向子对象的指针,基对象必须保留(即使应用程序已释放了其对基对象的引用)。为什么?这与下一条规则有关。
与往常一样,每个子对象的 QueryInterface
函数必须能够识别传递其自身 GUID 的情况。(也就是说,IMicIn
的 QueryInterface
必须识别其自身的 VTable GUID,ISpeakerOut
必须识别其自身的 VTable GUID,ILineIn
必须识别其自身的 VTable GUID。)但是,每个子对象的 QueryInterface
还必须识别其基对象的 GUID,并能够返回指向该基对象的指针。
例如,应用程序可以将我们的 IAudioCard
VTable 的 GUID 传递给 ISpeakerOut
的 QueryInterface
函数。ISpeakerOut
的 QueryInterface
必须识别这一点并返回指向基对象的指针。这应该告诉您,每个子对象都需要能够定位基对象。当然,这意味着基对象必须在任何其他子对象存在的同时存在。
// How an app gets IAudioCard's base object from // its ISpeakerOut sub-object. Error-checking omitted! // Assume that we've got a pointer to the ISpeakerOut sub-object // in our variable "speakerOut". To get the base object, we call // ISpeakerOut's QueryInterface, passing the GUID of IAudioCard's // VTable. Here, QueryInterface stores that pointer in our // variable "audioCard". IUnknown *audioCard; speakerOut->lpVtbl->QueryInterface(speakerOut, &IID_IAudioCard, &audioCard); // Note: We must audioCard->lpVtbl->Release(audioCard) when // we're done with the base object.
事实上,每个子对象都必须能够识别所有其他子对象的 VTable GUID,并定位/返回指向该其他子对象的指针。
例如,应用程序可以将我们 ILineIn
VTable 的 GUID 传递给 ISpeakerOut
的 QueryInterface
函数。ISpeakerOut
的 QueryInterface
必须识别这一点,并返回指向 ILineIn
子对象的指针。
// How an app gets the ILineIn sub-object from the ISpeakerOut sub-object. // Error-checking omitted! // Assume that we've got a pointer to the ISpeakerOut sub-object // in our variable "speakerOut". To get the ILineIn sub-object, we call // ISpeakerOut's QueryInterface, passing the GUID of ILineIn's // VTable. Here, QueryInterface stores that pointer in our // variable "lineIn". ILineIn *lineIn; speakerOut->lpVtbl->QueryInterface(speakerOut, &IID_ILineIn, &lineIn); // Note: We must lineIn->lpVtbl->Release(lineIn) when // we're done with the ILineIn.
委托
因此,我们必须确保每个子对象的 QueryInterface
都能识别所有其他子对象 VTable 的 GUID,以及我们 IAudioCard
的 VTable,并返回指向相应子对象的指针。
我们将如何实现这一点?
由于我们已经确定基对象必须识别所有不同的 VTable GUID(包括它自己的),并返回指向相应子对象的指针,因此我们的基对象的 QueryInterface
已经完成了定位/返回任何子对象的全部工作。因此,任何其他子对象的 QueryInterface
所需要做的就是调用其基对象的 QueryInterface
。换句话说,子对象的 QueryInterface
“将责任推给了”基对象。毕竟,如果子对象可以获取指向其基对象的指针,它就可以调用其基对象的 QueryInterface
,就像应用程序一样。
为了防止基对象在所有其他子对象都被释放之前消失,每当基对象返回一个子对象时,我们都必须增加其引用计数。这个引用计数将在应用程序每次释放子对象时相应地递减。这样,基对象的引用计数只有在所有指向基对象和其他子对象的未决指针都被释放后才会归零。但请注意,如果应用程序调用了某个子对象的 AddRef
函数,我们也必须增加基对象的引用计数。这样做是为了让基对象的引用计数与我们期望应用程序释放子对象的次数保持同步。因此,子对象的 AddRef
和 Release
函数将分别调用基对象的 AddRef
和 Release
函数。因此,每次其他子对象被 AddRef
时,基对象的引用计数会增加,每次子对象被 Release
时,基对象的引用计数会减少。
当子对象调用其基对象的 QueryInterface
、AddRef
和 Release
时,我们称之为“委托”。子对象正在将子对象本身应该执行的一些工作委托(给其基对象)。
我们的基对象的 QueryInterface、AddRef 和 Release
如前所述,我们可以将我们的 IAudioCard
定义为实际上将三个子对象直接嵌入其中。由于我们选择了 IMicIn
作为基对象,它必须在最前面。我们还必须向我们的对象添加一个引用计数成员。
typedef struct { IMicIn mic; // Our IMicIn (base) sub-object. ILineIn line; // Our ILineIn sub-object. ISpeakerOut speaker; // Our ISpeakerOut sub-object. long count; // The reference count. } IAudioCard;
一旦我们的 IAudioCard
对象被分配(可能是通过我们 IClassFactory
的 CreateInstance
),所有三个子对象也会同时被分配(因为它们直接存在于 IAudioCard
内部)。因此,我们可以立即初始化子对象,并将它们的 VTable 填充到它们各自的 lpVtbl
成员中。
例如,我们 IClassFactory
的 CreateInstance
可能会这样做:
IAudioCard *thisObj // Allocate the IAudioCard (and its 3 embedded sub-objects). thisobj = (IAudioCard *)GlobalAlloc(GMEM_FIXED, sizeof(IAudioCard)); // Store IMicIn's VTable in its lpVtbl member. thisobj->mic.lpVtbl = &IMicIn_Vtbl; // Store ILineIn's VTable in its lpVtbl member. thisobj->line.lpVtbl = &ILineIn_Vtbl; // Store ISpeakOut's VTable in its lpVtbl member. thisobj->speaker.lpVtbl = &ISpeakerOut_Vtbl;
在此之后,CreateInstance
将调用基对象(IMicIn
)的 QueryInterface
,以检查应用程序是否传递了我们的 IID_IAudioCard
GUID,并用基对象(IMicIn
)填充应用程序的指针。
当应用程序请求我们的基对象的 QueryInterface
提供子对象时,我们可以简单地使用指针算术在我们的 IAudioCard
对象中定位它。例如,这是 IMicIn
的 QueryInterface
如何定位 ISpeakerOut
子对象(假设“this
”是指向 IMicIn
的指针):
// Is the app asking for our ISpeakerOut sub-object? If it // passed ISpeakerOut's VTable GUID, that's the case. if (IsEqualIID(vTableGuid, &IID_ISpeakerOut)) // Just locate the ISpeakerOut object embedded directly // inside of IAudioCard *ppv = ((unsigned char *)this + offsetof(IAudioCard, speaker));
我们的子对象的 QueryInterface、AddRef 和 Release
由于每个子对象也直接存在于我们的 IAudioCard
内部,因此子对象也可以使用指针算术来定位基对象(IMicIn
)。例如,ISpeakerOut
的 QueryInterface
可以像这样调用 IMicIn
的 QueryInterface
(假设“this
”是 ISpeakerOut
):
IMicIn *base; base = (IMicIn *)((unsigned char *)this - offsetof(IAudioCard, speaker)); base->lpVtbl->QueryInterface(base, tableGuid, ppv);
向我们的对象添加子对象的另一种方法
我们可以选择向我们的对象添加子对象的另一种方法是,当应用程序第一次请求某个子对象时,让我们的基对象的 QueryInterface
分配并初始化该其他子对象。该子对象实际上直到应用程序请求它时才存在。
注意:基对象不能以这种方式实现。它必须被嵌入。
为了实现这一点,我们将为每个子对象向我们的 IAudioCard
添加一个额外的成员。该成员将是指向子对象的指针。例如,我们的 IAudioCard
对象可能如下所示:
typedef struct { IMicIn mic; // Our base (IMicIn) object. // It must be directly embedded. ILineIn *line; // A pointer to the ILineIn object. ISpeakerOut *speaker; // A pointer to the ISpeakerOut object. long count; // The reference count. } IAudioCard;
当 IAudioCard
本身被分配时,我们将这些指针清零,因为子对象尚未创建。在 IMicIn
的 QueryInterface
中,当应用程序请求其中一个子对象时,我们将分配并初始化它,然后将其指针填充到它各自的 IAudioCard
成员中。然后,我们将该指针返回给应用程序。下次应用程序再次调用我们的 QueryInterface
请求同一对象时,我们将简单地返回我们存储的相同指针。
但是,由于子对象需要能够找到基对象,因此我们需要向每个子对象添加一个额外的指针成员。该额外成员将存储指向其基对象的指针。因此,我们的 ISpeakerOut
对象可能如下所示:
typedef struct { ISpeakerOutVtbl *lpVtbl; // Our ISpeakerOut's VTable. Must be first. // NOTE: The sub-objects do not need their own reference count. // Instead, they increment/decrement the base's count. IMicIn *base; // A pointer to the base object. } ISpeakerOut;
在 IMicIn
的 QueryInterface
分配 ISpeakerOut
对象后,IMicIn
将立即将指向自身的指针填充到 ISpeakerOut
子对象的 base
成员中。
例如,IMicIn
的 QueryInterface
可能如下返回 ISpeakerOut
子对象:
// Is the app asking for our ISpeakerOut sub-object? If it // passed ISpeakerOut's VTable GUID, that's the case. if (IsEqualIID(vTableGuid, &IID_ISpeakerOut)) { IAudioCard *myObj; // Because our IMicIn is the base object, "this" is effectively // pointing to our IAudioCard too. myObj = (IAudioCard *)this; // If we've already allocated the ISpeakerOut, then our // IAudioCard->speaker member points to it. We just need // to return that pointer. if (!myObj->speaker) { // We didn't allocate the ISpeakerOut yet. Let's do so now, // and save the pointer in our IAudioCard->speaker member. if (!(myObj->speaker = (ISpeakerOut *)GlobaAlloc(GMEM_FIXED, sizeof(ISpeakerOut)))) return(E_OUTOFMEMORY); // Set the base member. myObj->speaker->base = this; // Set ISpeakerOut's VTable into its lpVtbl member. myObj->speaker->lpVtbl = &ISpeakerOut_Vtbl; } // Return the ISpeakerOut sub-object. *ppv = myObj->speaker; }
当应用程序最终释放我们的 IAudioCard
对象及其所有子对象时,我们需要在(IAudioCard
的 Release
中)检查这些指针成员,并释放任何现有的子对象(在我们释放我们 IAudioCard
本身之前)。
现在,当我们的 ISpeakerOut
的 QueryInterface
需要调用其基对象的 QueryInterface
时,ISpeakerOut
所需要做的就是(假设“this
”是指向 ISpeakerOut
的指针):
this->base->lpVtbl->QueryInterface(this->base, tableGuid, ppv);
您选择是将子对象直接嵌入到您的对象中,还是只在对象中放置一个指向子对象的指针然后单独分配子对象。单独分配子对象意味着大型子对象不需要存在,直到/除非应用程序实际需要它们。因此,如果某个子对象有很多私有数据成员,这也不会浪费内存。另一方面,嵌入子对象可以节省为每个子对象添加额外指针成员的麻烦,如果应用程序很可能会请求那些子对象,那么它们在父对象创建时就应该存在。
事实上,您甚至可以混合使用这些技术,让一些子对象嵌入,另一些单独分配。
一个具有多个接口的示例对象
让我们创建一个具有多个接口的对象。我们将这个对象称为 IMultInterface
,并创建一个名为 IMultInterface.DLL 的 DLL 来包含我们的对象。我们的对象将包含一个基对象,我们称之为 IBase
,以及两个额外的子对象,分别称为 ISub1
和 ISub2
。为了说明起见,我们将 ISub1
直接嵌入到 IMultInterface
中(当然,我们的 IBase
也必须嵌入在最前面),但我们将单独分配 ISub2
。
在 IMultInterface 目录中,您将找到源文件。
我们不会将所有三个子对象的函数放在一个源文件中,而是将它们放在三个单独的文件中,名为 IBase.c、Sub1.c 和 Sub2.c。
IMultInterface.h 包含三个子对象的定义以及所有 GUID。
仅为说明起见,我在 IBase
中添加了一个额外的函数,名为 Sum
。应用程序向此函数传递两个 long
值,它将返回这两个值的和(在应用程序提供的指向 long
的指针中)。
我在 ISub1
中添加了一个名为 ShowMessage
的额外函数。应用程序向此函数传递一个字符串,它将显示一个消息框。
我在 ISub2
中添加了三个额外的函数,名为 Increment
、Decrement
和 GetValue
。第一个函数递增 ISub2
的一个 long
成员。第二个函数递减该 long
成员。最后一个函数检索其当前值。
我在一个名为 IMultInterface2.h 的单独的 include 文件中定义了我们的 IMultInterface
对象。为什么?因为应用程序将 #include
我们的 IMultInterface.h 文件,而我们不希望应用程序了解 IMultInterface
的实际内部结构。因此,我们将后者信息放在一个单独的 .H 文件中,该文件将仅由我们的 IBase.c、ISub1.c 和 ISub2.c 文件 #include
。
我将我们的 IClassFactory
放在 IBase.c 中。如果您仔细查看 IBase.c,您会注意到这个基对象与我们第一个章节中的 IExample
对象几乎完全相同。事实上,很多代码是从 IExample.c 复制过来的,并且删除了重复代码中的注释。留在 IBase.c 中的注释涉及到添加 ISub1
和 ISub2
作为 IMultInterface
的子对象需要做什么。这里真的没有太多新东西。最大的变化在于基对象的 QueryInterface
和 Release
函数。IBase
的 QueryInterface
、AddRef
和 Release
函数被重命名为在前面加上 IBase_,并且移除了 static
关键字。由于 ISub1
和 ISub2
的代码将在单独的文件中,并且它们需要调用基对象的 QueryInterface
、AddRef
和 Release
,因此我们需要将这些函数设为全局函数,并避免名称冲突。
此外,我们对 IClassFactory
的 CreateInstance
进行了一些小的调整。
将此代码编译到 DLL IMultInterface.dll 后,可以通过修改我们在第一个章节中创建的安装程序(其源文件位于 RegIExample 目录)来注册它。只需将所有 IExample
的实例替换为 IMultInterface
。您可以通过对 UnregIExample 进行相同的操作来创建卸载程序。
一个使用我们对象的 C 语言应用程序示例
在 IMultInterfaceApp 目录中,有一个使用我们 IMultIterface
对象(即其三个子对象)的 C 语言应用程序示例。请仔细阅读代码中的注释,了解应用程序如何获取基对象,以及如何从另一个子对象获取任何子对象。
下一步?
如前所述,VBScript 或 JScript 等脚本语言无法直接使用具有多个接口的对象。为什么?因为为了获取子对象,脚本必须能够调用 QueryInterface
函数。要调用 QueryInterface
,脚本必须引用 VTable 的指针(我们对象的 lpVtbl
成员)。但是 VBScript 和 JScript 无法访问指针。
那么,这是否意味着多接口对象对 VBScript 或 JScript 完全无用?不一定。VBScript 和 JScript 引擎本身可以在脚本的代表下调用我们对象的 QueryInterface
。引擎会在什么特定情况下这样做吗?是的 — 在连接“事件接收器”时,这将是下一章的主题。