Original article: http://www.codeproject.com/Articles/14117/COM-in-plain-C-Part-4
Use C to construct COM objects with multiple interfaces
Download routine-194kb
Content
- Embed sub-objects in our objects
- How the application obtains the Base Object
- How can an application obtain sub-objects through a base object?
- How can an application obtain another sub-object from a child object?
- How the application obtains the object of our collection
- Delegate
- The QueryInterface, addref, and release of our base object
- QueryInterface, addref, and release of our sub-objects
- Another method for adding sub-objects to our objects
- A multi-interface object routine
- Use the c Application routine of our object
- What is next?
Introduction
Sometimes, a COM object may have multiple interfaces. A com object has multiple interfaces, that is, this object is composed of several "sub-objects. Each sub-object is a complete COM Object and has its own lpvtbl member (pointing to its own vtable ), it also has its own vtable function set (including its own QueryInterface, addref, and release functions ).
An object can have many types of sub-objects. For example, we have an object for managing sound cards. There may be several input and output ports on a sound card. We need to allocate a subobject to each input and output plug-in. Suppose we have an input wire plug-in, a microphone input plug-in, and a speaker output port, so we can have three sub-objects: ilinein, imicin, and ispeakerout. Each single object can have a function that controls its own plug-in. For example, a microphone input port may have a device that can switch between high and low impedance microphones, so our imicin object can have a setimpedance function. But the ilinein and ispeakerout objects do not need such functions, because the device has nothing to do with those plug-ins. Maybe the ilinein and ispeakerout objects need other functions to control those plug-ins, maybe ilinein has a mute signal called the mute function, and our ispeakerout has a sound called setvolume to set it. Here we use the macro defined by Microsoft to define the COM object to define these three objects.
#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; };
Note that the vtable of each sub-object must start with its own QueryInterface, addref, and release functions like all COM objects. After that, we will list some additional functions in the vtable of the object. Imicin has its setimpedance function, ilinein has its mute function, and ispeakerout has its setvolume function. (For this example, I have defined that they accept a long value parameter. In setimpedance, this parameter can be an impedance value. For mute, it can be set to 1 or 0 if it is not set to mute. The parameter setvolume can be the value of this sound)
Note:I didn't add the idispatch function to these sub-objects (nor did I specify that their vtable is based on idispatch ). Instead, I ignore the idipatch function and specify that the vtable is based on iunknowm. Therefore, these sub-object functions, such as VBScript or JScript, cannot be directly accessed. This is okay, because those scripting languages are not designed to use multi-interface objects.
At the same time, remember that the macro above will automatically define that the object itself has a member lpvtbl-pointer to its vtable:
typedef struct { IMicInVtbl *lpVtbl; }IMicIn; typedef struct { ILineInVtbl *lpVtbl; }ILineIn; typedef struct { ISpeakerOutVtbl *lpVtbl; }ISpeakerOut;
Embed sub-objects in our objects
There are several rules for sub-objects. A child object is treated as a "Base Object", and its vtable pointer must be the first real member of our object. For example, let's use our imicin sub-object as the base object, so that we define our object (we call it iaudiocard), embed the imicin sub-object to the first position, and then embed other sub-objects:
Typedef struct {imicin mic; // Our imicin (base) Sub-object. Ilinein line; // Our ilinein sub-object. Ispeakerout speaker; // Our ispeakerout sub-object. } Iaudiocard;
Remember that the first real member of imicin is its lpvtbl (that is, its vtable pointer ). Because our imicin is directly embedded at the beginning of our iaudiocard object structure, this means that the first real member in our iaudiocard object is actually a pointer to the imicin vtable, in this way, imicin becomes a base object. Therefore, iaudiocard is actually a pointer to a vtable (because imicin starts with it). Its vtable actually starts with the QueryInterface, addref, and release functions.
Another rule is that the vtable of each sub-object must have its own guid. Therefore, we need to run guidgen. EXE to create a guid for the vtable of imicin, ilinein, and ispeakerout. At the same time, we also need to give our iaudiocard object a guid.
How the application obtains the Base Object
Generally, an application can call the createinstance of iclassfactory to obtain the Base Object (imicin) by passing in the guid of iaudiocard ). (Or to get our base object, the application may call another additional function that we add to our object, as in the first part of this series, we return our collection object or ienumvariant object ). The createinstance of our iclassfactory will return a pointer to our base object.
// Obtain the basic object of iaudiocard. Ignore error check! // Contains the. h file that defines our iaudiocard sub-object vtable and all required guids # include "iaudiocard. H" // first, we need to obtain the base object of iaudiocard. Let's assume that we get it by calling cocreateinstance. We must pass in the guid of the DLL that contains the iaudiocard object. Assume that the guid has been named clsid_iaudiocard. h In // iaudiocard. We must also pass in the guid of the iaudiocard // object. Here we assume it is called iid_iaudiocard. Cocreateinstance returns a // pointer to the iaudiocard base object. We store it in our audiocard variable. // Note that we declare the audioncard variable as the iuknown * type because we do not know which type of object the base object is. (Well, it will be an imicin object. But if this DLL is written by someone else, // we may not know the actual type of the base object ). All we know is that it must have the QueryInterface function, // because all COM objects start with it. Iunknown * audiocard; cocreateinstance (& clsid_iaudiocard, 0, clsctx_all, & iid_iaudiocard, (void **) & audiocard ); // "audiocard" now contains a pointer to the Base Object (what is it-here, it is an imicin, // but we usually call QueryInterface, if we explicitly want imicin, // input iid_imicin ).
How can an application obtain sub-objects through a base object?
The application obtains a sub-object of our iaudiocard by calling the QueryInterface function of the Base Object (imicin) and passing in the guid of the vtable of the sub-object it needs. (Now, you may know that the QueryInterface function allows the application to check what type of object it is ). This means that when the application passes in the guid of the vtable of another sub-object, the QueryInterface function of the base object should be able to recognize this GUID, locate the sub-object and return (to the Application) a pointer to the sub-object.
Therefore, if an application wants to obtain our ispeakerout object, it must first obtain the base object of our iaudiocard. Then, the application must call the QueryInterface of the Base Object and pass in the guid of our ispeakerout vtable. QueryInterface returns a pointer to our ispeakerout object.
// The application obtains the ispeakerout sub-object method of iaudiocard. The error check is ignored! Iunknown * audiocard; ispeakerout * speakerout; // gets the base object of iaudiocard. Cocreateinstance (& clsid_iaudiocard, 0, clsctx_all, & iid_iaudiocard, (void **) & audiocard); // now we have the base object "audiocard" (whatever it is ). Call its QueryInterface, // request its ispeakerout sub-object. We call the QueryInterface of the Base Object and pass in the guid of the ispeakerout // vtable. We assume that its name is iid_ispeakerout. Quryinterface // stores the pointer in "speakerout. Audiocard-> lpvtbl-> QueryInterface (audiocard, & iid_ispeakerout, & speakerout); // now we have obtained ispeakerout, And we can release this base object. // Note: if the base object happens to be this ispeakerout, then "speakerout" and "audiocard" // are the same pointer (object ). But cocreateinstance has done addref for us. // At the same time, QueryInterface also implements an addref for it. Therefore, this release operation only revokes the/addref operation once, and our ispeakerout is not invalid. Therefore, we need to make another release for it. Audiocard-> lpvtbl-> release (audiocard );
How to get another sub-object from a sub-Object
Another rule is that the base object cannot be deleted before all other sub-objects are release by the application. If the application obtains the pointer to the sub-object, the base object must be retained (even if the application release the base object it obtains ). Why? Well, this is to be consistent with the next rule.
Generally, the QueryInterface function of each sub-object must recognize its own guid. (That is, imicin's QueryInterface must recognize its own vtable guid, ispeakerout must recognize its own vtable guid, and ilinein must recognize its own vtable guid ). However, the QueryInterface of each sub-object must also recognize its base object guid and return a pointer to the base object.
For example, an application may pass in the guid of our iaudioncard vtable to the QueryInterface function of isperakerout. the QueryInterface of ispeakerout must recognize it and return a pointer to the base object. This indicates that each sub-object must be able to locate the base object. Of course, this means that the base object must exist when the sub-object exists.
// The method by which the application obtains its base object from the ispeakerout sub-object of iaudiocard. Ignore the error check. // Assume that we have obtained the pointer to the ispeakerout sub-object and placed it in our "speakerout. // To get the base object, we call the QueryInterface of ispeakerout and pass in the guid of the iaudiocard // vtable. At this point, QueryInterface stores pointers in our "audiocard" variable. Iunknown * audiocard; speakerout-> lpvtbl-> QueryInterface (speakerout, & iid_iaudiocard, & audiocard); // note: when using the base object, we must use audiocard-> lpvtbl-> release (audiocard ). In fact, each sub-object must be able to recognize the guid of the vtable of other sub-objects, and be able to locate and return pointers to other sub-objects. For example, the application may pass the guid of our ilinein vtable to the QueryInterface function of ispeakerout. Queryinerface of ispeakerout must recognize it and return a pointer to the ilinein sub-object. // The method by which the application obtains the ilinein sub-object from the ispeakerout sub-object. The error check is ignored! // Assume that we have obtained the pointer to the ispeakerout sub-object to our "speakerout" variable. // To obtain the ilinein sub-object, we call the QueryInterface of ispeakerout and pass in the guid of the ilinein // vtable. At this point, QueryInterface stores the pointer to our "linein" variable. Ilinein * linein; speakerout-> lpvtbl-> QueryInterface (speakerout, & iid_ilinein, & linein ); // when we use ilinein, linein-> lpvtbl-> release (linein) is required ).
Delegate
Therefore, we must ensure that the QueryInterface of each sub-object can recognize the vtable of another sub-object and the guid of the vtable of iaudiocard, and return a pointer to the corresponding sub-object.
How can we do this?
Because we have defined that a base object must recognize the guid (including its own) of all different vtables and return a pointer to the appropriate sub-object, the QueryInterface of our base object has done all the work of locating and returning sub-objects. Therefore, the QueryInterface of all other sub-objects needs to call the QueryInterface of its base object. In other words, the QueryInterface of the sub-object "shirk responsibility" to the base object. After all, if a sub-object can get a pointer to the base object, it can call the queryinteface of its base object as the application does.
At the same time, in order to ensure that the base object does not disappear before all other sub-objects are released, as long as the Base Object returns a sub-object, we must increase its reference count once. This reference count will be reduced each time the application release sub-object. In this way, the reference count of the base object is not zero until the base object and other sub-objects are not completely release (outstanding pointer. But note that if the application calls the F function of the sub-object, we must also increase the reference count of the base object. The reference count of the base object must be consistent with the number of times that we think the application release sub-object is used. Therefore, the addref and release functions of sub-objects should also call the addref and release functions of the Base Object respectively. In this way, the reference count of the base object is added to 1 after each sub-object addref, and 1 after each sub-object release.
When a sub-object calls the QueryInterface, addref, and release of its base object, we call this a delegate ). The sub-object delegates the work it should do (to its base object ).
The QueryInterface, addref, and release of our base object
As previously, we defined our iaudiocard and embedded three child objects in it. Because we have selected imicin as the base object, it must be the first. We must also add a reference counting member to our object.
Typedef struct {imicin mic; // Our imicin (base) Sub-object. Ilinein line; // Our ilinein sub-object. Ispeakerout speaker; // Our ispeakerout sub-object. Long Count; // reference count. } Iaudiocard;
Once we allocate an iaudiocard object (possibly through createinstance of iclassfactory), the three child objects are also allocated (because they exist directly inside iaudiocard ). Therefore, when initializing sub-objects, we can use their vtable to fill in their respective lpvtbl members.
For example, the createinstance of our iclassfactory can do this:
Iaudiocard * thisobj // allocate iaudiocard (and its three embedded sub-objects) thisobj = (iaudiocard *) globalalloc (gmem_fixed, sizeof (iaudiocard )); // store the vtable of imicin to its lpvtbl member thisobj-> MIC. lpvtbl = & imicin_vtbl; // store the ilinein vtable to its lpvtbl member thisobj-> line. lpvtbl = & ilinein_vtbl; // store the ispeakout vtable to its lpvtbl member thisobj-> speaker. lpvtbl = & ispeakerout_vtbl;
When the createinstance calls the QueryInterface of the Base Object (imicin) to check the guid of the iid_iaudiocard passed in by the application, the Base Object (imicin) is used to fill in the pointer of the application.
When an application requests a subobject from the QueryInterface of our base object, we can use pointer calculation to locate it in iaudiocard. For example, this is the method for imicin QueryInterface to locate the ispeakerout sub-object (assuming "this" is a pointer to imicin ):
// Is the ispeakerout sub-object requested by the application? Assume that it passes in the guid of the vtable of ispeakerout. If (isequaliid (vtableguid, & iid_ispeakerout) // you can directly locate the ispeakerout object in iaudiocard * bp=( (unsigned char *) This + offsetof (iaudiocard, Speaker ));
QueryInterface, addref, and release of our sub-objects
Because each sub-object also exists directly inside our iaudiocard, the sub-object can also be located using pointer calculation (imicin ). For example, the QueryInterface of ispeakerout can call the queryinerface of imicin (assume that "This" is ispeakerout) as follows ):
IMicIn *base; base = (IMicIn *)((unsigned char *)this - offsetof(IAudioCard,speaker)); base->lpVtbl->QueryInterface(base, tableGuid, ppv);
Another method for adding sub-objects to our objects
Another method we can choose to add sub-objects to our objects is that when the application requests sub-objects for the first time, we can let the QueryInterface of our base object allocate and initialize other sub-objects. The sub-object does not exist before the application request.
Note:The base object cannot be implemented in this way. It must be embedded.
For convenience, we add an additional member to each sub-object in iaudiocard. This member is a pointer to the sub-object. For example, the iaudiocard object may be like this:
Typedef struct {imicin mic; // our base object (imicin ). It must be embedded. Ilinein * line; // pointer to the ilinein object. Ispeakerout * speaker; // pointer to the ispeakerout object. Long Count; // reference count. } Iaudiocard;
When the iaudiocard itself is allocated, we need to set these pointers to 0, because the sub-object has not yet been created. In the QueryInterface of imicin, when an application requests a sub-object, we call globalalloc and initialize it, and then fill in the pointer pointing to the sub-object to its iaudiocard members. Then, we return the pointer to the application. When the application calls QueryInterface to request this object next time, we only return the same pointer that has been stored.
However, to locate the base object for the sub-object, we need to add an additional pointer member for each sub-object. This extra member is used to store its base object pointer. Therefore, our ispeakerout object looks like this:
Typedef struct {ispeakeroutvtbl * lpvtbl; // The vtable of our ispeakerount. Must be in the first place. // Note: the sub-object itself does not require reference counting. Instead, they increase or decrease the count of the base object. Imicin * base; // pointer to the base object. } Ispeakerout;
After the QueryInterface of imicin assigns an ispeakerout object, imicin immediately fills in its own pointer to the base Member of the ispeakerout base object.
For example, the QueryInterface of imicin may return an ispeakerout sub-object as follows:
// Is the ispeakerout sub-object requested by the application? Assume that it passes in the guid of the/vtable of ispeakerout. If (isequaliid (vtableguid, & iid_ispeakerout) {iaudiocard * myobj; // because imicin is a base object. "This" actually points to iaudiocard. Myobj = (iaudiocard *) This; // if we have allocated ispeakerout, our iaudiocard-> speaker Member points to it. // We only need to return this pointer. If (! Myobj-> speaker) {// We have not allocated ispeakerout. Now we allocate it and store the pointer in the // iaudiocard-> speaker member. If (! (Myobj-> speaker = (ispeakerout *) globaalloc (gmem_fixed, sizeof (ispeakerout) Return (e_outofmemory); // set the base member. Myobj-> speaker-> base = This; // set the ispeakerout vtable to its lpvtbl member. Myobj-> speaker-> lpvtbl = & ispeakerout_vtbl;} // returns the ispeakerout sub-object. * GMM = myobj-> speaker ;}
When the application finally releaseiaudiocard and all its sub-objects, we need to check the existence of these pointer members (in the iaudiocard release) and globalfree sub-objects (before our globalfree iaudiocard ).
Now, when the QueryInterface of ispeakerout needs to call the queryinerface of its base object, what ispeakerout needs to do is (assuming "this" is a pointer to ispeakerout ):
this->base->lpVtbl->QueryInterface(this->base, tableGuid,ppv);
Embed the child object directly into your object, or place the pointer to the child object in the object and assign the child object separately. You can choose either of them. Allocating sub-objects independently means that it does not exist before the application really requires a large object. Therefore, if a specific sub-object has many private data members, there is no unnecessary memory consumption. On the other hand, embedding sub-objects saves Extra pointer members pointing to each sub-object. At the same time, no matter when the application requests these sub-objects, they exist as long as their parent objects are created.
In fact, you can even mix these technologies to embed some sub-objects, and assign them separately.
Multi-interface object routine
We create a multi-interface object. We call this object imultinterface and create a DLL containing our object named imultinterface. dll. Our object contains a base object named IBASE. Two additional sub-objects are called isub1 and isub2. To make it clearer, we embed isub1 directly into the imultinterface (of course, iBase must also be embedded in the start part), and assign isub2 separately.
You can find the source file in the imultinerface directory.
Instead of placing the functions of the three sub-objects in a source file, we put them in three separate files, iBase. C, sub1.c, and sub2.c. This is better. Multinterface. h contains the definition of three sub-objects and all guids.
To make it clearer, I add an additional function named sum in IBASE.
I added an additional function named showmessage in isub1. The application can pass in a string to this function, which will display a message box.
In isub2, I add three additional functions named increment, decrement, and getvalue. The first function is used to add a long member of isub2. The second function is used to reduce the long member. The final function is used to obtain the current value of this Member.
I have defined the imultinterface object in an included file named imultinterface2.h. Why? Because the application will # include our imultiinterface. h file, and we do not want the application to know the real internal structure of imultiinterface. Therefore, we put the following information in a separate. h file, which will only be included by our IBASE. C, isub1.c, and isub2.c files # include.
I put our iclassfactory in IBASE. C. If you take a closer look at IBASE. C, you will notice that this base object is very similar to the iexample object in Chapter 1. In fact, a large amount of code is copied from iexample. c one by one, and the comments in the copied code are deleted. In IBASE. C, the remaining annotations add isub1 and isub2 as sub-objects to the imultinterface section. There are actually no new things here. The biggest change is the QueryInterface and release functions of the base object. The IBASE QueryInterface, addref, and release functions are renamed with the added IBASE _ prefix and the static function is deleted. Since the code of isub1 and isub2 is in separate files, they need to call the QueryInterface, addref, and release functions of the base object. We need to make these functions Global to avoid name conflicts.
We also need to make small adjustments to createinstance of iclassfactory.
After you compile the code into an imultinterface. dll, you can register it by modifying the installer in chapter 1 (the source code is in the regiexample directory. Replace all iexample with imultinterface. You can create an uninstall program by performing the same operation on unregiexample.
Use the c Application routine of our object
The imultinterfaceapp directory is a C Application routine that uses our imutiinterface object (that is, its three child objects. After carefully reading the comments in the code, you will understand how the application obtains the base object and other sub-object objects through another sub-object.
What is next?
As mentioned earlier, multi-interface objects cannot be directly used by scripting languages (such as VBScript and JScript. Why? To obtain a subobject, the script must be able to call the QueryInterface function. To call QueryInterface, the script must involve a pointer to the vtable (the lpvtbl member of our object ). However, VBScript and JScript do not have the concept of how to access a pointer.
So this means that multi-interface objects are useless for VBSCRIPT or JScript? Of course not. VBScript and the JScript engine can call the QueryInterface of our object for the script. What are the special requirements of the engine? Yes-it needs to be mounted to the "event receiver", and the content will be the topic of the next section.