2004. június 1., kedd

Streaming COM Objects as XML


Problem/Question/Abstract:

Streaming COM Objects as XML

Answer:

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. Now, download my example by clicking here.

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:

<ISimpleObject>
  <Name>Alessandro Federici</Name>
  <Age>25</Age>
</ISimpleObject>

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!

Nincsenek megjegyzések:

Megjegyzés küldése