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.
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.
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.
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).
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).
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.
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:
- 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 nodefault
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
orFalse
). When you mark a class as Persistable, VB adds support for theIPersistStreamInit
interface. This is as if you had writtenImplements IPersistStreamInit
in your class. Adding support forIPersistStreamInit
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.
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 QI
s 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
QI
s.
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!
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.
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.
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.
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).
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:
Developer compiles ActiveX DLL.
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
, andDllGetClassObject
.VB automatically calls
DllRegisterServer
on the DLL.Server code for
DllRegisterServer
adds a registry key for eachHKCR\CLSID
. It also adds a registry key for the type library underHKCR\TypeLib
.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 inDllRegisterServer
.The developer authoring the client code uses the References dialog box to locate the type library.
At compile time for the client, VB reads the CLSIDs and IIDs for each class you specified in
New
. It then translates eachNew
call intoCoCreateInstanceEx
.At runtime, the client code executes
New
, which is translated into thevbaNew
function in the VB runtime, which is translated intoCoCreateIntanceEx
asking for four interfaces:IUnknown
,IDefaultInterface
,IPersistStreamInit
, andIPersistPropertyBag
. If you usedCreateObject
instead ofNew
, then the call is translated intoCLSIDFromProgIDEx
followed byCoCreateInstance
asking forIUnknown
, then threeQI
s:IDispatch
,IPersistStreamInit
, andIPersistPropertyBag
.CoCreateInstanceEx
is a function inOLE32.DLL
. It searches the registry for the CLSID you specified. It searches underHKCR\CLSID
. When it finds the key, it searches for theInprocServer32
subkey to find the path to the DLL.It loads the DLL with the
LoadLibrary
function. Then it calls theDllGetClassObject
function in the DLL.It retrieves from the DLL an instance of the class factory object for your class and stores it in an internal cache.
It then calls
CreateInstance
on the class factory to create an in-memory instance of the class you defined.It returns a pointer to a vptr to a vtable for the interface you specified.
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.
Get COM+ Programming with Visual Basic now with the O’Reilly learning platform.
O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.