This article may contain URLs that were valid when originally published, but now link to sites or pages that no longer exist. To maintain the flow of the article, we've left these URLs in the text, but disabled the links.
|
Dynamic Object Composition Using IDispatchEx
Joe Graf |
Sometimes a predefined COM component just doesn't fit your needs. With IDispatchEx, you can complete your interfaces at runtime. |
Most Web application developers are familiar with the document object model of their favorite browser. These object models are dynamically built from the elements in the Web pages themselves, giving the programmer full access to any edit boxes or radio buttons on the page. This gives the programmer a way to validate form input, calculate new field values from entered values, or generate new HTML based on the objects and their content (using DHTML). These powerful features are possible due to the runtime composition of the object model and late binding of the script code to the objects.
Late binding of objects is not new to the Internet. It has been around since the inception of Visual Basic® and OLE Automation (now just known as Automation). Automation provides a mechanism for coding to a set of object semantics without having to know the calling convention or the exact location of the method in an object; these are queried at runtime. With early binding languages such as C/C++, all of the object's methods and properties must be known at compile time. Scripting languages take the concept of late binding to its extreme: properties or methods of an object are queried just before they are executed. If the object does not support the requested member, an error is generated (Visual Basic returns errors, too). JScript® even allows members to be added to an object at runtime. This means that if a property does not exist, it can be added so that the line of source is not in error. You can also add JScript functions as methods to an object to build object-oriented script code. You can take advantage of these dynamic object features using your favorite languageexcept if your favorite is Visual Basic and VBScript.
Why Dynamic Objects?
|
|
instead of |
|
It also allows the consumer of an object to add dynamic data or behavior to the object that is context-specific or determined at runtime. For instance, in this code |
|
NewProp did not previously exist for the object and is of type string, and AnotherNewProp did not exist and is of type integer. The object programmer does not know about these properties, but the object carries them around to be used as the script programmer sees fit.
If the script programmer has access to a purely dynamic object, he can build objects out of script code that can then be passed to other objects or scripts for use. |
|
This codeadd it to an HTML page to see it in actioncreates a new object that is empty of properties or methods. It assigns the text "My name is John Doe" to the property Name, creating the property in the process. The Show method is added by assigning the function Test to it. This is essentially the same thing as assigning a function pointer. For the scripting engines, a function pointer is an LPDISPATCH that uses the DISPID of zero to invoke it. Notice this.Name being used as an argument from within the function Test. This dereferences the object to which the Test method is assigned, giving it the dynamically added property Name. If you call the Test function directly, the message box contains the text "undefined" since there is no valid this pointer.
Now that I've shown you why you would want to use dynamic objects, and some examples of using them within JScript, let's take the next step and discover how to create COM objects that support dynamic composition.
IDispatch and IDispatchEx
|
Figure 1: IDispatchEx Hierarchy |
As you can see from the interface definition, a number of new features are available with this interface. IDispatch is not as flexible in its processing of DISPIDs as IDispatchEx. The ability to iterate through the members of an object, determine a single DISPID from a name, or determine the name of a member by DISPID makes it easier for a consumer of an object to determine its capabilities. Of course, you can do the same things using the IDispatch interface, but it requires getting the ITypeInfo interface and using that to iterate through the members. The IDispatch approach works, but is more cumbersome. Aside from the member discovery features of IDispatchEx (see Figure 2 for a description of each member), it also implements a number of additional features as described in Figure 3. There are a few caveats surrounding the deletion of members. Though the IDispatchEx interface allows an object's set of members to be dynamic, there is a contract between a given name and its corresponding DISPID: once a name has been assigned a DISPID, it can never change or be reused by a member with a different name. This means that a deleted member's DISPID cannot be assigned to a newly added member. It also means that if the member is added to the object again, the DISPID previously assigned to it must be reissued. These requirements are due to the possibility of a consumer caching DISPIDs via the IDispatch interface (commonly done for performance reasons). Another consideration is that all of the DISPIDs passed out via the IDispatchEx interface must be valid for the IDispatch interface of that object. The easiest way to ensure this is to implement the IDispatch interface as part of your IDispatchEx implementation. Next I'll describe a template-based class that does just that.
IDispatchExImpl
|
Figure 4: IDispatchExImpl Hierarchy |
IDispatchExImpl implements both the IDispatch and IDispatchEx interfaces. It uses the CDynamicDispatchEntry class to hold the state information for a given dispatch member. The CDynamicDispatchEntry is aggregated in three MFC map classes for fast lookups of member entries. One of the maps provides case-sensitive lookups, another case-insensitive lookups, and the third provides for lookups based on DISPIDs. DISPIDs start from zero, although this default is easily changed by defining the macro DISPID_START n, where n is the DISPID to start with, before including IDispatchExImpl.h. Figure 5 lists all of the IDispatchExImpl members. The next available DISPID is contained in the member m_dispidNext and is used by the CreateNewEntry method to determine the DISPID of the new entry. Entries deleted by consumers are not actually removed from the map, but are marked as deleted. This allows for the revival of the same member later if the consumer desires (as required by the interface contract). Calls to the IDispatch members GetIDsOfNames and Invoke are forwarded under the covers to the IDispatchEx implementations of GetDispID and InvokeEx. This class is also thread-safe for use in freethreaded applications. IDispatchExImpl is implemented as a template class. This is in keeping with the tradition of ATL. Yes, MFC is used instead of STL for the map classes as some would prefer, but this is a matter of personal taste. Beyond just being ATL-like, implementing the class as a template has some real benefits: the ability to support interface inheritance, such as interfaces that are derived from IDispatchEx and contain their own members. The IDispatchImpl provided by ATL does this for dual COM interfaces and was the source of inspiration for this feature. By passing type information as template parameters, the class is able to support a derived class's members as dynamic dispatch entries. Any dynamic dispatch entries that are from a child interface are invoked using the ITypeInfo:: Invoke method. This allows the ITypeInfo interface to validate parameters and call the correct function in the vtable rather than having to hand-code this processing, which would be very complex. If you delve into ATL's IDispatchImpl, you will find that it does the same thing via CComTypeInfoHolder. To have the type information loaded into the object at creation time, a custom class factory (CDispatchExClassFactory) class is used. Unlike ATL's IDispatchImpl class, which loads type information the first time its CComTypeInfoHolder pointer is accessed, this class is preloaded to avoid using extra cycles to check whether the type information is valid. This is done automatically in the overloaded CreateInstance of the CDispatchExClassFactory by calling TypeInfoInit on the newly created object. The type information is read into an intermediate ITypeLib interface from the registry using the parameters passed into the template. From ITypeLib, the ITypeInfo interface is requested using the IID of the COM object's derived interface. With the ITypeInfo interface in hand, an internal helper function, AddTypeInfoDispIDs, is called. This method iterates through all of the members in ITypeInfo, adding them to the dynamic dispatch maps so that they can be called via the IDispatchEx interface. This process also sets the starting DISPID for dynamic members to be one greater than the largest DISPID in the ITypeInfo interface, preventing DISPID collisions between static (part of an interface that derives from IDispatchEx) and dynamic members. However, if you want a pure IDispatchEx COM object, just declare the macro IDISPATCHEX_NO_TYPEINFO and all type information will be compiled out of the class. An object of this type will have no default properties or methods.
Using IDispatchExImpl
|
|
This code creates an instance of the DynamicOnly object. Two properties are then supplied: one is in mixed case and the other is all lowercase. A method is added to the object by means of a function pointer. The newly added function is executed to illustrate calling a JScript function from within a C++ COM object. This method displays the two properties, last name followed by first. There is an intentional mistake on the second alert call to show what happens when a member cannot be found due to case inconsistencies.
The second COM object sample provides the same functionality as DynamicOnly, and also adds its own custom methods to provide an example of calls being forwarded to ITypeInfo.
Inheritance from IDispatchEx
|
|
This object illustrates the use of static and dynamic members on the same object. It also provides an example of using the ITypeInfo interface to forward calls to the correct function in the vtable. This COM object uses CDispatchExClassFactory to automatically process the type information at creation time. If the IDispatchExImpl class is unable to load the type information, the object will fail to create.
If you are using this implementation of IDispatchEx in your object and you internally use CComObject::CreateInstance to create the object, you need to call the TypeInfoInit method or the code will assert when the ITypeInfo pointer is accessed. This is necessary since the CComObject::CreateInstance bypasses the class factory of the object. The same holds true if you create the object with the new operator. When the script code invokes one of the static methods, the call is forwarded to the ITypeInfo pointer. Here is a snippet from the IDispatchExImpl::InvokeEx method showing how this is done: |
|
Notice the first parameter is the object's this pointer. The parameter provides to the ITypeInfo a pointer to the vtable of the object that is being called. The ITypeInfo interface does the offset calculation and invokes the function at the correct vtable index using the specified pointer. With some clever coding, that pointer need not be an object's this pointer. It could be an array of function pointers that was built dynamically, though the code to do this is tricky. However, doing this would allow a programmer to aggregate methods of multiple objects and expose them as one object. The code to accomplish this is beyond the scope of this article.
Now that we can forward the calls to the ITypeInfo interface, let's look at some sample JScript code that uses this feature. The code in Figure 7 shows both DynamicOnly and TestDispatchEx objects. Named properties on the TestDispatchEx object are modified using the Get and put_Set custom methods. Note that the methods are case-insensitive for compatibility with VBScript. Some dynamic properties are added to the objects, including the setting of a function pointer. When this function is invoked, more dynamic properties are added, which are displayed upon return from the function. The call to the function pointer illustrates the passing of parameters to a dynamic method. Finally, the static members of the TestDispatchEx object are called. Run this sample in the browser and set a breakpoint in the InvokeEx method. Interrogate the method parameters to see how the scripting engine packages everything for you. Set breakpoints in CTestDispatchEx to see the forwarding of the calls in action. By experimenting with these samples provided with the project, you can explore the range of features that IDispatchEx gives you.
Conclusion
|
http://msdn.microsoft.com/library/partbook/dhtml/theruntimeengineforserverscriptlets.htm and http://msdn.microsoft.com/library/techart/msdn_xmlscripts.htm |
From the October 1999 issue of Microsoft Internet Developer.
|