Image of Navigational Map linked to Home / Contents / Search Creating Remote DCOM Objects on Specific Servers

by Jim Karabatsos - GUI Computing
Image of Line Break

DCOM is really *hot* technology. The ability to distribute ActiveX servers across several machines opens up some very interesting possibilities in application architecture. DCOM is ready *now* for use in production applications, as long as you are prepared to work within the limits of the state of the art.

That's not to say everything is just rosy. One of the problems with using DCOM from VB is that the canonical way to set it up is to use DCOMCNFG to configure the system registry on the client machine so that a particular COM object is associated with a particular server. The client application then just creates the COM object in exactly the same way that it would a local object, with OLE handling the details of resolving which server is to supply the object.

That's OK in many scenarios, but it can be a bit restrictive in others. One of the real trends that I am beginning to see is the marked preference to minimise the configuration issues on the client. Ideally, the client should just be able to load an executable (perhaps from a network share) and run it without needing to set anything up on the client machine. To me, this is the attraction of "browser-hosted" applications -- not the fact that a browser is involved but that deployment is simple.

Well, that's the theory, anyway.

One project I have been working on recently has used a three-tier architecture where an ActiveX server object runs on the same machine as an SQL Server database. That object communicates with that database and another legacy application database using ODBC drivers. The application is not going to run on the internet, but we did want to limit access to the legacy database to just one connection (for monetary reasons -- do you realise that the mainframe guys are still charging megabucks per bum for access to their systems?). We also wanted to be able to walk up to any workstation and use the application, without "installing" anything. (Well, we did have to install DCOM, but that's now part of the OS as far as I'm concerned).

The problem was to allow the client to connect to the server (using DCOM) *without* requiring that we run DCOMCNFG on the client to specify the server machine. You'd think that this would be pretty fundamental stuff. What I wanted was essentially a CreateObject function that took an additional parameter - the name of the server to create the object on.

It turns out that this is deemed to be just too complex for us poor simple-minded VB programmers, so nobody has bothered to show us how to do it. All the literature I could find refered to using DCOMCNFG. Someone (in a newsgroup somewhere, I forget where) suggested that we use DCOMCNFG to set up the registry entries for a fictitious server and then searching the registry for the server name. That would tell us which keys are needed and we could change them before calling CreateObject. Of course, you would still need to use DCOMCNFG to set up remoting in the first place.

I actually looked at expanding on this idea but it is really ugly. You need to register the object first, then change the key. It *can* be done, but it's messy.

In desperation, I delved into the revisions to the OLE API introduced with DCOM. Guess what, it really isn't that hard, although I still need to do something that I would rather not do (at least until I get time to look into this a little bit further -- read on).

It turns out that it is really quite easy to create a remote object on any arbitrary machine. All you need is it's CLSID, you know, one of those 128-bit GUIDs that your registry is full of. You will need to find out the GUID of your server object. If you don't know how to do this, then you really should read "Inside OLE" by Craig Brockschmidt. In a nutshell, use RegEdit (carefully!) to look in HKEY_CLASSES_ROOT for your servers name (in the form MyServer.MyObject). Under that, you should find a "Clsid" branch and the default value for that is the CLSID in the canonical format (ie with the braces).

Now in Delphi (or C++ if you really must) you could create a remote server based on that GUID using that GUID alone. For example, here is some code written by Charles Calvert (of Delphi Unleashed fame) to create a server object on the named server (with a few typos fixed, refer to http://mix.hive.no/~xman/delphicom.html for the full article).

function CreateRemoteOleObject(ClassID: TGUID; const Server: string): Variant;
var
  Unknown: IUnknown;
  ClassFactory: IClassFactory;
  WideCharBuf: array[0..127] of WideChar;
  Info: TCoServerInfo;
  Dest: array[0..127] of WideChar;
begin
  ClassFactory := nil;
  Info.dwSize := SizeOf(Info);
  Info.pszName := StringToWideChar(Server, Dest, SizeOf(Dest) div 2);
  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;

// an example of using the above function:

const
  CLSID_MyObject: TGUID = (
    D1:$7DA2AE60;
    D2:$BFEE;
    D3:$11CF;
    D4:($8C,$CD,$00,$80,$C8,$0C, $F1, $D2));
    
    //  {7DA2AE60-BFEE-11CF-8CCD0080C80CF1D2}

var
  V: Variant;
  V := CreateRemoteOleObject(CLSID_MyObject, 'MyServer');

Note how in Delphi we can just declare a CLSID as a record of type TGUID. Obviously, the number must match that of your server object. Check it. Then check it again. Then get your mother to check it...

I'll leave you to read the article to figure out the details about how it works. For reasons I won't go into, I could not use this approach as I needed to do it all in VB.

The problem was that I had no readily apparent way to convert a human-readable format GUID into the appropriate binary structure and to work with the UNICODE strings directly across the calls to the OLE libraries (Unimess TM strikes again!). I'll get around to figuring it out one of these days, but, being pushed against a tight deadline, it was time to don the Micky Mouse ears and go into hack mode...

What I ended up doing was to deploy onto the client a cut-down version of my server object. I actually used my binary compatibility target, you know, that EXE that contains just empty procedures that effectively acts as a type library for your VB5 project. I could use standard installation tools (by which I mean WISE) to deploy and register this server on the client machine. What this does is allow me to use the API to convert the name of the server to a binary CLSID using a call to CLSIDFromProgID, as shown below. This uses the local registry to return the CLSID, which we can then use in a call to CoCreateInstanceEx. This code came (in large part) from a newsgroup posting; unfortunately, I have long lost the name of the author.

In a module somewhere, include this code:

   Private Type SERVER_STRUCTURE
      reserved1   As Long
      pServer     As Long
      AuthInfo    As Long
      reserved2   As Long
   End Type

   Private Type MULTI_QI
      pIID        As Long
      pInterface  As Object
      hResult     As Long
   End Type

   Private Declare Function CLSIDFromProgID Lib "ole32.dll" _
                    (Progid As Any, Clsid As Any) As Long

   Private Declare Function OleInitialize Lib "ole32.dll" _
                    (ByVal Nullptr As Long) As Long

   Private Declare Function CoCreateInstanceEx Lib "ole32.dll" _
                    (Clsid As Any, ByVal pUnkOuter As Long, _
                     ByVal Context As Long, Server As SERVER_STRUCTURE, _
                     ByVal nElems As Long, mqi As MULTI_QI) As Long

   Private Declare Function GetComputerName Lib "kernel32" _
                    Alias "GetComputerNameA" (ByVal lpBuffer As String, _
                                             nSize As Long) As Long

   ' The trick is to call CoCreateInstanceEx to do the dirty work - and get
   ' an iDispatch interface pointer in one step. This is very efficient,
   ' You get the IDISPATCH pointer by passing the 'well-known' REFIID of
   ' IDISPATCH.  Unfortunately, not being able to do this as a constant, 
   ' we can hard-code the REFIID into a little routine.

   Public LastError As String

   Public Function CreateRemoteObject(ObjectName As String, _
                   Optional ServerName As String) As Object

      Dim clsid(256) As Byte
      Dim progid()   As Byte
      Dim server()   As Byte
      Dim QI         As MULTI_QI
      Dim SS         As SERVER_STRUCTURE
      Dim refiid(16) As Byte
      Dim lrc        As Long

      LastError = ""
      
      ' We only need to create the object remotely if the server name is not
      ' the same as our machine (or if it is empty, allowing this function to
      ' act as a general replacement for CreateObject)

      If (Trim$(ServerName) = "") Or (UCase$(ServerName) = UCase$(GetCompName())) Then

         On Error Resume Next

            Err = 0

            Set CreateRemoteObject = CreateObject(ObjectName)

            If Err <> 0 Then
               LastError = Err.Description   'record last error
            End If

         On Error GoTo 0

         Exit Function

      End If

      'otherwise, it is genuinely remote.

      GetIIDforIDispatch refiid()                 'set an IID for IDispatch
      QI.pIID = VarPtr(refiid(0))                 'point to the IID
      progid = ObjectName & Chr$(0)               'specify the object to be launched
      server = ServerName & Chr$(0)               'specify the server
      OleInitialize 0                             'initialise OLE
      lrc = CLSIDFromProgID(progid(0), clsid(0))  'get the CLSID for the object

      If lrc <> 0 Then
         LastError = "Unable to obtain CLSID from progid " & ObjectName & vbCrLf _
            & "Possibly it is not registered on both this server and server " & ServerName
         Exit Function
      End If

      ' point to server name and
      ' invoke a remote instance of the desired object

      SS.pServer = VarPtr(server(0))
      lrc = CoCreateInstanceEx(clsid(0), 0, 16, SS, 1, QI)

      If lrc <> 0 Then
         LastError = "CoCreateInstanceEx failed with error code " & Hex$(lrc)
         Exit Function
      End If

      Set CreateRemoteObject = QI.pInterface      ' pass back object ref.

   End Function


   Public Sub GetIIDforIDispatch(p() As Byte)

      ' fills in the well-known IID for IDispatch into the byte array p.

      p(1) = 4
      p(2) = 2
      p(8) = &HC0
      p(15) = &H46

   End Sub

   Function GetCompName() As String

      ' return the computer name

      Dim buf As String
      Dim rc As Long

      buf = String$(256, 0)
      rc = GetComputerName(buf, Len(buf))

      If InStr(buf, Chr$(0)) > 1 Then
         GetCompName = UCase$(Left$(buf, InStr(buf, Chr$(0)) - 1))
      End If

   End Function

That's it. Just include this module in your project, then create your remote object using this syntax:

 Dim O as MyServer.MyObject  ' NB - you can use early binding !!!
   
   Set O = CreateRemoteObject("MyServer.MyObject","MyServer")

CreateRemoteObject is pretty much a drop-in replacement for CreateObject.

One day, I will look at how to convert the string-format CLID into the binary structure. A clue is embedded in this code: look at the GetIIDforIDispatch function - it that does that. Problem is, it seems to be wrong although it does work. When I get a spare hour with a debugger I'll look into it and nut it out, but for now other deadlines beckon and this works well enough.



Written by: Jim Karabatsos
April '98

Image of Arrow linked to Previous Article Image of Arrow linked to Next Article
Image of Line Break
[HOME] [TABLE OF CONTENTS] [SEARCH]