Delphi and Distributed COM (ver 1.01) (ver 1.1)

Copyright (c) 1997 by Charlie Calvert

Overview

Portions of the following paper tend to be a bit Delphi 2.0 centric, though Delphi 3.0 specific code is included in places. Over time I will present code and materials that are entirely specific to Delphi 3.0. Look for updates to this paper on my Web site: users.aol.com/charliecal. In particular, this paper does not discuss interfaces or type libraries, which are crucial to OLE Automation code in Delphi 3.0. The updated material will cover these subjects in depth. Even the Delphi 2.0 specific code in this paper will work fine in Delphi 3.0, but due to time constraints it ignores some important new features of the latest version of Delphi.

DCOM allows you to easily share objects over a network. In this paper you will see how to use Delphi to implement DCOM. In particular, you will see how to create two applications that can control one another across a network. In the process of describing this technology, I will also show you to create local OLE Automation objects.

Delphi and Windows NT 4.0 provide full support for the Distributed Component Object Model, or DCOM. It is also easy to add DCOM client support to Windows 95. If you wish, you can also add DCOM server support to Windows 95, though this option has some drawbacks and limitations.

Subjects covered in this paper include:

At the time of this writing, DCOM is not built into Windows 95, though it is built into Windows NT 4.0. To add DCOM support to Windows 95, you can download Windows 95 DCOM from the Microsoft Web Server. You should start looking for it in the OleDev section: www.micrsoft.com/oledev.

What is DCOM?

Before beginning a description of the fairly simple technical steps involved in implementing DCOM, it is perhaps worthwhile talking about this technology from a high level so that all the key ideas will be clear to everyone. If you already understand DCOM, and just want to see how to implement it in Delphi, then you can skip this section.

Distributed COM is important because it allows applications to talk to one another across a network. In particular, it allows you to share objects that reside on two separate machines. This means you can create an object in one application or DLL, then call the methods of that object from an application that resides on a different computer.

DCOM is built on top of COM, which is the technology that underlies both OLE and ActiveX. The relationship between COM and OLE is a bit confusing, and the boundaries separating the two technologies seems to shift at times, depending on the vagaries of the Microsoft marketing machine. In general, it is safe to say that OLE is a subset of COM. That is, everything that is part of OLE is also part of COM, but not everything that is part of COM is a part of OLE. There are, however, many people who appear to use the words COM and OLE virtually interchangeably, and indeed, the two technologies are very closely bound together.

COM is simply a specification for defining an object hierarchy. In particular, it lays out a set of rules for defining objects that can be used across applications and languages. DCOM extends this specification to allows objects on separate machines to talk to one another.

One of the most important aspects of this technology is that it allows you to distribute the load of a task across several machines. For instance, if you have a complex database query to run, then you can use DCOM to ask an object on a separate machine to run it. That way your current processor will not have to expend any clock cycles on the task, nor will any large database related tools be loaded onto memory on your own machine. This means that you are free to continue playing Doom or Quake while your server looses clock cycles and precious RAM to your background task.

It's important to understand that the COM specifications, as the name itself implies, is really only a set of rules for defining an object hierarchy. These rules include defining the names and methods of many of the key objects in the hierarchy, as well as the specific techniques for structuring the objects themselves. (When thought of in this way, there may be some value in regarding OLE as an implementation of certain parts of the COM specification.)

The COM specification can be compared to the specification for other object hierarchies such as VCL, OWL or MFC. For instance, all COM objects descend from a base class called IUnknown, just as all VCL objects descend from a base class called TObject. COM supports polymorphism and encapsulation, and it uses a series of unique interfaces to achieve the same ends as traditional inheritance in standard object oriented languages. Unlike OWL or VCL, COM is not tied to any particular language, nor is it bound by application boundaries.

In short, COM is an alternative to the VCL, OWL or MFC that attempts to go these object hierarchies one better by allowing you to use COM objects across language, application, and now even machine boundaries. This means you can write a COM object in Object Pascal, extend it in Delphi, and then use it in a third language such as Visual Basic. You can call methods of the object from inside a single application, from one application that is calling into a DLL, or from one application that is calling an object in a separate application. What DCOM brings to the picture is the ability to call objects that reside in applications located on separate machines. It allows you to "Distribute" the objects across the network. In particular, this allows you to divide up the load of running a major task across several machines.

If you already understand COM, then you are ready to use DCOM without any further work. In particular, DCOM works exactly the same way that COM works. You can, in fact, at least theoretically convert existing COM objects into DCOM objects with no change to your code. This system works great between two Windows NT machines, but Windows 95 requires that you switch from Share Level to User Level access, which crimp your style in some cases. In particular, User Level sharing requires that there be an NT machine or some other source of user access lists available on your network.

* * * Begin Note * * *

Note: You can switch from Share Level access to User Level Access via the Network applet found in the Control Panel. To then help configure your server, you can use the DComCfg.exe application freely available from Microsoft's web server. DComCfg will allow you to call DCOM servers on other machines exactly as if they were located on your current machine. In short, you can use CreateOleObject, rather than the custom CreateRemoteOleObject routine shown later in this paper.

* * * End Note * * *

If you don't want to switch to User Level access, then there are alternatives that will allow you to run DCOM as a client service on Windows 95 machines. In that case, you only you need to add a single new parameter to your calls into an OLE function named CoGetClassObject. More about this later, in the technical section of this chapter.

* * * Begin Note * * *

Note: Consider these three points:

The upshot of this is that you can't set up a DCOM server unless you have an NT machine on your network, and even under the best circumstance, a Windows 95 box is crippled as a server. Giving these facts, I prefer to have the NT machine act as my DCOM server, and to let the Windows 95 machines act only as clients.

* * * End Note * * *

The key point to grasp is that DCOM is really nothing more than a new capability added to the already existing COM technology. If you have working COM objects then it is trivial to upgrade them to work with DCOM.

At the time of this writing (February 97) DCOM has been working on Windows NT 4.0 for over 6 months, but Microsoft has only just released DCOM for Windows 95. Microsoft has stated that in the future, COM and DCOM will be ported to other platforms such as UNIX and the Mac.

Why is COM Controversial?

Having, in a sense, made the case for COM and DCOM in the previous section, its perhaps worth stepping back for a moment and describing some competing systems. This is a technical chapter, and I'm not interested in advocating any particular system. However, it's probably worth while describing the current state of this technology so that you can put this paper in perspective.

COM is a Microsoft technology that is competing with similar technologies such as CORBA and DSOM, which are created by other corporations or groups of corporations. Adherents of these alternate technologies can rally numerous arguments regarding who implemented what first and who has designed the most sophisticated technology. Furthermore, many people have invested themselves heavily in technologies such as OWL or MFC that can be seen as "competing", in some sense, with COM. OWL, MFC, and the VCL don't have the same capabilities as COM, DSOM, OPENDOC or CORBA, but still there is a feeling that the technologies are to some degree competing for the mind share of contemporary programmers. There is no specific reason why you can't use COM and VCL in the same program, and indeed that it is the approach I take in this chapter.

My point here is not to advocate any particular solution, but only to make it clear that this is a controversial topic that tends to excite strong opinions. If you are considering using COM in your projects, you might also want to take a look at CORBA and SOM. Conversely, if you hear criticisms of COM from other members of the industry, you might check to see if they are so heavily invested in some alternative technology that they are perhaps somewhat unfairly predisposed to be critical of COM and DCOM.

DCOM, IDispatch, Marshaling and OLE Automation

Delphi programmers can use the IDispatch COM interface to gain easy access to the capabilities of DCOM. This interface is encapsulated inside the Delphi TAutoObject class. If you understand TAutoObject, and the theory behind IDispatch and IMarshal, then you can probably skip this section and move on to the next.

OLE Automation is a technique that allows you to control one application from inside a second application. In particular, it allows you to control an object placed inside one application from the code of a second application.

The key to OLE Automation is a COM object called IDispatch. All OLE technologies are based on COM, and in this particular case the functionality behind OLE Automation is implemented by IDispatch. In short, OLE Automation is really just a marketing term for publicizing the technology found in IDispatch. Or, more charitably, OLE Automation is an implementation of the IDispatch specification.

IDispatch is not difficult to understand, but it can be a bit awkward at times to implement. To help simplify the use of IDispatch, the VCL has a class called TAutoObject that encapsulates all the functionality of IDispatch inside a very easy to use, and highly leveraged, technology.

This paper focuses much of its technical content on an analysis of TAutoObject. However, it is possible to automate any COM object, and not just IDispatch. I have chosen to concentrate on this one technology at the exclusion of others because it provides a simple work around to the difficult problem of trying to marshal code and data back and forth between two applications.

Marshaling is a COM specific term for the technique used to transfer data or function calls back and forth between two applications that reside in separate processes. For instance, if you have to pass a parameter to a function between two applications, then you have to be sure that it is treated properly by both applications. For instance, if you declare the parameter as an Integer in Pascal, then that means you are passing a four byte ordinal value. How do you express that same concept in C? How do you do it Visual Basic? The answers to these questions are expressed in COM by a complex interface called IMarshal that is beyond the scope of this chapter. Indeed, IMarshal is notorious for being difficult to implement.

Here is how the Microsoft documentation defines IMarshal: "'Marshaling' is the process of packaging data into packets for transmission to a different process or machine. 'Unmarshaling' is the process of recovering that data at the receiving end. In any given call, method arguments are marshaled and unmarshaled in one direction, while return values are marshaled and unmarshaled in the other." This is all good and well. Unfortunately, as I stated earlier, the IMarshal interface is very hard to implement.

If you are using a standard COM object, then you don't have to implement IMarshal because these interfaces will be marshaled for you automatically by the system. In other words, if you are implementing an instance of IDispatch, IUnknown, IClassFactory, IOleContainer, or any other predefined COM class, then you don't have to worry about Marshaling. Microsoft will take care of it for you. However, if you are creating a custom object of your own, then you need to implement IMarshal, or to come up with some alternative scheme.

Because of its complexity, C programmers also generally choose not to attempt an implementation of IMarshal. Instead, they rely on an intermediate language called IDL which can be compiled into source code by a Microsoft created program called MIDL.EXE. IDL (the Interface Definition Language) is a special language meant to allow people to define interfaces in a neutral language that can be compiled into source usable by multiple languages such as Pascal, C and Visual Basic.

In other words, you can theoretically write your COM object in C or Pascal, then use IDL to define its interface, and then use MIDL to turn that interface into a set of files usable by any language. In other words, MIDL automatically takes care of the IMarshal business for you so long as you first describe your interface in IDL.

This approach is quite reasonable, but I will not treat it in this current book, in part because Delphi does not ship with MIDL. It is, however, available from Microsoft in their SDKs, and may be freely available via their web site. You should also note that Delphi 3.0 includes tools that automate this process without having to learn IDL, or work with MIDL.

For now, however, I will back away from both IMarshal (because it is so complex) and MIDL (because it doesn't ship with Delphi). This situation would appear to leave no good way to handle DCOM, were it not for the power of IDispatch and TAutoObject. IDispatch is a COM interface designed to make it easy to control one application from inside a second application. In implementing this code, Microsoft provided an alternative means for solving the whole problem of marshaling data between applications. In TAutoObject, the VCL provides a very simple means of using IDispatch.

Thinking about IDispatch

Here is how Microsoft defines IDispatch: "IDispatch is a COM interface that is designed in such a way that it can call virtually any other COM interface." In other words, if you put a COM object in an application, then you can calls its methods from a second application by using IDispatch. This is how OLE Automation allows you to control one application from inside a second application. (In particular, IDispatch was created to help make COM programming easier from inside the limited confines of a Visual BASIC application.)

To understand why IDispatch works its necessary to remember that marshaling is taken care of for you automatically so long as you are using an existing COM interface. In other words, you don't have to implement marshaling for IDispatch because it is a standard COM object, and not a custom object designed by yourself or someone on your team. IDispatch exists to allow you to call the methods of any legal COM object. In other words, it is designed to solve the whole problem of marshaling data. As such, it is the perfect solution for Delphi programmers who want to use DCOM without engaging in too much manual labor.

Before closing this section of the Chapter, I should perhaps emphasize that you don't have to use IDispatch. If you prefer, you could use other predefined COM objects, or you could implement IMarshal, or you could attempt to use MIDL. Delphi has none of the limitation found in languages like Visual Basic, so there is no reason to be confined to IDispatch unless you find its relative simplicity appealing.

Using TAutoObject to Implement a DCOM Server

It's finally time to move away from theoretical issues and to concentrate instead on technical matters. Ironically, the theory behind this technology is much harder to understand than the technology itself. In short, this section of the paper and the next outline a simple technique for using DCOM that can be used by any intermediate level Delphi programmer.

You learned in the last few sections that TAutoObject is Delphi's wrapper around IDispatch. IDispatch is the COM object that makes OLE Automation possible. In other words, this section of the paper shows how to implement OLE Automation that works not only between two applications, but between two applications that reside on separate machines.

If you go to the File menu in Delphi and choose New, you can pop up the Object Repository. On the first page of the Object Repository is an icon you can select if you want to create an Automation Object.

After selecting the Automation Object icon, you are presented with a dialog. You can fill in the fields of this dialog as you like, or you can put in the following default values:

ClassName: IMyDCOM

OLE ClassName: MyProj.IMyDCOM

Description: My DCOM Object

Instancing: Multiple Instance

When you are done, Delphi spits out a page of code that looks like this:

unit Unit2;
 
interface
 
uses
  OleAuto;
 
type
  IMyDCOM = class(TAutoObject)
  private
    { Private declarations }
  automated
    { Automated declarations }
  end;
 
implementation
 
procedure RegisterIMyDCOM;
const
  AutoClassInfo: TAutoClassInfo = (
    AutoClass: IMyDCOM;
    ProgID: 'Project1.IMyDCOM';
    ClassID: '{7DA2AE60-BFEE-11CF-8CCD-0080C80CF1D2}';
    Description: 'My DCOM object';
    Instancing: acMultiInstance);
begin
  Automation.RegisterClass(AutoClassInfo);
end;
 
initialization
  RegisterIMyDCOM;
end.
 
 

The RegisterIMyDCOM procedure is used to register your object with the system, that is, to list it in the registry. The details of this process are described in the section after next, called "Registration Issues". For now, you need only take note of the ClassID assigned to your object, since you will need this ID when you try to call the object from another machine, as described in the next section of this paper.

The act of registering the object is not something you necessarily have to understand, since it will occur automatically whenever you run the client application of which IMyDCOM is a part. Note that the object will be registered repeatedly, whenever you run the program, which ensures that you will find it easy to register the object, while simultaneously requiring very little overhead in terms of system resources. If you move the application to a new location, you can register this change with the system by running it once. This guarentees that the old items associated with your CLSID will be erased, and new items will be filled in their place. Registering a class ID multiple times does not mean that you will end up with multiple items in the registry, since each registration of a CLSID will overwrite the previous registration.

Besides the registration procedure, the other key part of the code generated by the Automation Expert is the class definition found at the top of the unit:

  IMyDCOM = class(TAutoObject)
  private
    { Private declarations }
  automated
    { Automated declarations }
  end;
 
 

This code has two sections, one called private and the other called automated. The automated section is where you can declare methods or properties that you want to call across program or machine boundaries. In other words, any methods or properties that you declare in this space will automatically be marshaled for you by the underlying IDispatch object encapsulated by TAutoObject.

Consider the following code fragment:

type
  IMyDCOM = class(TAutoObject)
  private
    { Private declarations }
  automated
    function GetName: string;
    function ASquare(A: Integer): Integer;
  end;
 
function IMyDCOM.GetName: string;
begin
  Result := 'IMyDCOM';
end;
 
function IMyDCOM.ASquare(A: Integer): Integer;
begin
  Result := A * A;
end;    
 
 

The IMyDCOM object now exports two methods that IDispatch will automatically marshal for you across application or machine boundaries. You can go on adding methods to this object as you like. Any data that you want to add to the object should go in the private section, and any methods or properties that you don't want to export should also go in the private section. All methods that you want to call from inside another application should go in the automated section.

There are some limits to the marshaling that will be done for you by IDispatch. In particular, the following types are legal to use in the declarations for the methods or properties in the automated section:

Currency

Double

Integer

LongInt

Single

SmallInt

String

TDateTime

Variant

WordBool

The following types are illegal to use in the declarations for the methods or properties in the automated section:

Arrays

Boolean

Byte

Cardinal

Comp

Pointer

POleStr

Records

ShortInt

Word

The apparent limitations created by the lack of support from IDispatch for custom types can be considerably mitigated by an intelligent use of variant arrays. These structures can be so helpful that I have added a section to the end of this paper that describes there use.

That's all I'm going to say for now about creating the server side of a Delphi DCOM project. Remember that this code will not work unless you first register the IMyDCOM object with the system by running the server once. After you run the server the first time, you never have to run it again, as it will be called automatically by the client program described in the next section. Once again, the whole point of this exercise is that the client program can be located on a separate machine.

Creating the DCOM Client

The following unit will call the functions in the server program described above:

unit Main;
 
interface
 
uses
  Windows, Messages, SysUtils,
  Classes, Graphics, Controls,
  Forms, Dialogs, StdCtrls;
 
type
  TForm1 = class(TForm)
    Button1: TButton;
    procedure Button1Click(Sender: TObject);
  end;
 
var
  Form1: TForm1;
 
implementation
 
uses
  Ole2, OleBox;
 
{$R *.DFM}
 
const
  CLSID_MyDCOM: TGUID = (
    D1:$7DA2AE60; 
    D2:$BFEE; 
    D3:$11CF; 
    D4:($8C,$CD,$00,$80,$C8,$0C, $F1, $D2));
 
procedure TForm1.Button1Click(Sender: TObject);
var
  V: Variant;
begin
  Screen.Cursor := crHourGlass;
  V := CreateRemoteOleObject(CLSID_MySam, 'CharlieC');
  Screen.Cursor := crDefault;
  ShowMessage(V.Getname);
  ShowMessage(V.ASquare(10));
end;
 
end.
 
 

The code first declares the CLSID created by the Delphi Automation Expert in the previous section of this paper. This is the CLSID associated with the server half of this DCOM project, and you need to include it here in the client program. Additional information about CLSIDs and the registration process will be presented below.

The actual call to automate the object is nearly identical the call you would make if you wanted to automated an object on your local machine. The only difference is that you call CreateRemoteOleObject rather than CreateOleObject.

CreateRemoteOleObject is a custom function I have written that looks like this:

function CoCreateInstanceEx; external ole32 name 'CoCreateInstanceEx';
 
function GetRemoteOleObject(ClassID: TGUID; const Server: string): Variant;
var
  Unknown: IUnknown;
  ClassFactory: IClassFactory;
  Info: TCoServerInfo;
  Dest: Array[0..127] of WideChar;
begin
  ClassFactory := nil;
  Info.dwReserved1 := 0;
  Info.pwszName := StringToWideChar(Server, Dest, SizeOf(Dest) div 2);
  Info.pAuthInfo := nil;
  Info.dwReserved2 := 0; 
  OleCheck(CoGetClassObject(ClassID, CLSCTX_REMOTE_SERVER, @Info,
                            IID_IClassFactory, ClassFactory));
  if ClassFactory = nil then
    ShowMessage('No Class Factory')
  else
    ClassFactory.CreateInstance(nil, IID_IUnknown, Unknown);
  try
    Result := VarFromInterface(Unknown);
  finally
    ClassFactory.Release;
    Unknown.Release;
  end;
end;
 
function CreateRemoteOleObject(ClassID: TGUID; const Server: string): Variant;
var
  Info: TCoServerInfo;
  Dest: Array[0..127] of WideChar;
  MultiQI: TMultiQi;
begin
  MultiQi.IID := @IID_IDispatch;
  MultiQI.Unknown := nil;
  FillChar(Info, sizeOF(Info), #0);
  Info.pwszName := StringToWideChar(Server, Dest, SizeOf(Dest) div 2);
  OleCheck(CoCreateInstanceEx(ClassID, nil, CLSCTX_REMOTE_SERVER,
                              @Info, 1, @MultiQI));
  try
    Result := VarFromInterface(MultiQI.Unknown);
  finally
    MultiQi.Unknown.Release;
  end;
end;
 

CoServerInfo and CoCreateInstanceEx are declared for you in Delphi 3.0. In fact, a routine called GetRemoteCOMObject is declared in COMOBJ.PAS, which ships with Delphi 3. However, if you prefer to use custom code, here is code for use in Delphi 3.0:

function GetRemoteOleObject(ClassID: TGUID; const Server: string): Variant;
var
  Unknown: IUnknown;
  ClassFactory: IClassFactory;
  Info: TCoServerInfo;
  Dest: Array[0..127] of WideChar;
begin
  ClassFactory := nil;
  Info.dwReserved1 := 0;
  Info.pwszName := StringToWideChar(Server, Dest, SizeOf(Dest) div 2);
  Info.pAuthInfo := nil;
  Info.dwReserved2 := 0; 
  OleCheck(CoGetClassObject(ClassID, CLSCTX_REMOTE_SERVER, @Info,
                            IClassFactory, ClassFactory));
  if ClassFactory = nil then
    ShowMessage('No Class Factory')
  else
    ClassFactory.CreateInstance(nil, IUnknown, Unknown);
  try
    Result := Unknown;
  finally
    ClassFactory._Release;
    Unknown._Release;
  end;
end;
 
function CreateRemoteUnknown(ClassID: TGUID; const Server: string): IUnknown;
var
  Info: TCoServerInfo;
  Dest: Array[0..127] of WideChar;
  MultiQI: TMultiQi;
  Guid: TGuid;
begin
  Guid := IDispatch;
  MultiQi.IID := @Guid;
  MultiQI.Unknown := nil;
  FillChar(Info, sizeOF(Info), #0);
  Info.pwszName := StringToWideChar(Server, Dest, SizeOf(Dest) div 2);
  OleCheck(CoCreateInstanceEx(ClassID, nil, CLSCTX_REMOTE_SERVER,
                              @Info, 1, @MultiQI));
  Result := MultiQI.Unknown;
end;
 
function CreateRemoteOleObject(ClassID: TGUID; const Server: string): Variant;
begin
  Result := CreateRemoteUnknown(ClassID, Server) as IDispatch;
end;
 

Remember that this code is not really necessary if you are able to set up DComCfg.exe on your Windows 95 machine, or if you are using Windows NT on all your machines. In many cases, however, it is simpler to use these routines than to wrestle with Windows 95 and DComCfg.exe.

These CreateRemoteOleObject routines takes two parameters. The first contains the ID of the object you want to obtain, and the second the name of the server where the object resides. (Sometimes you may have to use the IP address itself, rather than the name of the server.) CreateRemoteOleObject returns a variant that "contains" a copy of the object that you want to call. You can use this variant to call all the methods in the automated section of your object:
var V: Variant; S: String; I: Integer;begin V := CreateRemoteOleObject(CLSID_MySam, 'CharlieC'); S := V.Getname; I := V.ASquare(10);end;

Variants are a special Delphi type that can contain a wide variety of data types, including OLE objects. When you call the methods of an OLE object off a variant then there is no run time checking for the calls. Delphi just assumes you know what you are doing, and if the call fails, you won't know until run time. For more information on Variants, see the Delphi online help, or the SYSTEM.PAS source file that ships with Delphi.

The key call in CreateRemoteOleObject is to CoGetClassObject:

OleCheck(CoGetClassObject(ClassID, CLSCTX_REMOTE_SERVER, @INFO,
                         IID_IClassFactory, ClassFactory));
 
 

This routine has long been a part of COM, but it has been altered slightly to support DCOM. Here is how the routine is currently declared in OLE2.PAS:

function CoGetClassObject(
  const clsid: TCLSID;   // The ID of the object you want
  dwClsContext: Longint; // In process, local or remote server?
  pvReserved: Pointer;   // Previously reserved, now used!!!!
  const iid: TIID;       // Usually IID_IClassFactory
  var pv):               // Where the class factory is returned
HResult; stdcall;        // Success? Error?
 
 

The third parameter, that was previously reserved, is now the place where you pass in the name of the server you want to access. The server is usually designated with either a string, or a literal IP address, such as 143.186.149.111. You would pass in the IP address in the form of a string. That is, don't try to pass a number, just put the IP address in quotes, and pass it in as a string. Here is the new declaration for CoGetClassObject:

function CoGetClassObject(
  const clsid: TCLSID;       // The ID of the object you want
  dwClsContext: Longint;     // In process, local or remote server?
  ServerInfo: PCoServerInfo; // Previously reserved, now used!!!!
  const iid: TIID;           // Usually IID_IClassFactory
  var pv):                   // Where the class factory is returned
HResult; stdcall;            // Success? Error?
 
 

In particular, here are the declarations for the records you pass in for the third parameter. You need this declaration for Delphi 2.0, but they should be included in Delphi 3.0:

const
  SOleError = 62211;
  CLSCTX_REMOTE_SERVER = $10;
 
type
  PMultiQi = ^TMultiQI;
  TMultiQi = record
    IID: PIID;
    Unknown: IUnknown;
    hr: HRESULT;
  end;
 
PCoAuthIdentity = ^TCoAuthIdentity;
TCoAuthIdentity = record
//  User: PUSHORT;
  UserLength: ULong;
//  Domain: PUShort;
  DomainLength: ULong;
//  Password: PUShort;
  PasswordLength: ULong;
  Flags: ULong;
end;
 
PCoAuthInfo = ^TCoAuthInfo;
TCoAuthInfo = record
  dwAuthnSvc: DWord;
  dwAuthzSvc: DWord;
  pwszServerPrincName: PWideChar;
  dwImpersonationLevel: DWord;
  pAuthIdentityData: PCoAuthIdentity;
  dwCapabilities: Dword;
end;
 
PCoServerInfo = ^TCoServerInfo;
TCoserverinfo = record
  dwReserved1: DWord;
  pwszName: PWideChar;
  pAuthInfo: PCoAuthInfo;
  dwReserved2: DWord;
end;
 
 

The first field of this record is just a version check field which should contain the size of the TCoServerInfo record. The second parameter contains a Unicode string that has the name of the server or its IP address embedded in it. You can use the StringToWideChar function shown above to convert a standard Delphi string into a Unicode string.

The call to CoGetClassObject retrieves a ClassFactory. Once you have the ClassFactory back from the server, then you can use it to retrieve an instance of the object you want to call. What you retrieve back, of course, is an instance of IDispatch. You can convert this instance into a variant by calling the Delphi routine VarFromInterface, which is found in the OLEAUTO unit that ships with Delphi.

I am currently storing the entire CreateRemoteOleObject routine in a unit of my own called OLEBOX.PAS, which is appended to the end of this paper. By the time I give the talk at BDC, this routine will probably have changed to incorporate a new version of CoCreateInstance called CoCreateInstanceEx. CoCreateInstanceEx is superior to CoGetClassObject because it retrieves the object you want with only one call, rather than having to first get the ClassFactory, and then call CreateInstance on the ClassFactory. In short, CoCreateInstanceEx executes faster than CoGetClassObject. (Remember, all calls between objects on separate machines are going to have a considerable overhead associated with them!)

Before closing this section of the paper, let me review the key points covered so far:

Registration Issues

Before closing this paper I want to mention a few issues about CLSIDs and the Registry. If you already understand the registry, then you can skip this section.

The registry is a place where information can be stored. It's a database.

CLSIDs are statistically unique numbers that can be used by the operating system to reference an OLE object. CLSIDs are stored in the Registry.

In this case, it's probably best if you visit the actual perpetrator in its native habitat. In the example explained here, I'm assuming that you have a copy of Word loaded on your system.

To get started, use the Run menu on the Windows Taskbar to launch the RegEdit program that ships with Windows NT. Just type in the word RegEdit and press the OK button. Search through the HKEY_CLASSES_ROOT for the Word.Basic entry, shown in Figure 29.2. When you find it, you can see that it's associated with the following CLSID:

{000209FE-0000-0000-C000-000000000046}
 
 
 

This is a unique class ID that is inserted into the registry of all machines that contain a valid, and properly installed, copy of Word for Windows. The only application that uses this ID is Word for Windows. It belongs uniquely to that application.

Figure 3. If you run the Windows program REGEDIT.EXE, then you can see the registration database entry for Word.Basic under HKEY_CLASSES_ROOT.

Now go further up HKEY_CLASSES_ROOT and look for the CLSID branch. Open it up and search for the CLSID shown above. When you find it, you can see two entries associated with it: one is called LocalServer, or LocalServer32, and the other is called ProgID. The ProgID is set to word.basic. The LocalServer entry looks something like this:

C:\WINWORD\WINWORD.EXE /Automation
 

If you take a look at this command, you can begin to grasp how Windows can translate the CLSID passed to CoGetClassObject into the name of an executable. In particular, Windows looks up the CLSID in the registry, and then uses the LocalServer32 entry to find the directory and name of the executable or DLL you want to launch.

Having these kinds of entries in the registration database does not mean that the applications in question are necessarily automation servers. For instance, there are many applications with LocalServer and ProgID entries that are not automation servers. However, all automation servers do have these two entries. Note, further, that this is a reference to the automation server in Word, and not a reference to Word as a generic application. It references an automation object inside of Word, and not Word itself. (The automation object is an instance of IDispatch. It was not created with TAutoObject, but it has all the same attributes.)

The same basic scenario outlined here takes place when you call CoGetClassObject and specify the CLSID of an object on another machine. In particular, Windows contacts the specified machine, asks it to look up the CLSID in the Registry, and then marshals information back and forth between the two machines.

CLSID are said to be statistically unique. You can create a new CLSID by calling CoCreateGuid. The following code shows one way to make this call:

  CoInitialize(nil);
  CoCreateGuid(GUID);
  StringFromCLSID(GUID, P);
  Edit1.Text := WideCharToString(P);
  S := ParseGuid(Edit1.Text);
  CoUninitialize;
 
 

The code shown here begins by calling CoInitialize, which is usually unnecessary in Delphi since the OLE2 unit will call this function automatically when your program is launched, that is, it will do so if you include OLE2 in the uses clause of one of your units.

CoCreateGuid is the call that retrieves the new CLSID from the system. This ID is guaranteed to be unique so long as you have a network card on your system. Each network card has a unique number on it, and this card number is combined with the date and time and other random bits of information to create a unique number that could only be generated on a machine with your network card at a particular date and time. Rumors that the phase of the moon and current age of Bill Gates children are also factored in are probably not true. At any rate, the result is a number which is guaranteed to be statistically unique, within the tolerance levels for your definition of that word given your faith in mathematicians in general, and Microsoft based mathematicians in particular.

The StringFromCLSID routine converts a CLSID into a string and the ParseGuid routine is a custom function I wrote to convert a string of this type:

{FC41CC90-C01D-11CF-8CCD-0080C80CF1D2}
 
 
 

into a record of type TGUID that can be used in a Delphi application:

CLSID_MyObject: TGUID = (      
  D1:$FC41CC90;
  D2:$C01D;
  D3:$11CF;
  D4:($8C,$CD,$00,$80,$C8,$0C,$F1,$D2));
 
 

Here is the code for ParseGuid:

function ReplaceString(NewStr, ReplaceStr, Data: string): string;
var
  OffSet: Integer;
begin
  OffSet := Pos(ReplaceStr, Data);
  Delete(Data, OffSet, Length(ReplaceStr));
  Insert(NewStr, Data, OffSet);
  Result := Data;
end;
 
function TForm1.ParseGUID(S: string): string;
var
  Len, i: Integer;
begin
  FOrigClassID := S;
  S := ReplaceString('D1:$', '{', S);
  S := ReplaceString(';D2:$', '-', S);
  S := ReplaceString(';D3:$', '-', S);
  S := ReplaceString(';D4:($', '-', S);
  S := ReplaceString(',$', '-', S);
  S := ReplaceString('));', '}', S);
  for i := 1 to 7 do begin
    Len := Length(S);
    if i <> 6 then
      Insert(',$', S, Len - (4 * i));
  end;
  S := '  CLSID_++++: TGUID = (' + #13#10#32#32#32#32 +  S;
  Result := S;
end;
 
 

That is all I want to say about the registry for now. At the time when I deliver this talk I should have additional information to give about inserting entries into the registry that reference objects on other machines, as well as using the registry to add security to your programs.

Using Variant Arrays to Pass Data

Delphi enables you to create variant arrays, which are the Delphi version of the safe arrays used in OLE Automation. You can use Variant arrays to pass large chunks of data back and forth between COM objects. For example, you could pass a bitmap, AVI file, or text file between two application using Variant arrays. In short, this type can help you avoid the shortcomings created by the limited types supported by IDispatch.

Variant arrays (and safe arrays) are costly in terms of memory and CPU cycles, so you would not normally use them except in automation or DCOM code, or in special cases where they provide obvious benefits over standard arrays. For instance, the database code makes some use of variant arrays.

The most important calls for manipulating variant arrays are VarArrayCreate and VarArrayOf. In particular, these functions are both used to create variant arrays.

The declaration for VarArrayCreate looks like this:

function VarArrayCreate(const Bounds: array of Integer;
  VarType: Integer): Variant;
 

The Bounds parameter defines the dimensions of the array. The VarType parameter defines the type of variable stored in the array. A one-dimensional array of variants would be allocated like this:

MyVariant := VarArrayCreate([0, 5], varVariant);
 

This array has six elements in it, where each element is a variant. You can assign a variant array to one or more of the elements in this array. That way you can have arrays within arrays within arrays, if you so desire.

If you know the type of the elements to be used in an array, you can set the VarType parameter to that type. For instance, if you knew you were going to be working with integers, you could write:

MyVariant := VarArrayCreate([0, 5], varInteger);
 
 
 

You cannot use varString in the second parameter; instead, use varOleStr. Remember that an array of Variant takes up 16 bytes for each member of the array, while other types might take up less space.

Arrays of Variant can be resized with the VarArrayRedim function:

procedure VarArrayRedim(var A: Variant; HighBound: Integer);
 
 
 

The variable to be resized is passed in the first parameter, and the number of elements to be contained in the resized array is held in the second parameter.

A two-dimensional array would be declared like this:

MyVariant := VarArrayCreate([0, 5, 0, 5], varVariant);
 
 
 

This array has two dimensions, each with six elements. To access a member of this array you would write code that looks like this:

procedure TForm1.GridClick(Sender: TObject);
var
  MyVariant: Variant;
begin
  MyVariant := VarArrayCreate([0, 5, 0, 5], varVariant);
  MyVariant[0, 1] := 42;
  Form1.Caption := MyVariant[0, 1];
end;
 
 

Notice that the array performs type conversions for you, as it is an array of variants and not, for instance, an array of integer.

You can use the VarArrayOf routine to quickly construct a one-dimensional variant array:

function VarArrayOf(const Values: array of Variant): Variant;
 
 
 

The function internally calls VarArrayCreate, passing an array of Variant in the first parameter and varVariant in the second parameter. Here is a typical call to VarArrayOf:

V := VarArrayOf([1, 2, 3, 'Total', 5]);
 
 
 

The following code fragment shows how to use the VarArrayOf function:

procedure TForm1.ShowInfo(V: Variant);
begin
  Caption := V[3];
end;
 
procedure TForm1.Button1Click(Sender: TObject);
var
  V: Variant;
begin
  V := VarArrayOf([1, 2, 3, 'Total', 5]);
  ShowInfo(V);
end;
 
 

This code prints the word "Total" in the caption of Form1.

The ShowInfo method demonstrates how to work with a variant array passed as a parameter. Notice that you don't have to do anything special to access a variant as an array. The type travels with the variable.

If you tried to pass a variant with a VType of varInteger to this function, Delphi would raise an exception when you tried to treat the variant as an array. In short, the Variant must have a VType of VarArray or the call to ShowInfo will fail. You can use the VarType function to check the current setting for the VType of a Variant, or you can call VarIsArray, which returns a Boolean value.

You can use the VarArrayHighBound, VarArrayLowBound, and VarArrayDimCount functions to find out about the number of dimensions in your array, and about the bounds of each dimension. The following function creates a pop-up message box showing the number of dimensions in a variant array, as well as the high and low values for each dimension:

procedure TForm1.ShowInfo(V: Variant);
var
  Count, HighBound, LowBound, i: Integer;
  S: string;
begin
  Count := VarArrayDimCount(V);
  S := #13 + 'DimCount: ' + IntToStr(Count) + #13;
  for i := 1 to Count do begin
    HighBound := VarArrayHighBound(V, i);
    LowBound := VarArrayLowBound(V, i);
    S := S + 'HighBound: ' + IntToStr(HighBound) + #13;
    S := S + 'LowBound: ' + IntToStr(LowBound) + #13;
  end;
  ShowMessage(S);
end;
 
 

This routine starts by getting the number of dimensions in the array. It then iterates through each dimension, retrieving its high and low values. If you created an array with the following call:

MyVariant := VarArrayCreate([0, 5, 1, 3], varVariant);
 
 
 

the ShowInfo function would produce the following output if passed MyVariant:

DimCount: 2
HighBound: 5
LowBound: 0
HighBound: 3
LowBound: 1
 
 

ShowInfo would raise an exception if you passed in a variant that would cause VarIsArray to return False.

There is a certain amount of overhead in working with variant arrays. If you want to process the arrays quickly, you can use two functions called VarArrayLock and VarArrayUnlock. The first of these routines returns a pointer to the data stored in an array. In particular, VarArrayLock takes a variant array and returns a standard Pascal array. For this to work, the array must be explicitly declared with one of the standard types, such as Integer, Bool, string, Byte, or Float. The type used in the variant array and the type used in the Pascal array must be identical.

Here is an example of using VarArrayLock and VarArrayUnlock:

const
  HighVal = 12;
 
function GetArray: Variant;
var
  V: Variant;
  i, j: Integer;
begin
  V := VarArrayCreate([0, HighVal, 0, HighVal], varInteger);
  for i := 0 to HighVal do
    for j := 0 to HighVal do
      V[j, i] := i * j;
  Result := V;
end;
 
procedure TForm1.LockedArray1Click(Sender: TObject);
type
  TData = array[0..HighVal, 0..HighVal] of Integer;
var
  i, j: Integer;
  V: Variant;
  Data: ^TData;
begin
  V := GetArray;
  Data := VarArrayLock(V);
  for i := 0 to HighVal do
    for j := 0 to HighVal do
       Grid.Cells[i, j] := IntToStr(Data^[i, j]);
  VarArrayUnLock(V);
end;
 
 

Notice that this code first locks down the array, then accesses it as a pointer to a standard array. Finally, it releases the array when the operation is finished. You must remember to call VarArrayUnlock when you are finished working with the data from the array:

  Data := VarArrayLock(V);
  for i := 0 to HighVal do
    for j := 0 to HighVal do
       Grid.Cells[i, j] := IntToStr(Data^[i, j]);
  VarArrayUnLock(V);
 
 

Remember that the point of using VarArrayLock and VarArrayUnlock is that it speeds access to the array. The actual code you write is more complex and verbose, but the performance is faster.

One of the most useful reasons for using a variant array is to transfer binary data to and from a server. If you have a binary file, say a WAV file or AVI file, you can pass it back and forth between your program and an OLE server using variant arrays. Such a situation would present an ideal time for using VarArrayLock and VarArrayUnlock. You would, of course, use VarByte as the second parameter to VarArrayCreate when you were creating the array. That is, you would be working with an array of Byte, and accessing it directly by locking down the array before moving data in and out of the structure. Such arrays are not subject to translation while being marshaled across boundaries.

Listing 1 contains a single example program that encapsulates most of the ideas that you have seen in this section on variant arrays. The program from which this code is excerpted is called VarArray.

Listing 1 The VarArray program shows how to use variant arrays.

unit main;
 
interface
 
uses
  Windows, Messages, SysUtils,
  Classes, Graphics, Controls,
  Forms, Dialogs, StdCtrls,
  Menus, Grids;
 
type
  TForm1 = class(TForm)
    Grid: TStringGrid;
    MainMenu1: TMainMenu;
    Info1: TMenuItem;
    OneDimensino1: TMenuItem;
    TwoDimension1: TMenuItem;
    Show1: TMenuItem;
    Normal1: TMenuItem;
    LockedArray1: TMenuItem;
    procedure bOneDimClick(Sender: TObject);
    procedure bTwoDimClick(Sender: TObject);
    procedure Normal1Click(Sender: TObject);
    procedure LockedArray1Click(Sender: TObject);
  private
    procedure ShowInfo(V: Variant);
  end;
 
var
  Form1: TForm1;
 
implementation
 
{$R *.DFM}
 
procedure TForm1.ShowInfo(V: Variant);
var
  Count, HighBound, LowBound, i: Integer;
  S: string;
begin
  Count := VarArrayDimCount(V);
  S := #13 + 'DimCount: ' + IntToStr(Count) + #13;
  for i := 1 to Count do begin
    HighBound := VarArrayHighBound(V, i);
    LowBound := VarArrayLowBound(V, i);
    S := S + 'HighBound: ' + IntToStr(HighBound) + #13;
    S := S + 'LowBound: ' + IntToStr(LowBound) + #13;
  end;
  ShowMessage(S);
end;
 
procedure TForm1.bOneDimClick(Sender: TObject);
var
  V: Variant;
begin
  V := VarArrayOf(['Varariant Info', 12, 15, 23, 25]);
  ShowInfo(V);
end;
 
procedure TForm1.bTwoDimClick(Sender: TObject);
var
  V: Variant;
begin
  V := VarArrayCreate([0, 5, 1, 5], varVariant);
  ShowInfo(V);
end;
 
function GetArray: Variant;
var
  V: Variant;
  i, j: Integer;
begin
  V := VarArrayCreate([0, 12, 0, 12], varInteger);
  for i := 0 to 12 do
    for j := 0 to 12 do
      V[j, i] := i * j;
  Result := V;
end;
 
procedure TForm1.Normal1Click(Sender: TObject);
var
  i, j: Integer;
  V: Variant;
begin
  V := GetArray;
  for i := 0 to VarArrayHighBound(V, 1) do
    for j := 0 to VarArrayHighBound(V, 2) do
       Grid.Cells[i, j] := V[i, j];
end;
 
procedure TForm1.LockedArray1Click(Sender: TObject);
type
  TData = array[0..12, 0..12] of Integer;
var
  i, j: Integer;
  V: Variant;
  Data: ^TData;
begin
  V := GetArray;
  Data := VarArrayLock(V);
  for i := 0 to 12 do
    for j := 0 to 12 do
       Grid.Cells[i, j] := IntToStr(Data^[i, j]);
  VarArrayUnLock(V);
end;
 
end.
 
 

This program has two menu items:

Remember that variant arrays are of use only in special circumstances. They are useful tools, especially when making calls to OLE automation objects. However, they are slower and bulkier than standard Delphi arrays, and should be used only when necessary.

Summary

In this paper you have learned how to use Delphi to build applications that take advantage of the Distributed Component Object Model. You have seen that combining Delphi, DCOM and OLE Automation provides a simple method for allowing one application to control or use another application that resides on a second machine.

Please remember that this paper will be updated at the conference with material that is completely specific to Delphi 3.0. In the updated materials you will find information on type libraries, interfaces, and more details on using DComCfg.exe with Windows 95.

 

Íàçàä