Title: Streaming COM objects as XML
Question: Quick and dirty example on how to read Type Library information and generate XML files out of a COM object
Answer:
Streaming COM Objects as XML
Introduction
When I was making my first COM objects I used to think that I could give my COM classes the same shape of regular Delphi ones. I am specifically talking about properties and a "stateful life-style" in which you keep using its methods similarily to what you'd do with a DataSet (i.e. Open, Next, Next, Close). Well yes, COM allows you to do that and if the objects lives on the client's computer everything works fast and efficiently. The problem araises if and when you move your object to another machine. In that situation then your application suddenly starts to slow down and your client becomes dependent on the network condition. Each time you access a property you are invoking a method that executes on another machine. If your code makes such calls continuosly you will be in big trouble. The ideal solution would be to redesign such objects making them "stateless". With "stateless object" I mean those whose methods do all they say they will do (i.e. ExecuteMoneyTransfer) and don't depend on other methods (i.e. ExecuteMoneyTransfer doesn't expect a Commit or Rollback method to be called by the client after it is completion). But often the amount of legacy code makes a resedign not practical. Is it possible then to do anything to improve performance and makes stateful objects stateless? As you would expect it is possible (no point in writing this article otherwise ;-) ). By persisting the object's state in some kind of intermediary format (I choose XML) and streaming it in one shot you can achieve the goal. Remember that this is not an optimal solution and the code I am presenting here is not optimized either. If you are starting from scratch you should design stateless objects.
TlbInf32.dll
Included in Visual Studio 6 and Visual Basic 6.0 CDs you can find a very handy DLL called TlbInf32. You can download the documentation for this file on the MSDN webside. If you don't have Visual Studio you can download the DLL from the website Compiled.org
TlbInf32 includes a set of classes that can help you reading type information for both type libaries and COM objects.
The code
The example is very straightforward. I won't spend too much time on it. There's a simple COM library with an object which implements the following interface:
ISimpleObject = interface(IDispatch)
function Name: WideString [propget, dispid $00000001]; safecall;
procedure Name(Value: WideString) [propput, dispid $00000001]; safecall;
function Age: Integer [propget, dispid $00000002]; safecall;
procedure Age(Value: Integer) [propput, dispid $00000002]; safecall;
end;
Then there's a client application that is able to persist and restore the state of it by using an auxiliary class called TTypeInfoStreamer which is defined as:
TTypeInfoStreamer = class
private
fTLI : _TLIApplication;
public
constructor Create;
function GetObjectAsXML(const anObject : IDispatch) : widestring;
procedure SetObjectAsXML(const anObject : IDispatch; aString : widestring);
end;
The two methods GetObjectAsXML and SetObjectAsXMl are what interest us. Please, don't make an example of the code. I put this sample togheter in 10 minutes to answer a question of a guy in a newsgroup. Take a look at the lines I highlighted:
function TTypeInfoStreamer.GetObjectAsXML(const anObject: IDispatch): widestring;
var xml : DOMDocument30;
intfinfo : InterfaceInfo;
root,
node : IXMLDOMNode;
i : integer;
val : OleVariant;
p : PSafeArray;
begin
p := MakeEmptyParmsArray;
try
intfinfo := fTLI.InterfaceInfoFromObject(anObject);
xml := CoDOMDocument30.Create;
xml.async := FALSE;
root := xml.createNode('element', intfinfo.Get_Name, '');
xml.appendChild(root);
with intfinfo do
for i := 1 to (Members.Count) do begin
if not (Members[i].InvokeKind=INVOKE_PROPERTYGET) then Continue;
val := fTLI.InvokeHook(anObject, Members[i].Get_MemberId, INVOKE_PROPERTYGET, p);
node := xml.createNode('element', Members[i].Name, '');
node.text := VarToStr(val);
root.appendChild(node);
end;
finally
result := root.xml;
SafeArrayDestroy(p);
end;
end;
As you can see we created an instance of the TLIApplication object (included in TlbInf32.dll), passed a pointer to the object we want to stream and then looped tough its Members collections. The members collection is the list of methods implemented by the object. What we want to read is the value of the object's properties so we will only stop on the methods that return the value of a property (Members[i].InvokeKind=INVOKE_PROPERTYGET) . In order to invoke the method we need to call the method TLIApplication.InvokeHook which is defined as:
function InvokeHook(const Object_: IDispatch; ID: OleVariant; InvokeKind: InvokeKinds;
var ReverseArgList: PSafeArray): OleVariant; safecall;
It's interesting to note how the ID parameter could be either the name of the method or its DispID. So, in case you have the DispID already, you wouldn't need to use late bound calls (which first invoke the IDispatch.GetIDOfNames method slowing things down a *lot*). InvokeKind tells the TLIApplication *how* to invoke it and finally the ReverseArgList is a safe array that in our case only contains no values. See the rest of the code to find out how I build one. The result is something like this:
Alessandro Federici
25
Et voila'! We have our COM object persisted into XML! Now we need to set back these values. See the code below.
procedure TTypeInfoStreamer.SetObjectAsXML(const anObject: IDispatch;
aString: widestring);
var xml : DOMDocument30;
intfinfo : InterfaceInfo;
root,
node : IXMLDOMNode;
i : integer;
val : OleVariant;
p : PSafeArray;
s : string;
begin
p := MakeOneElementArray;
try
intfinfo := fTLI.InterfaceInfoFromObject(anObject);
xml := CoDOMDocument30.Create;
xml.async := FALSE;
xml.loadXML(aString);
root := xml.documentElement;
with root do
for i := 0 to (childNodes.length-1) do begin
s := childNodes[i].nodeName;
SetOneElementArray(p, childNodes[i].Text);
fTLI.InvokeHook(anObject, s, INVOKE_PROPERTYPUT, p);
end;
finally
SafeArrayDestroy(p);
end;
end;
As you can see we did the exact opposite of what the had done before except that in this case we invoked the method as a property writer. I hope this will demistify a little how to read COM type information and stream its contents in an arbitraty format. The TTypeInfoStreamer class is far from being a complete class but feel free to use the code as a start. Happy coding!
Alessandro Federici
System Architect
Borland Certified Consultant
-----
http://www.msdelphi.com (home of the DSOAP Toolkit 2.0)
alef@msdelphi.com (primary)
alef@bigfoot.com (routes to the first)
kingalef@hotmail.com (if you have troubles with the others)