Remote Object Allocation and Serialization (Delphi)

by Brian Moelk (http://www.brainendeavor.com)

Note from RemObjects: although we believe the content of this article to be correct, we do not warrant that it is so. We thank Brian very much for producing this, which we are pleased to publish here.

Introduction

Remoting SDK is a powerful and extensible remoting framework; but dementia sets in when I think about remote object allocation/deallocation/serialization. This article attempts to provide some clarity by discussing the internals of RO.

Delphi developers are fortunate that we can choose Remoting SDK/DataAbstract to build n-tier application servers. Using RO, we design our Service Oriented Architecture by creating a Library of service definitions in a tool called the Service Builder.

One of the things that we can define is a Struct type to be used for input/output parameters. RO uses this definition to generate code so it can stream these guys across the wire. This is very easy to use and RO does most of the heavy lifting for our n-tier apps.

Unfortunately there is a bit of housekeeping we must do; i.e. clean up the memory we've allocated. Just like Delphi. Remoting SDK endeavors to make remote calls look like local calls and to a large degree it does, however there are some differences which we'll discuss a bit later. Here is the rule of thumb: Always free the memory on the client and never free it on the server.

Just to be clear, I am talking about the code that we have to write. RO takes care of freeing objects created on the server for us so there will not be any memory leaks from those guys escaping our clutches.

I want to cover it in more detail here, since there are details we must be aware of to really understand the rule of thumb and we must be particularly cautious with in/out (aka var) parameters.

OK, the first thing to notice about all RO Structs is that they descend from TROComplexType. TROComplexType enables the TROSerializer to move the object's data across the wire; i.e. RO can only serialize objects that descend from TROComplexType.

Use The Source, always use The Source

The best way to see what happens is to follow along in the code so let's take a look at some service calls and track the memory allocation/deallocation of RO's Structs or TROComplexTypes.

I've built a sample project to help us look at the RO framework through this process.

Here is the Service Interface and methods we will use to examine the execution path:

{ MyStruct }
  MyStruct = class(TROComplexType)
  private
    fStructId: Integer;
    fStructData: String;
  public
    procedure Assign(iSource: TPersistent); override;
  published
    property StructId: Integer read fStructId write fStructId;
    property StructData: String read fStructData write fStructData;
  end;

  [...]

  { ITestStructsService }
  ITestStructsService = interface
    ['{7FD55CCE-01E8-4F4A-B64B-02380B31A8FC}']
    procedure ProcessStruct(const aStruct: MyStruct);
    procedure OutStruct(out aStruct: MyStruct);
    function GetStruct: MyStruct;
    procedure VarStruct(var aStruct: MyStruct);
  end;

The ITestStructsService interface has all the combinations you can have for parameter passing: in, out, result and in/out.

For simplicity, we'll only consider the SOAP message, TROSOAPMessage, however the same principles apply to TROBinMessage. Also for simplicity, we will only consider Structs, however the principles should apply to RO Arrays since they also descend from TROComplexType.

Tracing through the client code...

Ok, the first method we'll look at is the ProcessStruct method. Our client side implementation is straightforward. We construct the Struct, fill it with data, make the call and free it:

procedure TClientForm.ButtonProcessStructClick(Sender: TObject);
var
  aStruct: MyStruct;
begin
  aStruct := MyStruct.Create;
  try
    aStruct.StructId := 0;
    aStruct.StructData := 'Client ProcessStruct';
    MemoOutput.Lines.Add('invoke ProcessStruct');
    OutputStruct(aStruct);
    (RORemoteService as ITestStructsService).ProcessStruct(aStruct);
    OutputStruct(aStruct);
  finally
    FreeAndNil(aStruct);
  end;
end;

The client implementation that we need to write is identical to invoking a normal Delphi method that requires a const object of some kind. The client is responsible for the lifetime of the object passed as a parameter.

RO does all the nitty gritty work here; let's look at what it actually does. In the ROmemberLibrary_Intf.pas we see that RO generates a proxy class TTestStructsService_Proxy that serializes the Struct across the wire for each interface method.

The parts of code in the proxy that we're interested in are the __Message.Read and __Message.Write invocations. We can see that the serialization and deserialization of Structs happens in the Message. This makes sense since the Message is responsible for the format of the invocations.

Let's look at TROMessage.Read and TROMessage.Write:

procedure TROMessage.Read(const aName: string;
  aTypeInfo: PTypeInfo;
  var Ptr; Attributes: TParamAttributes);
begin
  Serializer.Read(aName, aTypeInfo, Ptr);
  if Assigned(fOnReadMessageParameter) then
    fOnReadMessageParameter(Self, aName,
      aTypeInfo, pointer(Ptr),
      Attributes);
end;

procedure TROMessage.Write(const aName: string;
  aTypeInfo: PTypeInfo;
  const Ptr; Attributes: TParamAttributes);
begin
  if Assigned(fOnWriteMessageParameter) then
    fOnWriteMessageParameter(Self, aName,
      aTypeInfo, pointer(Ptr),
      Attributes);
  Serializer.Write(aName, aTypeInfo, Ptr);
end;

These guys delegate to the "Serializer" of the TROMessage. The method:

function CreateSerializer : TROSerializer; virtual; abstract;

creates the message's corresponding TROSerializer. The TROSerializer instance is created and the "fSerializer" member is assigned to that instance in the construction of TROMessage. In the case of TROSOAPMessage, it creates a TROXMLSerializer.

function TROSOAPMessage.CreateSerializer : TROSerializer;
begin
  result := TROXMLSerializer.Create(pointer(fMessageNode));
end;

Ok, now that we've seen the relationship between the TROMessage and TROSerializer, let's consider the specific case of ProcessStruct once again. TROSerializer.Write determines that MyStruct's typeinfo indicates it's a tkClass, it therefore calls TROSerializer.WriteObject.

WriteObject is a complex method that we won't cover in detail here, but it essentially uses RTTI to read the values of MyStruct and writes them out. If you do look, notice there is no .Create or .Free code here. Be sure to look at TROXMLSerializer.BeginWriteObject as well to verify there is no .Create or .Free code. Therefore the serialization on the client of const Structs will simply use the instance that we pass into it. So we allocate it and deallocate it accordingly.

Tracing through the server code...

What happens on the Server? The server processes the request from the client by using TTestStructsService_Invoker.Invoke_ProcessStruct as found in the ROmemberLibrary_Invk.pas file.

procedure TTestStructsService_Invoker.Invoke_ProcessStruct
  (const __Instance:IInterface;
   const __Message:IROMessage;
   const __Transport:IROTransport;
   out __oResponseOptions:TROResponseOptions);
{ procedure ProcessStruct(const aStruct: MyStruct); }
var
  aStruct: ROmemberLibrary_Intf.MyStruct;
  __lObjectDisposer: TROObjectDisposer;
begin
  aStruct := nil;
  try
    __Message.Read('aStruct',
      TypeInfo(ROmemberLibrary_Intf.MyStruct),
      aStruct, []);

    (__Instance as ITestStructsService).ProcessStruct(aStruct);

    __Message.Initialize(__Transport, 'ROmemberLibrary',
      'TestStructsService', 'ProcessStructResponse');
    __Message.Finalize;

    __oResponseOptions := [roNoResponse];

  finally
    __lObjectDisposer := TROObjectDisposer.Create(__Instance);
    try
      __lObjectDisposer.Add(aStruct);
    finally
      __lObjectDisposer.Free();
    end;
  end;
end;

We can clearly see that the Struct is read from the message, and hence read using the serializer. We can also see that there is a TROObjectDisposer being used in a try..finally statement; the logical inference is that this frees the object referenced by aStruct. That guess is correct. So what does this mean?

It means that __Message.Read uses the serializer to instantiate an instance of MyStruct. The Serializer's Read method determines that the type of MyStruct is an object type, so it invokes ReadObject. This subsequently invokes BeginReadObject. In the TROXMLSerializer, BeginReadObject will execute the following virtual constructor:

procedure TROXMLSerializer.BeginReadObject
[...]
    anObject := TROComplexTypeClass(aClass).Create;
[...]
end;

This is where the instance is constructed, therefore this is why the TROObjectDisposer is required. The RO framework instantiates the Struct and then cleans it up for you on the server.

So, by looking at this one method, we actually have all the information we need to know about when RO instantiates Structs, when the existing instance is used and when they are destroyed automatically for us. The key is in the serializer. Whenever a Struct is read from a stream, the serializer creates an instance; if it is written to the stream it merely uses the existing instance. This makes a lot of sense.

OutStruct and GetStruct

So the next method to look at is OutStruct. Knowing what we know about RO serializers, when we look at the generated code TTestStructsService_Proxy.OutStruct, it is clear what our responsibility actually is:

procedure TTestStructsService_Proxy.OutStruct(out aStruct: MyStruct);
var
  __request, __response : TMemoryStream;
begin
  aStruct := nil;
  __request := TMemoryStream.Create;
  __response := TMemoryStream.Create;

  try
     __Message.Initialize(__TransportChannel,
        'ROmemberLibrary', __InterfaceName, 'OutStruct');
     __Message.Finalize;

     __Message.WriteToStream(__request);
     __TransportChannel.Dispatch(__request, __response);
     __Message.ReadFromStream(__response);

     __Message.Read('aStruct', TypeInfo(ROmemberLibrary_Intf.MyStruct),
         aStruct, []);
  finally
    __request.Free;
    __response.Free;
  end
end;

Here the serializer is asked to read the Struct; which means it will create an instance. Notice that it doesn't free that instance. Therefore it is the client's responsibility to do so.

GetStruct is exactly the same as OutStruct except that instead of using aStruct, it uses Result. If we look at the code generated in TTestStructsService_Proxy.GetStruct we can see this.

Be Careful with var parameters that are Structs!!!

The final method to look at is: VarStruct. This is very important so let's look at the TTestStructsService_Proxy.VarStruct code:

procedure TTestStructsService_Proxy.VarStruct(var aStruct: MyStruct);
var
  __request, __response : TMemoryStream;
begin
  __request := TMemoryStream.Create;
  __response := TMemoryStream.Create;

  try
     __Message.Initialize(__TransportChannel, 'ROmemberLibrary',
         __InterfaceName, 'VarStruct');
     __Message.Write('aStruct', TypeInfo(ROmemberLibrary_Intf.MyStruct),
         aStruct, []);
     __Message.Finalize;

     __Message.WriteToStream(__request);
     __TransportChannel.Dispatch(__request, __response);
     __Message.ReadFromStream(__response);

     __Message.Read('aStruct', TypeInfo(ROmemberLibrary_Intf.MyStruct),
        aStruct, []);
  finally
    __request.Free;
    __response.Free;
  end
end;

This code uses __Message.Write and __Message.Read, which means it uses the instance passed into it via the aStruct parameter and instantiates a new instance of aStruct. This is important because of what the client code looks like. The correct way to write the client code so that it does not leak any memory looks like this:

procedure TClientForm.ButtonVarStructClick(Sender: TObject);
var
  aInStruct, aOutStruct: MyStruct;
begin
  aInStruct := MyStruct.Create;
  try
    aInStruct.StructId := 2;
    aInStruct.StructData := 'Client VarStruct';
    MemoOutput.Lines.Add('invoke VarStruct');
    OutputStruct(aInStruct);
    aOutStruct := aInStruct;
    (RORemoteService as ITestStructsService).VarStruct(aOutStruct);
    OutputStruct(aOutStruct);
  finally
    FreeAndNil(aOutStruct);
    FreeAndNil(aInStruct);
  end;
end;

We must Free both the aInStruct and the aOutStruct. We must instantiate the aInStruct so we have something to pass into the proxy. The proxy code writes this to the stream to make the call. The proxy code then instantiates a new MyStruct and overwrites the var parameter reference.

This means that the client must keep track of what is passed into the proxy and what comes out of the proxy and Free both of those objects.

Why did RO do this? Consider if RO free'd one of these guys for us. The problem here is that we may still be using that reference in other parts of our code. But ultimately, here is where remote calls are typically different than local calls which I attempt to explain in gory detail in the next section.

In my opinion, the ideal solution would be to use interface reference counting to manage the lifetimes. So we would have IROComplexType instead of TROComplexType and all our Structs would be interface based. The problem is that RO v2 supported Delphi 5, so there is no RTTI on interfaces. This means that streaming them in and out using RTTI is a no-go; however there are other ways of doing this without utilizing RTTI.

If we look at the server code to invoke VarStruct we will see the same sort of thing going on:

procedure TTestStructsService_Invoker.Invoke_VarStruct
  (const __Instance:IInterface;
   const __Message:IROMessage;
   const __Transport:IROTransport;
   out __oResponseOptions:TROResponseOptions);
{ procedure VarStruct(var aStruct: MyStruct); }
var
  aStruct: ROmemberLibrary_Intf.MyStruct;
  __in_aStruct: ROmemberLibrary_Intf.MyStruct;
  __lObjectDisposer: TROObjectDisposer;
begin
  aStruct := nil;
  __in_aStruct := nil;
  try
    __Message.Read('aStruct', TypeInfo(ROmemberLibrary_Intf.MyStruct),
        aStruct, []);
    __in_aStruct := aStruct;

    (__Instance as ITestStructsService).VarStruct(aStruct);

    __Message.Initialize(__Transport, 'ROmemberLibrary',
       'TestStructsService', 'VarStructResponse');
    __Message.Write('aStruct', TypeInfo(ROmemberLibrary_Intf.MyStruct),
        aStruct, []);
    __Message.Finalize;

  finally
    __lObjectDisposer := TROObjectDisposer.Create(__Instance);
    try
      __lObjectDisposer.Add(__in_aStruct);
      __lObjectDisposer.Add(aStruct);
    finally
      __lObjectDisposer.Free();
    end;
  end;
end;

We can see that it disposes of the __in_aStruct and the aStruct instances via the TROObjectDisposer. This is exactly what we must do on the client to ensure we don't have any memory leaks.

Digressing into Regular Delphi stuff to show the differences between local and remote calls...

To really understand how RO calls are different from local calls and why we must free two Struct instances, we need to look at some local calls using var parameters. Feel free to skip this section if you are sufficiently satisfied as to why we must write that code with var parameters in RO. If you want to contrast remote with local calls...keep reading.

Why do we use in/out or var parameters with RO? This is the only way we can pass Struct data into the server and get updated/modified Struct data out of the server. Local calls have much more flexibility to accomplish this goal so we will look at each of our options in turn. It is worth noting that you cannot pass a non-const Struct param in RO.

If I wanted to accomplish what ITestStructsService.VarStruct actually does locally, I would consider coding this using a Record type rather than a Class type (remember that RO Structs are really objects derived from TROComplexType). I would consider using Records, because they provide the same level of functionality as RO Structs do when doing local invocations. So let's examine what using Records would be like:

type
  PMyRecord = ^TMyRecord;
  TMyRecord = record
    RecId: integer;
    RecData: shortstring;
  end;

[...]
procedure ChangeTheRecord(var aRec: TMyRecord);
begin
  aRec.RecId := aRec.RecId + 1;
  aRec.RecData := aRec.RecData + ' Changed';
end;

[...]
procedure TClientForm.ButtonLocalStackRecordClick(Sender: TObject);
var
  LocalRecord: TMyRecord;
begin
  LocalRecord.RecId := 1;
  LocalRecord.RecData := 'Foo';
  OutputRecord('before stack record', LocalRecord);
  ChangeTheRecord(LocalRecord);

  //value of LocalRecord is: Id = 2 and Data = 'Foo Changed'
  OutputRecord('after stack record', LocalRecord);
  OutputSeparator;
end;

In this case, Delphi handles all the allocation/deallocation for us since the Record instance, LocalRecord, is allocated on the stack of the client method (i.e. TClientForm.ButtonLocalStackRecordClick). Here the parameter of ChangeTheRecord must be var otherwise the procedure won't update the record. But let's try a different method using records; suppose we put the Record on the heap:

procedure ChangeTheHeapRecord(aRec: PMyRecord);
begin
  aRec.RecId := aRec.RecId + 1;
  aRec.RecData := aRec.RecData + ' Changed';
end;

procedure ChangeTheHeapRecordVar(var aRec: PMyRecord);
begin
  aRec.RecId := aRec.RecId + 1;
  aRec.RecData := aRec.RecData + ' Changed';
end;

[...]
procedure TClientForm.ButtonLocalHeapRecordClick(Sender: TObject);
var
  HeapRecord: PMyRecord;
begin
  GetMem(HeapRecord, SizeOf(TMyRecord));
  try
    HeapRecord.RecId := 1;
    HeapRecord.RecData := 'Foo';

    OutputRecord('before heap record', HeapRecord^);
    //all three do the same thing...so use any one of them..
    //ChangeTheRecord(HeapRecord^);
    ChangeTheHeapRecord(HeapRecord);
    //ChangeTheHeapRecordVar(HeapRecord);

    //value of HeapRecord is: Id = 2 and Data = 'Foo Changed'
    OutputRecord('after heap record', HeapRecord^);
  finally
    FreeMem(HeapRecord);
  end;
  OutputSeparator;
end;

All three options work: ChangeTheRecord, ChangeTheHeapRecord and ChangeTheHeapRecordVar. We can pass the heap reference and change the values there (regardless of whether or not the param is a var parameter) as we do in ChangeTheHeapRecord and ChangeTheHeapRecordVar. Or we can pass it as a dereferenced var Struct to ChangeTheRecord, exactly as we did with the stack based record.

The only difference here is that we have to allocate and deallocate the memory for the record on the heap. But notice that there is only one allocation/deallocation that we write.

To be absolutely thorough, let's look at what the code of passing the MyStruct object is like if we did it locally instead of remotely:

procedure ChangeTheStruct(aStruct: MyStruct);
begin
  aStruct.StructId := aStruct.StructId + 1;
  aStruct.StructData := aStruct.StructData + ' Changed';
end;

procedure ChangeTheStructVar(var aStruct: MyStruct);
begin
  if (Assigned(aStruct) = False) then
  begin
    aStruct := MyStruct.Create;
  end;

  aStruct.StructId := aStruct.StructId + 1;
  aStruct.StructData := aStruct.StructData + ' Changed';
end;

[...]
procedure TClientForm.ButtonLocalObjectClick(Sender: TObject);
var
  aStruct: MyStruct;
begin
  aStruct := MyStruct.Create;
  try
    aStruct.StructId := 1;
    aStruct.StructData := 'Foo';

    OutputStruct('invoke Local Object', aStruct);
    ChangeTheStruct(aStruct);
    OutputStruct('after Local Object', aStruct);
  finally
    FreeAndNil(aStruct);
  end;
  OutputSeparator;
end;

procedure TClientForm.ButtonLocalObjectVarClick(Sender: TObject);
var
  aStruct: MyStruct;
begin
  aStruct := nil;
  try
//OR replace above two lines with:
  //aStruct := MyStruct.Create;
  //try
  //  aStruct.StructId := 1;
  //  aStruct.StructData := 'Foo';

    OutputStruct('before Local Object var', aStruct);
    ChangeTheStructVar(aStruct);
    OutputStruct('after Local Object var', aStruct);
  finally
    FreeAndNil(aStruct);
  end;
  OutputSeparator;
end;

Again, it's clear to see that we only allocate the Struct once and deallocate the same Struct only once. Because the invocation is local and it is passing an object reference, the method's changes will take place on the object itself, therefore the var param is not necessary to accomplish our goal.

In the case of a var param for that Struct we can either pass in nil or an allocated Struct. In most cases, the intent of a method with an object reference var parameter is to act like a Source method. That is, if a nil value is passed into the method, it will allocate an instance of that object for the caller. This is exactly what is done in ChangeTheStructVar. But note that even in this case, we only deallocate one instance.

Certainly, the way and reasons we allocate/deallocate objects are completely dependent on the contract of the methods/procedures we invoke. The specific contract here is that they will change the record/object in the memory location that we provide them. This is so we can get the changed values back to the caller.

There is a variation of the contract though, the ChangeTheStructVar is different in that it acts as a "source". The contract that the RO var parameter imposes is different still. It can be simulated as a local invocation by the following code:

procedure ChangeTheStructVarLikeRO(var aStruct: MyStruct);
var
  InStruct: MyStruct;
begin
  InStruct := aStruct;

  //this simulates the __Message.Read construction on the client
    //after the server method is invoked
  aStruct := MyStruct.Create;
  //this simulates the __Message.Read deserialization
  aStruct.Assign(InStruct);

  aStruct.StructId := aStruct.StructId + 1;
  aStruct.StructData := aStruct.StructData + ' Changed';
end;

[...]
procedure TClientForm.ButtonLocalObjectLikeROClick(Sender: TObject);
var
  aInStruct, aOutStruct: MyStruct;
begin
  aInStruct := MyStruct.Create;
  try
    aInStruct.StructId := 1;
    aInStruct.StructData := 'Foo';

    OutputStruct('before Local Object var like RO', aInStruct);
    aOutStruct := aInStruct;

    ChangeTheStructVarLikeRO(aOutStruct);

    OutputStruct('after Local Object var like RO (aInStruct)',
                 aInStruct);
    OutputStruct('after Local Object var like RO (aOutStruct)',
                 aOutStruct);
  finally
    FreeAndNil(aInStruct);
    FreeAndNil(aOutStruct);
  end;
  OutputSeparator;
end;

As you can see in this code, the Struct must be deallocated twice in TClientForm.ButtonLocalObjectLikeROClick. This is because the contract that ChangeTheStructVarLikeRO implements is very much like the contract that RO implements for remote Struct var parameters.

Conclusion

RO is an amazing framework but there are some caveats to be aware of when using it. It is important to understand what is covered here so that we do not write code that has memory leaks. I hope that this article has shed some light and that you have learned as much reading it as I have learned by writing it.