|
Servers and Objects
by Binh Ly
Simply put, a server is a container for objects. Servers can be created as
DLL binaries (sometimes called in-process servers because a DLL runs “in”
the address space of the client) or as EXE binaries (sometimes called
out-of-process servers because an EXE runs in a distinct/different/”out”side
process than the client).
Some advantages of creating DLL servers are:
- DLLs are run “in-process” with the client application and therefore communication between client and server is optimal (maximum performance).
- DLLs are normally preferred when creating servers that surface some type of User Interface oriented functionality. It doesn’t make much sense to create servers as EXEs when you want the server to create forms/windows that will be “docked onto” or “plugged into” the client application.
- DLL servers are normally easier to test/debug than EXE servers-which may need to potentially be tested/debugged in remote environments. Testing/debugging DLL servers
is almost always done on a single machine and together with the client application.
- DLL servers are better suited in situations where the DLL binary architecture is advantageous such as when creating servers that need to be extension DLLs for web servers or when your server will be integrated with Microsoft Transaction Server
(MTS).
Some advantages of creating EXE servers are:
- EXE servers are more robust in terms of fault-isolation. When using EXE servers, a client that goes down (crashes) does not affect the server and vice-versa whereas in the DLL case, if the client or the server goes down, everything goes down along with it.
- EXE servers are better suited to environments where remote distribution and security are prime considerations.
- EXE servers are better suited in situations where the EXE binary architecture is advantageous, such as when creating servers that need to be NT services.
Although DLL servers are normally run in-process with the client
application, it is possible to "wrap" a DLL server inside another EXE
application that will serve as its "host" (or surrogate). By doing this, any DLL
server can be used as if it was an EXE server simply because the host executes out-of-process.
Microsoft Transaction Server (MTS) is an example of a DLL host application.
| DLL servers are becoming
more and more important because with the integration of MTS and COM in the
upcoming COM+ and
Windows 2000, the servers you will be writing are all DLLs that will be run by
a host that's part of the COM+ runtime. |
All this time, we've
been talking about how a client contacts a server and requests to create a
particular object in the server. How exactly does the client do that? Does it load the server manually using ShellExecute?
And after the
server loads, how does a client reach into the server and ask for an
object?
Consider a client that launches a server using the Win32 ShellExecute API
(for those unfamiliar with ShellExecute, it's an API that can be used to launch
external applications). ShellExecute requires a full path (or at least your
application must exist in the system path) to the application
that you want to execute. That means that if you move the server to another
location, you'll have to rebuild the client application with the new path. A
step in the right direction might be to store the path into an external location
such as the Windows registry - this way if the server relocates, we change the
path in the registry and the client simply reads the new path. Sooner or later,
our server might become energetic and may want to relocate to another machine.
Now we have a problem because ShellExecute will not work anymore, i.e. you
cannot use ShellExecute to execute an application from another machine. We'd
have to somehow develop a new application that would handle a cross-machine
ShellExecute. Again, a step in the right direction here would be to leave it to
the experts to write the application that would handle cross-machine activation.
Once that is available, we simply use it in our application.
Server location is an implementation detail that we don't want to waste a lot
of time working on. COM helps a bit in this area by defining a standard means of 1) how
servers tell clients where they are and 2) how clients activate servers without
getting tied into the gory details of location issues.
A server tells the world where it is located
using a process called
"registration", i.e. the server gives COM information about its
location. Under Windows, this location information is stored in the registry -
a conventional place to store application settings. Before we go further, let's
recall that a client is not really interested in the server itself, per se; the
client's interest is more towards the objects that are contained in the server -
after all it's the objects that actually expose interfaces and it's the objects
that do the work for the client. Therefore, what we really need stored in the
registry is a list of server objects and under each object, a subkey that points
to the physical location of the server file. This way, a client that wants to
use a particular server object will say "Hey COM, I want to use this object
named Foo." COM then goes into the registry, looks for Foo and if it finds
it, looks under its subkey to get the actual server filename, and then launches
the server. That's really the gist of server activation in COM.
Let's look at some advantages of the activation mechanism described above:
- Clients do not have to deal with
registration details. The registration process is done by the server. This means that
the server must have some code in it that gives the necessary registration
information to COM. This process commonly involves writing entries into the
Windows registry.
- If the server relocates, you simply register the server again. This way,
the registry will contain updated information about the server's
whereabouts. The next time the client needs an object from the server, COM
will know exactly where to look for the server.
- The client is rather "detached" from the server. The client
simply tells COM which object it wants and COM takes care of scanning the
registry, finding the server, and then activating the server. Why is this
important? Recall our example of the energetic server that wants to relocate
to another machine. Using the registration mechanism, this energetic server will register itself with a
location pointing to a machine name or network address of the machine it
resides in. When the client later asks for an object from that server, COM
will look in the registry, find a remote machine address, establish a
connection to the remote machine, and then activate the server on the remote
machine. In short, the client doesn't really know nor care about the
exact location of a server; all it knows is it wants a server object, it
tells COM about it, and COM takes care of the rest!
In order for a server to register its objects, there needs to be a way to
identify or name server objects. Similar in concept to our previous discussion on
interfaces, each object also has an ugly name and a friendly name. If you
recall, the ugly name is a sequence of numbers that guarantees name uniqueness
and the friendly name is the one that makes it easier for clients to remember.
Since the ugly name's purpose is to be able to uniquely identify objects (and
interfaces), it's time to introduce a more appropriate term for ugly names: globally
unique identifier or GUID for short. GUIDs in COM are generated
using an algorithm that guarantees statistically unique values. Interface
GUIDs are also called interface identifiers or IIDs and object
GUIDs are also called class identifiers or CLSIDs.
Going back to the registration process, a server registers itself by handing
out the CLSID of each of its objects. In the registry, each of those CLSIDs will
contain a subkey that points to the physical server file. For example, if you
have a server named FooServer.exe (assume FooServer.exe resides in C:\) that
contains 2 objects Foo and Bar, this is how its registration looks like
theoretically:
<CLSID registry section>
... other CLSIDs ...
CLSID of Foo
CLSID of Foo\ServerLocation = "c:\FooServer.exe"
CLSID of Bar
CLSID of Bar\ServerLocation = "c:\FooServer.exe"
...other CLSIDs...
A client that wants to create Foo simply passes COM Foo's
CLSID and COM will take care of scanning the registry to locate and activate
FooServer. Since CLSIDs are
ugly names, the client might prefer to work with friendly names. By convention,
server objects have friendly names using the <ServerName>.<ObjectName>
format. For our Foo object, that would be "FooServer.Foo" and for our
Bar object, that would be "FooServer.Bar." Friendly names for objects
are termed programmatic identifiers or PROGIDs.
To enable clients
to specify PROGIDs when creating objects, PROGIDs also get registered.
Theoretically, this is how PROGID registration looks like:
<PROGID registry section>
... other PROGIDs ...
FooServer.Foo = CLSID of Foo
FooServer.Bar = CLSID of Bar
... other PROGIDs ...
Notice how the PROGIDs simply point to the corresponding CLSIDs. So when a
client asks COM "Create me a FooServer.Foo object", COM looks in the registry
for "FooServer.Foo" in the PROGIDs section, finds it and sees that it
maps to Foo's CLSID, follows that mapping into the CLSIDs section and finds the
correct server to activate. Pretty cool huh?!
All this business of PROGIDs and CLSIDs pretty much sum up the activation
process for COM servers. If you were paying attention, it's easy to see that the
entire activation mechanism is almost made transparent to clients - all the
client needs to know is the CLSID or the PROGID of the object it wants and then
it lets COM take care of the rest. Location transparency is the term that COM
uses to describe this entire concept and process.
Object Creation
After COM activates a server, the next thing it needs to do is to somehow
reach into the server for the object that the client wants. Again, COM steps in
here and defines a standard protocol for servers to "hand out" its
objects to clients. The protocol is extremely simple and works this way:
- A client wants to create an object from a server. This client asks COM to
create that object. COM does a registry lookup to locate and
activate the corresponding server as described previously.
- Once the server is up and running, COM asks the server for an "object
creator" object. The term "object creator" means that this
object creates other objects. In particular, an object creator creates the
actual object that the client wants.
- The "object creator" object exposes a standard interface that
contains a method, CreateInstance, used to create the actual server object.
COM (or the client) then calls CreateInstance to create the server object, gets back an
interface pointer, and then runs all the way back to the client with the
interface pointer.

Figure: Standard server object
creation protocol
The "object creator" described above is what is called a
class
factory in COM (many people believe that the term "object factory" is
more appropriate since it creates objects, not classes). A class factory exposes a standard interface named
IClassFactory which contains the CreateInstance method, among others.
IClassFactory
= interface
procedure CreateInstance;
...
end;
This class
factory protocol is enforced by COM, meaning that every server must:
- Implement a class factory's function correctly. Each type of object in a
server requires is own class factory. This enables the server to hand out
the correct class factory required to create a given object that the client
asks for.
- Implement class factories as if they are regular server objects. In other
words, since a class factory exports an interface (IClassFactory), it is a
bonafide COM object. This means that class factories should reference count
and also support vtable binding.
To make things more concrete, lets consider what happens exactly when a
client asks to create Foo from our FooServer server example:
- Client asks COM "Create me a FooServer.Foo object"
- COM goes to the registry and finds the translation path from "FooServer.Foo"
to Foo's CLSID
- COM activates FooServer.exe
- COM reaches into FooServer.exe asking "Give me a class factory for this
object", passing in Foo's CLSID so the FooServer knows which
class factory to hand out. Remember, every class in FooServer has a
corresponding class factory, i.e. there is a class factory for Foo and
another class factory for Bar.
- Given Foo's CLSID, FooServer finds the corresponding class factory and, if
found, hands the class factory's IClassFactory pointer to COM
- COM calls IClassFactory.CreateInstance to create an instance of Foo. As a
result, IClassFactory.CreateInstance returns an IFoo pointer.
- COM takes the IFoo pointer and runs back to the client with it
Figure: Server object creation
using the Class Factory
| As a side note, COM also provides a
mechanism for the client to directly ask for an object's class factory. In this case, COM
will go through the same exact steps above but stops at step 5. After step 5,
COM will then run back to the client with the IClassFactory pointer. The client
can then manually call IClassFactory.CreateInstance to create instances of the server
object. |
Developing class factories is tedious and
is not for the faint of heart. Luckily, today's
development environments hide a lot of the complexity involved in creating
class factories. For instance, Visual Basic completely hides the class factory
protocol that not a lot of VBers even know of its concept!
Where Are We?
We've just seen the basics of servers, server activation, and how objects are created.
Knowledge of these two concepts are important and useful milestones in becoming
an effective COM developer.
Further Reading
- Understanding ActiveX and OLE by David
Chappell
- Inside COM by Dale Rogerson
- Essential COM by Don Box
|