Creating Remote DCOM Objects on Specific Servers
by Jim Karabatsos - GUI Computing
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.