O'Reilly logo

COM+ Programming with Visual Basic by Jose Mojica

Stay ahead with the world's most comprehensive technology and business learning platform.

With Safari, you learn the way you learn best. Get unlimited access to videos, live online training, learning paths, books, tutorials, and more.

Start Free Trial

No credit card required

Client-Server Communication: A Low-Level View

Let’s begin by examining what is required for a client to be able to create a COM object that lives in a DLL. (In Chapter 5, you will see that the process is slightly different for ActiveX EXEs.) But first you must learn the true name of a COM component.

GUIDs

As we noted in Section 3.5 in Chapter 3, the official name for classes and interfaces is not the string name you assign to the class or interface while developing in VB. You may recall that QI enables developers to ask an object for a particular interface implementation. It also enables developers to find out if an object in fact supports an interface. The first parameter in QI is the interface “name.” It is an input parameter. The second parameter is an out parameter returned by the object with a pointer to the vptr of the requested interface.

The name IAccount is not unique enough for QI to work for every interface ever defined. Think about how many companies writing COM-based banking applications may use the name IAccount for their primary interface definition. QueryInterface would not know if you were asking for company A’s IAccount or company B’s IAccount. Therefore, Microsoft decided to use another mechanism for naming interfaces. Instead of using the string name, COM identifies each interface by a number—a very big number known as a globally unique identifier, or GUID.

A GUID is a 128-bit number. An example of a GUID is 36B1C83C-62D0-4199-AF2E-A7A73505EA65—the registry is full of them. In fact, most of COM’s registry keys can be found in four locations: HKEY_CLASSES_ROOT, HKEY_CLASSES_ROOT\CLSID, HKEY_CLASSES_ROOT\TypeLib, and HKEY_CLASSES_ROOT\Interface. If you look at the registry with Regedit.exe, you will notice that the registry hierarchy is divided into four main trees. Microsoft has suggested that the HKEY_CLASSES_ROOT tree in the registry (normally abbreviated as HKCR) should be used for storing information about COM components. Microsoft provides an API function called CoCreateGUID in OLE32.DLL that uses a special algorithm to generate unique GUIDs. GUIDs were not conceived by Microsoft developers. GUIDs, normally called UUIDs (universally unique identifiers), were first introduced by the Open Software Foundation (OSF) as part of their Distributed Computing Environment specification. They are also the creators of the algorithm to generate unique numbers and the ones who specified the format in which they are written (xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx).

The algorithm for generating GUIDs uses, among other things, the unique number of your network adapter (if your computer has a network adapter). Each network card has a unique identifier assigned by the manufacturer. The algorithm for generating GUIDs has been declared statistically guaranteed to generate unique numbers until the year 3000 (by then our brains will be large enough that we will not need computers anymore). The bottom line is that if you use the function CoCreateGUID, you do not ever have to worry about getting the same number twice.

Tip

Believe it or not, there is a small controversy among COM developers about how to pronounce the acronym GUID. Some people like to make it rhyme with the word druid, as in goo-id, while others like to make it rhyme with the word squid. I must warn you that a number of famous COM authors (including my coworkers) like to make it rhyme with squid, and this now seems to be the standard. I, however, like to say it goo-id, because this is the way I first learned it five years ago, and old habits are hard to break.

The reason you may not have ever seen a GUID in VB is that VB generates GUIDs using CoCreateGUID behind the scenes and hides all evidence of them. This is a blessing and a curse, as you will see at the end of this chapter when we discuss versioning.

In COM, GUIDs are used to name other elements aside from interfaces, such as classes and type libraries. When a GUID is used to name an interface, it is known as an IID (interface ID). Likewise, when it is used to name a class or library, it is referred to as a CLSID (class ID) or a LIBID (library ID), respectively.

In reality, COM does not refer to the IAccount interface by its string name IAccount, but by its IID (written as IID_IAccount in C++). In the same fashion, COM knows nothing of the name CChecking—it knows the class by its numeric name, the CLSID (written as CLSID_CChecking in C++).

VB generates an IID for each interface you define, a CLSID for each COM class you define, and a single LIBID for the type library of the project at compile time. Because GUIDs are guaranteed to be unique, if you were to compile the source code for BankServer in your machine, and I were to compile it in my machine, we would definitely end up with different numbers for IIDs, CLSIDs, and LIBIDs. Later in this chapter you will see how these numbers are stored and how the client becomes aware of them.

ProgIDs

GUIDs are not very readable. You cannot read a GUID and easily identify the component to which it belongs; for example, you have never read in a manual to “create an instance of the 36B1C83C-62D0-4199-AF2E-A7A73505EA65 class.” Using GUIDs can make programming cumbersome for scripting languages or even for VB developers. Therefore, even though a class name such as CChecking is not guaranteed to be unique, COM does enable a developer to refer to a class by a string name called a Programmatic ID (ProgID). A ProgID is a string name that identifies an object. It is not guaranteed to be unique—two companies may come up with the same ProgID.

The ProgID is supposed to be constructed from the company name plus the name of the server plus the class name (for example, WidgetsUSA.BankServer.CChecking). Although this name is not guaranteed to be unique, it can be close. ProgIDs are stored in the registry under HKCR; COM rules say that ProgIDs are optional, but if they are assigned, each COM class in your server must have its own ProgID. Under the ProgID key, you store the GUID for the class (the CLSID). COM provides two API functions called CLSIDFromProgID and CLSIDFromProgIDEx, which search the registry for a specific ProgID and return the CLSID. Thus, a COM developer who does not know the component’s GUID may use this API to obtain it from a readable name and then use the GUID in other COM calls.

Each machine maintains only one copy of a ProgID. That means that if two companies use the same ProgID, they will override each other’s setting in the registry; the ProgID will point to the CLSID of the last COM server to be registered.

Microsoft decided this was too much work for us VB developers. Every time you compile a COM project (ActiveX DLL, ActiveX EXE, or ActiveX OCX), VB automatically generates a ProgID for each of your classes and adds it to the registry. VB constructs the ProgID from the project name plus the class name. So for CChecking, the ProgID is BankServer.CChecking. If we had a class called CSavings, we would have two ProgIDs (BankServer.CChecking and BankServer.CSavings). Notice that this can be a problem, since it is very likely that two companies can name their projects BankServer and both include a CChecking class. Nonetheless, there is nothing that can be done about this. VB does not let us choose our own ProgIDs. If you are concerned about your classes, you can always name your project CompanyName BankServer and at least have a fair shot at uniqueness. If you are really concerned, you can always take matters into your own hands and come up with ProgIDs that you feel are unique, then manually add them to the registry through your installation program.

CoCreateInstance and CoCreateInstanceEx

Whether you are working with VB, Java, or Visual C++, each language eventually uses one of two API functions to create a COM object: CoCreateInstance or CoCreateInstanceEx. CoCreateInstance is an earlier version of the API—internally CoCreateInstance simply calls CoCreateInstanceEx, passing some default parameters. However, each works slightly differently. Both of these APIs are in OLE32.DLL.

CoCreateInstance has five parameters. The first parameter is the CLSID of the class you wish to create (the real name of the class). The second parameter is for a feature called aggregation, which is outside the scope of this book. (Aggregation enables you to make a client think that another already-compiled COM object is actually part of your object. In other words, it enables two COM objects to act as a single object. VB objects cannot be aggregated.) The third parameter is the creation flag. The creation flag enables you to specify whether the API should search for the object inside a DLL, inside an EXE in the same machine, inside an EXE in another machine, or in any possible way starting with a DLL. The fourth parameter is the IID of the interface you wish to start with. When you create the object, you can tell the COM API to do a QI for you so that you can execute both creation and QI at the same time. The fifth parameter is an output parameter that receives the memory address of the vptr of the vtable you asked for in the fourth parameter.

CoCreateInstanceEx is slightly more advanced. For one thing, it enables you to specify a remote server where the object lives. Another advantage of CoCreateInstanceEx is that instead of asking for a single interface, you can ask for an array of interfaces at once. This is particularly useful when creating an object remotely, since it minimizes the number of times you have to access the remote server to do a QI. Whenever you need to create a COM object from a remote machine, and you wish to specify the remote machine’s name, you must use the CoCreateInstanceEx function.

However, you never see these API functions in VB because VB makes the call for you. Yet, it is good to know which one VB uses in each circumstance, because it turns out that this information is crucial when versioning your components (see Chapter 6).

Type Libraries (Revisited)

VB translates the New operator into a call to CoCreateInstanceEx. CoCreateInstanceEx takes as its first parameter the CLSID of the class you wish to create. This means that VB must know the CLSID of the class at compile time. Where does it get the CLSID when using the New command? Consider the following code:

Dim Acct As IAccount
Set Acct = New CChecking
Call Acct.MakeDeposit(5000)

You learned from Chapter 3 that the line Set Acct = New CChecking does two things: first, it tells the server to allocate memory for the CChecking class; second, it uses QI to get a pointer to the IAccount interface.

QI requires that you specify the interface you wish to obtain using the interface’s IID. This means that VB must know both the CLSID of CChecking and the IID of IAccount to execute the preceding code. Not only that, but if you read the last chapter carefully, you should know that the call Acct.MakeDeposit is translated to a vtable call. This means VB has to know that MakeDeposit is function number 8 in the vtable for IAccount.

Tip

Why number 8? First, there are the three methods for IUnknown in the vtable, then the four methods for IDispatch, and then MakeDeposit.

In other words, at compile time VB needs to translate the line Acct.MakeDeposit(5000) to use the vtable, find the entry to the MakeDeposit function (i.e., read the address of function number 8 from the array of function pointers), then make the call. For this to work at compile time, VB must know the layout of the interface. Thus, there are at least three pieces of information that it needs to create that object: one CLSID, one IID, and the layout of the IAccount interface. Where does all this information come from?

All the information the client needs to create a COM object comes from the type library. The type library is a binary file that VB generates at compile time. The format for the type library file is undocumented; however, Microsoft provides tools for generating type libraries as well as an array of API functions for parsing a type library file. Type libraries are generated by VB when the server is compiled, and they provide clients with the information necessary for the client to use early binding (see Chapter 3 for details). A C++ developer defines the interfaces and COM classes first using the Interface Definition Language (IDL). Microsoft provides an IDL compiler called MIDL. This compiler can generate (along with other files) a type library file with the extension .TLB. The idea is that you should be able to create a COM server in Visual C++ and generate a .TLB file, then use the .TLB file in a VB client in order to give VB the information it needs to create instances of the classes in the C++ server. This is a two-way street. VC can also use the type library from a VB server to get all the information it needs to create classes in that server.

The COM specification says that a server may distribute a type library in one of two ways: as a standalone file or as part of the server file (i.e., embedded into the DLL or the EXE as a resource). VB chooses to embed the type library file into the DLL or EXE at compile time, although you can request to also have the type library as a standalone file. To have VB generate a standalone file as well as embed the file into the compiled image, choose Project BankServer Properties. In the Project Properties dialog box, click on the Component tab, and choose the Remote Server Files option.

It is possible to see a server’s raw type library information using a tool Microsoft provides with Visual Studio called OLE/COM Object Viewer. To use the OLE/COM Object Viewer, choose Start Programs Microsoft Visual Studio 6.0 Microsoft Visual Studio 6.0 Tools OLE View (see Figure 4-2).

OLE/COM Object Viewer

Figure 4-2. OLE/COM Object Viewer

If you compiled the BankServer code from the beginning of the chapter, you may examine the type library information embedded in the BankServer.DLL file. To do so, expand the Type Libraries branch and find the entry that reads BankServer. Right-click on BankServer and choose View.... Example 4-1 shows portions of the raw type library information in BankServer.DLL.

Example 4-1. Portions of the raw type library information in BankServer.DLL

// Generated .IDL file (by the OLE/COM Object Viewer)
// 
// typelib filename: BankServer.dll

[
  uuid(46D3EE36-C807-4B58-90EE-B2E81FB80317),
  version(1.0),
  custom(50867B00-BB69-11D0-A8FF-00A0C9110059, 8495)

]
library BankServer
{
    [
      uuid(14172653-F73E-4BB0-8392-2E1D61528542),
      dual,
      hidden,
    ]
    interface _CChecking : IDispatch {
    };

    [
      uuid(4B9095B4-809C-4669-B54B-DD9610E054A4),
    ]
    coclass CChecking {
        [default] interface _CChecking;
        interface _IAccount;
    };

    [
      uuid(36B1C83C-62D0-4199-AF2E-A7A73505EA65),
      dual,
      hidden,
     ]
    interface _IAccount : IDispatch {
        [id(0x68030000), propget]
        HRESULT Balance([out, retval] CURRENCY* );
        [id(0x60030001)]
        HRESULT MakeDeposit([in] CURRENCY Amount);
    };

    [
      uuid(E6ECF4C7-69D9-43FB-8A80-BA24AF5A4E14),
      noncreatable
    ]
    coclass IAccount {
        [default] interface _IAccount;
    };

};

I have condensed the code in Example 4-1 for the sake of clarity. Notice that the type library has three important pieces of information: the library block, the interface definitions, and the class definitions. What may seem strange from the code is that there are two interface definitions and two class definitions. Example 4-1 shows an interface called _CChecking and a class called CChecking. The following fragment from Example 4-1 includes the interface definition:

    [
      uuid(14172653-F73E-4BB0-8392-2E1D61528542),
      dual,
      hidden,
    ]
    interface _CChecking : IDispatch {
    };

Before the interface definition are attributes for the interface enclosed in brackets. One of these attributes is the uuid (or GUID) or, more specifically, the interface’s IID. The interface is also marked as hidden. This attribute works only for VB clients—it means that a VB client is not able to see this interface in Intellisense (the drop-downs you see when you are coding) unless you select View Object Browser, right-click anywhere in the window, and select Show Hidden Members from the drop-down menu.

If you read Chapter 2, you learned that _CChecking is the default interface for the CChecking class. The default interface contains the public members (properties and functions) of a class. The CChecking class does not have any public members of its own; it gets its functionality from implementing the IAccount interface. This is why there are no functions declared in the interface definition for _CChecking. Remember that what VB does when it allocates an instance of the CChecking class is to create two little objects in memory, one to handle calls to the default interface and one to handle calls to the implemented interface.

If you are unfamiliar with C++ syntax, you may not realize that the statement interface _CChecking : IDispatch means that _CChecking is derived from IDispatch. In fact, every interface in Example 4-1 is derived from IDispatch. In Chapter 3, you learned that all VB-generated interfaces are dual interfaces to support scripting clients.

Following the definition for the interface in Example 4-1 is the definition for the CChecking coclass (COM class):

    [
      uuid(4B9095B4-809C-4669-B54B-DD9610E054A4),
    ]
    coclass CChecking {
        [default] interface _CChecking;
 interface _IAccount;
    };

Again, notice that the class has a uuid attribute, which is the CLSID for the class. Interesting, however, is that two interfaces are listed for the class. The coclass definition is an advertisement to the client of what the class supports. The interfaces listed in the coclass definition have no effect on the compiler, only on Intellisense at design time; the important thing for the compiler is the CLSID for CChecking. Note that the list of interfaces contains interface _CChecking marked with a [default] attribute. The default attribute is used only by VB clients; a C++ programmer couldn’t care less if an interface is marked as the default. To obtain any interface from an object, C++ or VB has to use QI. So a C++ client simply creates an instance of the class and asks for _CChecking or for _IAccount. VB uses the default attribute so that at design time a programmer may write code like the following:

Dim Account As CChecking
Set Account = New CChecking

In the preceding code, Account is defined as being of type CChecking. The compiler knows that what the developer means is:

Dim Account As _CChecking
Set Account = New CChecking

In other words, VB will do a QI in the assignment for the default interface, which is _CChecking. In this way, VB hides the fact that interfaces even exist when using VB classes.

Example 4-1 also has a definition for the IAccount interface—except that IAccount is not the interface name. As in the example of _CChecking, VB creates a default interface for the IAccount class. The default interface is _IAccount:

    [
      uuid(36B1C83C-62D0-4199-AF2E-A7A73505EA65),
      dual,
      hidden,
     ]
    interface _IAccount : IDispatch {
        [id(0x68030000), propget]
        HRESULT Balance([out, retval] CURRENCY* );
        [id(0x60030001)]
        HRESULT MakeDeposit([in] CURRENCY Amount);
    };

An interesting fact is that VB also defines a class with the name IAccount. In fact, for every class whose Instancing property is not marked as Private, VB adds two things to the type library: a default interface and a class definition. VB does not know that the intent of the IAccount class was to serve as an interface definition. It treats it as any other class at compile time. Take a look at this IAccount coclass:

    [
      uuid(E6ECF4C7-69D9-43FB-8A80-BA24AF5A4E14),
      noncreatable
    ]
    coclass IAccount {
        [default] interface _IAccount;
    };

The only attribute that is different is noncreatable. This attribute is enforced by the VB IDE when a developer is coding a client at design time, and it is also enforced at runtime if a program attempts to create an instance of the component. A component marked as noncreatable lacks the code necessary for the client to create an instance of the class. Because we marked the class’ Instancing property as 2 - PublicNotCreatable, VB added this attribute to the type library.

Not obvious from looking at the type library source is that there is no mention of the DLL file or path to the DLL file that contains this type library (except for the comment at the top that has no effect on the client). How does the client code know what DLL to use to create the objects at runtime? Or does it need to know the DLL name and path at runtime? The answer is, “No.” The VB client needs to know only three things: the CLSID of the class you wish to create, the IID of the interface within the class you wish to use, and the interface layout (function position within vtables). Therefore, think of the type library as separate from the DLL—the fact that it is compiled into the DLL is only for convenience.

VB COM Creation Functions

You have already seen from the previous chapter that you are able to create COM objects in VB using the New operator. The VB compiler replaces the call to New with a call into the VB runtime MSVBVM60.DLL. The VB runtime executes a function called vbaNew. This function calls CoCreateInstanceEx. If you recall, CoCreateInstanceEx requires the compiler to know at compile time the CLSID of the class you wish to create. VB gets this information from the type library. This, in turn, requires that you tell VB what type libraries to use. You do this through the References dialog box in VB. The References dialog box lists all the type libraries available in the system. It builds a list of these type libraries from reading the registry HKEY_CLASSES_ROOT\TypeLib subkeys.

The advantage of VB’s using CoCreateInstanceEx over CoCreateInstance is that CoCreateInstanceEx enables you to ask for an array of interfaces at once instead of a single interface, and VB certainly makes use of this. VB is particularly interested in four interfaces upon creation:

IUnknown

By obtaining a pointer to IUnknown, VB can do reference counting more easily.

The object’s default interface

VB guesses when asking for the second interface. The VB team made an assumption that is accurate for many developers. However, it is not accurate for us COM-savvy VB developers. The VB team assumed that you would most likely be using the object’s default interface. Therefore, the second interface asked for is the default interface. VB knows which is the default interface at compile time from reading the default attribute in the type library. If there is no default attribute, VB automatically asks for the first interface listed in the coclass definition in the type library.

IPersistStreamInit

This third interface is a Microsoft-defined interface whose purpose is to talk to objects that are persistable. VB 6 added a property to the class module called Persistable (True or False). When you mark a class as Persistable, VB adds support for the IPersistStreamInit interface. This is as if you had written Implements IPersistStreamInit in your class. Adding support for IPersistStreamInit doesn’t mean that a client program will definitely make use of the interface. However, VB, being the kind of environment it is, asks to see if the object has this interface and starts using it.

IPersistPropertyBag

This fourth interface is an alternative to IPersistStreamInit. Both have similar methods—the idea is that if the class does not support IPersistStreamInit, it will try to use IPersistPropertyBag.

The important thing to know is that VB client programs are written to ask for these four interfaces internally: IUnknown, I_DefaultInterface, IPersistStreamInit, and IPersistPropertyBag.

VB executes the statement Set Account = New CChecking in two stages. The first stage is creation (the right side of the equals sign), and the second stage is assignment (the left side of the equals sign). In creation, VB calls CoCreateInstanceEx to create the object, and while in creation it asks the object for those four interfaces. If VB cannot find IUnknown, then it is not a COM object, and VB generates an error. If VB cannot find the default interface, it also generates an error. (This is unfortunate, since the default interface may not be the interface the client needs, and this is what gives us 90% of the problems with versioning). The IPersistStreamInit and IPersistPropertyBag interfaces are optional, and VB does not generate an error if it cannot find them.

In the assignment stage, VB calls QueryInterface and asks for the interface specified by the type of the variable. Thus, since Account is of type IAccount, VB will ask for what interface? Notice that IAccount was the name we gave to the class, and in the type library source you can see that the coclass is named IAccount. VB does a QI for the default interface of IAccount, which is _IAccount. In VB there is a one-to-one mapping of the default interface to the class name.

It is also possible to create a class from its ProgID. To do that, you must use the CreateObject function. This function has two parameters, although the second is optional and will be discussed in Chapter 5. The first parameter is required—it is the ProgID of the object you wish to create. VB will automatically call CLSIDFromProgIDEx for you to obtain the class’ GUID and use that whenever it needs to make a COM call for you.

The following code shows how to use the ProgID to create an instance of the CChecking class from the client:

Private Sub cmdCreate_Click(  )
    Dim Acct As IAccount
    Set Acct = CreateObject("BankServer.CChecking")
    Call Acct.MakeDeposit(5000)
    MsgBox Acct.Balance
End Sub

The code is the same as before, except that the CreateObject function is used instead of the New keyword. What VB does internally with CreateObject, however, is very different from what it does with New—it turns out that New is faster than CreateObject in many instances. In informal tests, I found that New is much faster than CreateObject when using components from an ActiveX DLL but that the difference is not as dramatic when the components are in an ActiveX EXE in the same machine. If the ActiveX EXE is on another machine and you specify a remote machine in the second parameter of the CreateObject function, then CreateObject and New do almost the same thing, so the difference in speed is very small. Before you switch all your code to use New, however, you should know that New has limitations when used with COM+-configured components (see Chapter 7 for details).

CreateObject was introduced in VB 3, which may explain why it works the way it does. CreateObject does not call CoCreateInstanceEx ; instead, it calls CoCreateInstance. Remember that with CoCreateInstance, VB can ask for only one interface upon creation. VB takes the safe route and asks for IUnknown. The problem is that because VB still needs IPersistStreamInit, it immediately calls QueryInterface for this interface. If it fails to find it, it asks for IPersistPropertyBag. Then comes the interesting part. Because CreateObject was written for scripting clients, VB assumes that you wish to get hold of the object’s IDispatch interface (scripting clients talk only to IDispatch). Therefore, VB immediately QIs for IDispatch. The CreateObject function fails only if it cannot translate the object’s ProgID into a CLSID or if the object does not support the IUnknown interface. The IDispatch interface is optional. Remember that New asks for the default interface, whereas CreateObject asks for IDispatch; this is an important distinction, particularly when versioning your components.

Using CreateObject requires four round-trips before it can execute the last QI for the interface you are looking for. Compare this to the New operator, which takes only one round-trip to complete.

In VB 3, the only way to create a COM object was through the CreateObject function. The CreateObject function was intended to be used with the Object datatype. In VB you can declare a variable to be of type Object as follows:

Private Sub cmdCreate_Click(  )
    Dim Acct As Object
    Set Acct = CreateObject("BankServer.CChecking")
End Sub

The preceding code segment uses the Object datatype instead of the IAccount interface. The Object datatype stands for the IDispatch interface. Whenever you set a variable of type Object equal to a variable pointing to a COM object, VB does one of two things. If the variable points to a dual interface, an interface derived from IDispatch, VB simply increases the reference count for the COM object. If the variable points to a custom interface (derived from IUnknown), VB calls QueryInterface and asks for IDispatch. The Object type is discouraged, because each method call goes through GetIdsOfNames and Invoke (as you learned in the previous chapter) instead of using direct table binding.

There is one advantage in using the Object datatype in conjunction with CreateObject. The previous code sample does not require you to have a type library. Type libraries are required for early binding. The previous code uses late binding (IDispatch). What’s more, the code uses a ProgID to create the object. Thus, the executable code finds out the CLSID of the object at runtime; New requires that you know the CLSID at design time (hence the need for the type library).

To summarize our discussion so far, at the server’s compile time, VB assigns GUIDs to each interface and to each COM class. VB puts the information for each interface and class into a type library file and embeds the type library into the DLL. VB client programs read the type library file to read the object’s definition. Upon compilation of the client, VB translates a New call into CoCreateInstanceEx, and it translates CreateObject into CLSIDFromProgIDEx CoCreateInstance 3 QIs.

There are, however, a few unresolved issues. One is that, since CoCreateInstanceEx and CoCreateInstance require only the CLSID of a class to create it, how do they know which EXE or DLL contains that class? Second, once the server is located, how is it told to create the class? Read on!

Registry Keys

To find information about the DLL that contains the requested class, CoCreateInstanceEx in OLE32.DLL searches the registry for a key containing the CLSID requested by the client. (The CLSID information comes from the type library file that you have to import into the client project using Project References.) The piece of code in OLE32.DLL that is involved in searching the registry and finding the associated DLL is called the Service Control Manager (SCM). Real COM+ developers refer to the SCM as the scum. The code for the SCM is scattered throughout OLE32.DLL and RPCSS.DLL (a service loaded by svchost.exe). This makes the SCM a logical entity rather than a physical entity that you can pinpoint to a single piece of code. For that reason, activation of a COM+ component is always blamed on the SCM.

Under COM+, it is possible to make a class available at two different levels: machinewide or user specific. If the class is to be available for all users, information for the class is added to the HKEY_CLASSES_ROOT\CLSID branch. If a class is meant to be used only by a certain user, information is added to the HKEY_CURRENT_USER\Software\Classes\CLSID branch. Each COM class needs to be added to the registry. It is done in the following fashion. Suppose that the CLSID for CChecking is {7C0B41FF-4935-499D-A1BD-465E98655C89}. The registry may contain HKCR\CLSID\{7C0B41FF-4935-499D-A1BD-465E98655C89} or HKCU\Software\Classes\CLSID\{7C0B41FF-4935-499D-A1BD-465E98655C89} or both. It is possible to add information to both branches and have slightly different information for a certain user and general information for all users. This cannot be done with VB, however; VB writes information only to HKCR. In fact, most COM developers ignore HKCU; it is an enhancement that never caught on. If you want to use HKCU, you have to write the registry key manually with code or use your setup program of choice. COM+ always searches the HKCU branch first, then the HKCR key (even if it finds information under HKCU).

If the SCM finds the CLSID in one of the two branches, it searches for other subkeys. The first subkey that it searches for is TreatAs. The TreatAs key enables a developer to redirect the key information to another key. In other words, it is possible to tell OLE32.DLL that whenever it encounters key A, it should really look in key B for information. After searching for the TreatAs subkey, OLE32.DLL searches for the InProcServer32 key. The InProcServer32 key provides the location of the COM server DLL in 32-bit Windows. In 64-bit Windows, OLE32.DLL searches for the InProcServer64 key. Assuming you are using 32-bit Windows, the CChecking may have the following information in the registry:

HKCU
    CLSID
        {7C0B41FF-4935-499D-A1BD-465E98655C89}
            InProcServer32   (Default) C:\Bank\BankServer.DLL

What if it cannot find the InProcServer32 key? It then searches for a LocalServer32 key. You are going to learn more about this key in the next chapter when we discuss EXE servers. However, for now, realize that the SCM prefers for classes to be in DLLs, so it searches for the InProcServer32 key first. This key is meant to store only a path to a DLL. If you build a server as an EXE, the path to that file must be stored in the LocalServer32 key. The term local server is used in the COM spec to denote an ActiveX EXE server that lives on the same machine as the client program.

When the SCM locates the registry information for the CLSID, it adds the information to an internal cache. This means that if you request the information a second time, the SCM does not search the registry again; it simply gets the information from its internal cache.

If there is an InprocServer32 key, the SCM reads the path to the DLL file. It then calls the WIN32 API function LoadLibrary. This function causes the kernel to load the DLL file into the same memory space allocated for your executable. In Windows, each process is given a virtual memory space. In Windows 2000, an application has an address space of 4 GB. Into this memory space Windows maps the system DLLs as well as any COM Server DLLs that OLE32.DLL loads (as well as OLE32.DLL itself). The end result is that it appears as though your application is running by itself in the machine and it can reach any of the functions in its memory. This is required by the Windows architecture: for an executable to make a direct function call into a DLL, the functions need to be in the same address space as the process.

Once the SCM loads the DLL with the LoadLibrary function, it calls the Win32 API function GetProcAddress. This function enables the SCM to find the location in memory of a certain function in the DLL. The SCM uses this function to find the function DllGetClassObject. DllGetClassObject is one of the required functions for every COM Server DLL. The COM+ specification says that every COM+ DLL must have four functions that can be called externally. (This does not mean that your component must have four functions; rather, the DLL itself must.)

The reason you may never have known that every COM DLL has four exported functions is that VB automatically adds them to the DLL image at compile time.

DLL Exports

To the operating system, an ActiveX DLL is like any other DLL. DLLs export functions. DLLs do not have the necessary code to be standalone programs. They must be run from within a process.

When you run an executable in Windows 2000, the OS allocates 4 GB of virtual memory (in 32-bit Windows) for your executable; each executable has its own address space. The details of how virtual memory translates into physical memory are beyond the scope of this book. Nonetheless, these 4 GB of virtual memory are known as your application’s address space. Each executable has a region of this memory available for storing variables and allocating objects. The variables that are allocated in each executable are visible only to the executable (or to DLLs loaded by the executable). In other words, two executables cannot see each other’s memory contents. This means that an executable that makes a mistake with its memory and crashes will not affect the memory contents of another executable.

Also within this memory is the code for the executable. When an executable loads a DLL, the DLL code is mapped into this portion of the executable’s address space. Also, any variables that are global to the DLL will be allocated into the address space of the executable. In fact, when an executable loads a DLL, the DLL in a sense becomes part of the executable. The DLL code is able to reach the memory of the executable, and the executable is able to make calls into functions of the DLL as if they were functions in the executable. In theory, a faulty application should not bring down the OS. A faulty DLL, on the other hand, can cause a process to crash, because once it is loaded, it is part of the executable’s address space. It is the same as the executable itself crashing.

The DLL functions that are available to the client are known as exported functions. VB does not enable developers to have functions that are exported directly. Instead, functions are exported through COM objects. Yet, for the ActiveX DLL to function, it must export functions that the executable can call. The COM specification says that for a DLL to be considered an ActiveX DLL (or simply a COM DLL), it must export four functions: DllRegisterServer, DllUnregisterServer, DllGetClassObject, and DllCanUnloadNow.

For the SCM to find the COM server DLL, it must find the CLSID key in the registry. Who puts this registry information there? The registry information comes from the COM server DLL itself through its DllRegisterServer function. In Visual C++, for example, developers have to write code to add their COM classes as well as their type library information to the registry. (This is very tedious code to write. Luckily, C++ offers premade code through ATL or MFC to make this easier.) Because a DLL cannot run on its own, another program must run it to enable it to add itself to the registry. Microsoft provides the program: RegSvr32.EXE (an eight-character filename—the habit is hard to break). When you run this program, you must specify the location of the DLL you wish to register. RegSvr32 takes this path and loads the DLL into its address space with LoadLibrary. Then it finds the DllRegisterServer function with the aid of the GetProcAddress WIN32 API and executes it. It is the responsibility of the developer of the COM server to write code in this function to add a CLSID key in the registry and an InprocServer32 key. After adding itself to the registry, the function ends and RegSvr32 exits.

VB automatically calls the DllRegisterServer function immediately after it compiles the COM DLL. This means that as soon as you compile your COM DLL, you can start using it. InstallShield and Wise Installation System have an option to autoregister DLLs. These programs also simply find the DllRegisterServer function.

The DllUnregisterServer function’s job is to remove the registry keys for the server. Again, this is done by the DLL itself. The developer authoring the server is responsible for writing code to remove the registry keys when a program calls this function. You can use RegSvr32.EXE to unregister the DLL as well. To do so, simply run RegSvr32.EXE with the /U switch followed by the path to the DLL. In this case, RegSvr32.EXE simply calls the DllUnregisterServer function. Notice that this helps programs like InstallShield and Wise to clean up registry keys, since they do not have to remember what the DLL added to the registry—they simply call this function, and the DLL itself takes care of removing the keys.

The function DllCanUnloadNow tells a process if it is OK to unload the DLL. If you were writing a COM server in C++ from scratch, you would have to keep track of outstanding references to any of your objects. In other words, every time someone calls AddRef in any of your objects (to notify you that they are using your object), you must increase a global counter for all the objects in the DLL. In the same fashion, you must decrease the counter every time someone calls Release in any of the objects. This is a different counter from the reference counter through Iunknown—that is a per-class counter that lets you know when to remove an instance of the class from memory. This counter knows when to unload the entire server from memory. The developer of the COM DLL simply checks the counter and returns 0 (the C++ constant is S_OK) if the counter reaches 0. If the counter is greater than 0, then the developer returns 1 (the C++ S_FALSE constant).

To create an instance of the class the client requested, the SCM calls the DllGetClassObject function.

Class Factory

When a client requests an instance of a class through CoCreateInstanceEx, the function first retrieves a COM class object. The COM specification says that creation happens in two steps. When you define a class module in VB, say CChecking, it serves as a template for creating instances of the CChecking object in memory. When you compile your project, however, if your class’ Instancing property is set to a value that makes your component creatable, VB creates another class, which we refer to as the class factory. (Values that result in a creatable component are 5 - MultiUse and 6 - GlobalMultiUse for ActiveX DLL classes and 3 - SingleUse, 4 - GlobalSingleUse, 5 - MultiUse, and 6 - GlobalMultiUse for ActiveX EXE classes.) It is called the class factory because VB adds the implementation to this class for the IClassFactory interface. The SCM loads the DLL that contains the requested class and calls DllGetClassObject. The purpose of this function is to return an instance of the class factory class, not the class you defined. The IClassFactory interface has a method called CreateInstance. The CreateInstance function has an incoming parameter of the IID for the interface the client wishes to use. CreateInstance returns a pointer to the object that implements this interface. Therefore, each creatable class you create in VB has a class factory class that VB creates. The SCM requests the class factory object and calls CreateInstance to create instances of your class. Once it creates one instance of your class, it releases the class factory object.

Observations About Activation

So why do you need to know about the process of activation? It is possible to live a healthy and fruitful life without knowing how COM in-process activation works, and in fact many VB developers build complex applications with COM objects without knowing anything at all about the process. Although you may want to learn COM simply for the sake of knowing what’s happening underneath, I feel it necessary to outline some of the benefits of knowing this information. Consider the following situation.

A developer offers you an ActiveX DLL for which you write a client. Before you can use the DLL, you must make sure it is registered. Why do you need to register the DLL? Because, as you may recall, CoCreateInstanceEx finds the ActiveX DLL by reading the InprocServer32 key from the registry. You can register the DLL with RegSvr32.EXE. Registration also adds a type library entry to HKCR\TypeLib. This is important for two reasons. One has to do with COM’s remoting layer; you will learn about it in the next chapter. The other is that it will cause VB to include the type library in the References dialog box. Why do you need to add the type library as a reference? You need it for early binding. For early binding the compiler needs to know three things from the type library: CLSIDs, IIDs, and interface layouts. VB reads the CLSIDs and IIDs from the type library in order to translate the New statement into CoCreateInstanceEx. VB also uses IIDs when it needs to make a call to QueryInterface.

Another need for understanding the activation story is to know what to do when versioning your DLLs (covered in Chapter 6).

Summary

It is now time to put together all the pieces that constitute in-process activation. The following list offers a step-by-step summation of the process:

  1. Developer compiles ActiveX DLL.

  2. VB creates a type library file and embeds it into the DLL image. It also compiles in the image a class factory class for each class you defined and adds entry points for the four COM DLL required functions: DllRegisterServer, DllUnregisterServer, DllCanUnloadNow, and DllGetClassObject.

  3. VB automatically calls DllRegisterServer on the DLL.

  4. Server code for DllRegisterServer adds a registry key for each HKCR\CLSID. It also adds a registry key for the type library under HKCR\TypeLib.

  5. VB automatically runs the registration code for your server when you compile it. If you move the DLL to another machine, then you can use RegSvr32.exe to execute the registration code in DllRegisterServer.

  6. The developer authoring the client code uses the References dialog box to locate the type library.

  7. At compile time for the client, VB reads the CLSIDs and IIDs for each class you specified in New. It then translates each New call into CoCreateInstanceEx.

  8. At runtime, the client code executes New, which is translated into the vbaNew function in the VB runtime, which is translated into CoCreateIntanceEx asking for four interfaces: IUnknown, IDefaultInterface, IPersistStreamInit, and IPersistPropertyBag. If you used CreateObject instead of New, then the call is translated into CLSIDFromProgIDEx followed by CoCreateInstance asking for IUnknown, then three QIs: IDispatch, IPersistStreamInit, and IPersistPropertyBag.

  9. CoCreateInstanceEx is a function in OLE32.DLL. It searches the registry for the CLSID you specified. It searches under HKCR\CLSID. When it finds the key, it searches for the InprocServer32 subkey to find the path to the DLL.

  10. It loads the DLL with the LoadLibrary function. Then it calls the DllGetClassObject function in the DLL.

  11. It retrieves from the DLL an instance of the class factory object for your class and stores it in an internal cache.

  12. It then calls CreateInstance on the class factory to create an in-memory instance of the class you defined.

  13. It returns a pointer to a vptr to a vtable for the interface you specified.

  14. The client makes method calls using the vtable. It knows which method in the vtable to call from having read the type library definition for the interface at compile time.

In the next chapter you are going to learn about COM’s remoting architecture and how the process activation works. In Chapter 5, we will also modify the activation story for both in-process and out-of-process servers slightly to account for the remoting architecture.

With Safari, you learn the way you learn best. Get unlimited access to videos, live online training, learning paths, books, interactive tutorials, and more.

Start Free Trial

No credit card required