- Rule 2-1: Think in Terms of Interfaces
- Rule 2-2: Use Custom Interfaces
- Rule 2-3: Define Custom Interfaces Separately, Preferably Using IDL
- Rule 2-4: Avoid the Limitations of Class-Based Events with Custom Callbacks
- Rule 2-5: Be Deliberate About Maintaining Compatibility
- Rule 2-6: Choose the Right COM Activation Technique
- Rule 2-7: Beware of Class_Terminate
- Rule 2-8: Model in Terms of Sessions Instead of Entities
- Rule 2-9: Avoid ActiveX EXEs Except for Simple, Small-Scale Needs
Rule 2-6: Choose the Right COM Activation Technique
In VB, the traditional mechanism for creating an object is the New operator. For example,
Set rEmp = New Employees.CConsultant
creates a new instance of the CConsultant class. However, this is not the only alternative. If the class is registered as a COM component, you can also use CreateObject and GetObject:
Set rEmp2 = CreateObject("Employees.CConsultant") Set rEmp3 = GetObject("", "Employees.CConsultant")
On the other hand, if the class is configured to run under MTS, then you should probably be using CreateInstance instead:
Set rEmp4 = GetObjectContext.CreateInstance( _ "Employees.CConsultant")
Finally, as if this wasn't enough, when you are writing server-side ASP code you will want to use Server.CreateObject:
Set rEmp5 = Server.CreateObject( _ "Employees.CConsultant")
Do you know when to use each technique? If not, then read on . . .
First off, let's clear up a common misconception about how clients bind to objects at run-time.16 Logically, the situation is depicted in Figure 2.18. Clients hold references to interfaces and use these references to access objects. Physically, however, the binding mechanism used between client and object can vary, depending on how the reference variables are declared in the client. There are two main approaches: vtable-binding and late-binding. The former is more efficient, because the client is bound directly to an interface's implementation. This is available only in compiled environments such as VB, VC++, and soon ASP.Net. For example, the following two declarations dictate vtable-binding (1) to the default interface of class CConsultant and (2) to the custom interface IEmployee:
Dim rEmp1 As Employees.CConsultant '** (1) default interface Dim rEmp2 As Employees.IEmployee '** (2) custom interface
This is true regardless of how the objects are created. Regardless of whether the client uses New or CreateObject or some other mechanism, rEmp1 and rEmp2 are vtable-bound to the objects they reference. In contrast, consider the following declarations:
Dim rEmp3 As Variant Dim rEmp4 As Object Dim rEmp5 '** implies Variant
Figure 2.18 A client holding a reference to an object
These references all dictate late-binding, a less efficient mechanism based on COM's IDispatch interface (and one that always maps to the object's default interface). Again, this is true regardless of how the objects are created. For example, any use of rEmp5 is late-bound, even if the object is created using New:
Set rEmp5 = New Employees.CConsultant rEmp5.SomeMethod '** this implies a lookup of SomeMethod, then invoke
Although late-binding is possible in compiled environments like VB (using the previous declarations), it is the only binding technique available in scripting environments such as IE, ASP, and WSH.
Note that the object being created must support the type of binding requested by the client; otherwise a run-time error occurs. Objects built with VB automatically support both vtable-binding and late-binding.
COM Activation
COM activation is the process by which a client creates a COM object at run-time. It is a somewhat complex process that involves the client, GUIDs, the COM infrastructure, one or more registries, and the COM server. Although the details are interesting, what's important here are the goals of activation: (1) create the object and (2) obtain the necessary interface references. 17 Keep in mind that objects can be activated across process and machine boundaries, a daunting task that is automatically handled by the COM infrastructure.
The New Operator
The most important characteristic of New is that it does not always trigger COM activation. In some cases, a call to New results in an optimized form of object creation performed entirely by VB. 18 How the New operator behaves depends on whether the class is internal or external, from the perspective of the client creating the object. For example, consider the following client code:
Dim rObj1 As IInterface Set rObj1 = New CClass
The call to New results in VB's optimized creation if the class is internal, (i.e., CClass is either (1) part of the same VB project/DLL/EXE as the client, or (2) part of the same VB group as the client [and you are running that group inside the VB IDE]). Otherwise, the class is considered external, and COM activation is performed in an attempt to instantiate the object.
Being aware of New's optimized behavior is important for two reasons. First, it is much more efficient than COM activation, and thus is preferable for performance reasons. But, second, it is incorrect in certain situations, for example, when the class being instantiated is configured to run under MTS or COM+. In this case, COM activation is required for the class to receive the necessary MTS/COM+ services, but if the class is internal then New bypasses COM activation, creating an object that may not run properly. For this reason, the conservative programmer should avoid the use of New.
Note that the New operator can be applied in two different ways: traditional and shortcut. With the traditional approach, you declare a reference and then create the object separately as needed:
Dim rObj2 As IInterface . . . Set rObj2 = New CClass rObj2.SomeMethod
This allows your references to be of any interface type, and makes object creation visible in the code. The second alternative is the shortcut approach, in which you embed the New operator in the variable declaration:
Dim rObj3 As New CClass . . . rObj3.SomeMethod
In this case, VB automatically creates an object on the first use of the reference variable (rObj2). Although this requires less typing, this approach restricts you to a class's default interface, and can lead to interesting runtime behavior. For example, consider the following code fragment:
Set rObj3 = Nothing rObj3.SomeMethod '** traditionally, this would fail . . . Set rObj3 = Nothing If rObj3 Is Nothing Then '** traditionally, this would be true Msgbox "you'll never see this dialog" End If
Each time you use a shortcut reference in a statement, VB first checks to see if the reference is Nothing. If so, it creates an object before executing the statement. Not only does this result in additional overhead, but it also prevents you from checking whether an object has been destroyed (the act of testing re-creates another object!). For these reasons, we generally recommend that you avoid the shortcut approach.
CreateObject
Unlike New, the CreateObject function always creates objects using COM activation. You supply a string-based ProgID, and CreateObject converts this to a CLSID (via a registry lookup) before performing a standard COM activation. Here's a generic example:
Dim rObj1 As TLibName.IInterface Set rObj1 = CreateObject("TLibName.CClass")
The advantage to this approach is flexibility. First, because CreateObject is based on strings and not class names, the class to be instantiated can be computed at run-time based on user input, configuration files, or records in a database. Second, CreateObject has an optional parameter for specifying where to create the object (i.e., on which remote server machine). This overrides the local registry settings, once again providing more flexibility at run-time. For example, this feature can be used to implement simple schemes for fault tolerance:
On Error Resume Next Dim rObj2 As TLibName.IInterface Set rObj2 = CreateObject("TLibName.CClass", "Server1") If rObj2 Is Nothing Then '** server1 is down, try server2... Set rObj2 = CreateObject("TLibName.CClass", "Server2") End If If rObj2 Is Nothing Then '** both servers are down, give up... On Error Goto 0 '** disable local error handling Err.Raise ... '** inform the client End If On Error Goto Handler '** success, reset error handler and begin... . . .
Note that the machine names are also string based, and thus can be read from configuration files or a database.
Because CreateObject only performs COM activation, it cannot instantiate Private or PublicNotCreatable VB classes. The class must be a registered COM object. Furthermore, whether you are using vtable-binding or late-binding, instantiation via CreateObject requires that the object support late-binding. 19 If the object does not, VB raises error 429. In these cases, the only way to instantiate the object is via New.
GetObject
As the name implies, GetObject is designed to gain access to existing objects; for example, an MS Word document object in a file:
Dim rDoc As Word.Document '**set a reference to MS Word Object Library Set rDoc = GetObject("C:\DOCS\file.doc", "Word.Document") rDoc.Activate
However, it can also be used to create new objects. For example,
Dim rObj1 As TLibName.IInterface Set rObj1 = GetObject("", "TLibName.CClass")
In this sense, GetObject is equivalent to CreateObject, albeit without the ability to specify a remote server name.
Interestingly, as we'll see shortly, there's a version of GetObject that is more efficient than CreateObject, yet it is rarely used for this reason. Instead, it is commonly used to access MS Office objects or Windows services such as Active Directory, Windows Management Instrumentation (WMI), and the Internet Information Server (IIS) metabase. It is also used in conjunction with Windows 2000-based queued components (i.e., objects with method calls that are translated into queued messages for asynchronous processing). For example, suppose the class CQClass is configured as a queued component under COM+ on Windows 2000. The following gains access to the appropriate queue object for queuing of method calls (versus creating a traditional COM object that executes the method calls):
Dim rQObj As TLibName.CQClass Set rQObj = GetObject("Queue:/new:TLibName.CQClass") rQObj.SomeMethod '** call to SomeMethod is queued rQObj.SomeMethod2 "parameter" '** call to SomeMethod2 is queued MsgBox "client is done"
In this case, the calls to SomeMethod and SomeMethod2 are queued for later processing by some instance of CQClass. This implies that a MsgBox dialog appears on the screen long before the actual method calls take place.
GetObjectContext.CreateInstance and Server.CreateObject
Suppose you have two classes configured to run under MTS, CRoot and CHelper. If CRoot needs to create an instance of CHelper, then there is exactly one way for CRoot to instantiate this class properlyvia GetObjectContext.CreateInstance:
'** code for configured class CRoot Dim rObj1 As TLibName.IInterface Set rObj1 = GetObjectContext.CreateInstance( _ "TLibName.CHelper")
Likewise, if CRoot is an ASP page, then the proper way to instantiate CHelper is using Server.CreateObject:
'** code for ASP page Dim rObj2 Set rObj2 = Server.CreateObject("TLibName.CHelper")
These methods are essentially wrappers around CreateObject, accepting a ProgID and performing COM activation. However, they enable the surrounding environment (MTS and ASP respectively) to recognize and to participate in the creation of the COM object. Direct calls to New and CreateObject bypass the surrounding environment, leading to slower or incorrect execution.20 For more details on the rationale and proper use of CreateInstance, see rule 3-3.
Performance Considerations
Most discussions involving the performance of COM objects focus on two things: (1) the type of binding (vtable versus late) and (2) the marshaling characteristics of any parameters. Although these are very important, little attention is paid to the cost of COM activation. Thus, assuming there is no compelling design reason to choose between New, CreateObject, and GetObject, is there a performance reason?
First, keep in mind that New is optimized for internal classes, so it is always the most efficient mechanism when COM activation is not needed. However, let's assume our goal is COM activation. There are three types of activation: in-process, local, and remote. In-process activation means the resulting object resides in the same process as the client. Both local and remote activation represent out-of-process activation, in which the object resides in a process separate from the clienteither on the same machine (local) or a different one (remote). Examples of in-process activation include classes packaged as an ActiveX DLL and then registered as COM objects, and classes configured to run under MTS as a library package. Examples of local and remote activation include classes packaged in an ActiveX EXE, and classes configured to run under MTS as a server package.
In the case of in-process activation, New is always the most efficient: It is 10 times faster than CreateObject and is 10 to 20 times faster than GetObject. This is mainly the result of the fact that CreateObject and GetObject require additional steps (e.g., the conversion of the ProgID to a CLSID). Interestingly, in the out-of-process cases, the best performer varies: New is more efficient (10 percent) when you plan to use vtable-binding against the object's default interface, whereas CreateObject and GetObject are more efficient when you plan to use late-binding against the default interface (two times) or vtable-binding against a custom interface (10 to 15 percent). Let's discuss why this is so.
As noted earlier, COM activation has two goals: (1) create the object and (2) acquire the necessary interface references. The New operator is essentially optimized for vtable-binding against an object's default interface. In one API call, New creates the object and acquires four interface references: the default interface, IUnknown, IPersistStreamInit, and IPersistPropertyBag. On the other hand, CreateObject and GetObject are optimized for late-binding, because they acquire a slightly different set of interface references: IDispatch, IUnknown, IPersistStreamInit, and IPersistPropertyBag. Note that CreateObject and GetObject also take longer to create the object and to acquire these references (two API calls and three method calls).
So why is New slower in some cases? Recall that out-of-process activation yields proxy and stub objects to handle the communication between client and object (Figure 2.19). A proxy/stub pair is created during activation for each interface that is acquired, and thus forms part of the activation cost. Assuming the object does not perform custom marshaling, 21 the proxy-stub pair associated with its default interface is much more expensive to create than those associated with predefined COM interfaces such as IDispatch. As a result, if the client ends up using the default interface, then New is faster because it automatically acquires a reference to the object's default interface. However, if the client needs IDispatch (late-binding) or a custom interface, then CreateObject and GetObject are faster because time is not wasted building an expensive proxy/stub pair that will never be used. The results are summarized in Table 2.2.
Figure 2.19 COM out-of-process activation yields proxy and stub objects
Table 2.2 Maximizing performance of COM objects
Activation Type |
Interface Client Will Use |
No. of Calls Client Will Make |
Best Performance |
|
Activation |
Binding |
|||
In-process |
|
|
New |
vtable |
Local |
default |
< 10 |
CreateObject |
late |
|
3 10 |
New |
vtable |
|
custom |
|
CreateObject |
vtable |
|
Remote |
default |
< 3 |
CreateObject |
late |
|
3 3 |
New |
vtable |
|
custom |
|
CreateObject |
vtable |
What's fascinating is that activation is not the complete picture. The conventional wisdom for best overall performance is to access the object using vtable-binding because it requires fewer actual calls to the object and passes parameters more efficiently. However, vtable-binding implies the direct use of an object's interface (default or custom), and hence the need for an expensive proxy/stub pair in the out-of-process case. For example, assume the following client-side code is activating an out-of-process COM object:
Dim rObj1 As TLibName.CClass '** implies default interface Set rObj1 = CreateObject("TLibName.CClass")
Even though CreateObject avoids the expensive proxy/stub pair, the Set statement will trigger their creation because the type of the variable being assigned is one of the object's interfaces. Therefore, to get the full benefit of using CreateObject, it turns out that you must also use late-binding! In other words,
Dim rObj2 As Object '** implies IDispatch to default interface Set rObj2 = CreateObject("TLibName.CClass")
is roughly twice as fast as the previous code fragment. Of course, late-binding is more expensive per call, and thus the advantage of this approach diminishes as the number of calls increases. This explains the results in Table 2.2, in which there exists some threshold at which point vtable-binding becomes more efficient. Note that the exact threshold will vary in different situations (based on network speeds and distances, interface designs, and so on).
Lastly, if you are running Windows 2000 and are truly concerned with performance, you might consider using GetObject in place of CreateObject. GetObject is slightly more efficient when used as follows:
Dim rObj As ... Set rObj = GetObject("new:TLibName.CClass")
In this case, GetObject acquires only two interface references instead of four; namely, IDispatch and IUnknown. Although this speeds up activation by reducing the number of method calls (which may be traversing across the network), it prevents the proper activation of "persistable" objects because IPersistStreamInit and IPersistPropertyBag are no longer available.
Fortunately or unfortunately, VB offers a number of different techniques for creating objects. Some always perform COM activation (CreateObject and GetObject); some do not (New). Some are more flexible (CreateObject and GetObject), whereas others must be used in certain cases for correct execution (GetObjectContext.CreateInstance, Server.CreateObject, and New). And some are more efficient than others, although one must take into account the type of activation, the interface being used, and the number of calls the client plans to make.
If you do not need COM activation, use New and vtable-binding. Otherwise, consult Table 2.2 to maximize performance. Although it may be counterintuitive, if your design involves "one-shot" objects (i.e., create, call, and destroy), then CreateObject with late-binding may be the most efficient approach. However, keep in mind that you lose IntelliSense and type checking with late-binding. For this reason, the conservative programmer should consider sticking with vtable-binding.