RO34 - I Cannot ROmember

 

RO34 - I Cannot ROmember
作者 Brian Moelk (www.brainendeavor.com)
RemObjects 提示 : 我们相信本文是正确的 , 但我们不做任何保证 . 在此感谢 Henrick 写的文章 , 很高兴在此发表 .
介绍
RemObjects 是功能强大可扩展的远程框架 ; 但是当考虑远程对象的 allocation(内存 分配 )/deallocation(内存释放)/serialization( 序列化 ) 问题时让人摸不到边际 . 本文将讨论 RO 内核澄清这些问题 .
Delphi 开发者可以很幸运的使用 RemObjects/DataAbstract 创建 n 层服务 . 使用 RO, 我们通过在 Service Builder 工具重创建服务定义库以导向的架构设计我们服务 .
其中之一就是我们可以自定义结构体类型作为输入输出参数 . RO 使用其定义创建代码所以可以序列化他们并在网络上传输 . 这能够轻松做到 ,RO 最大程度的提升了我们的 n 层应用 .
不幸的是还有一些细节需要我们自己管理 ; . 清除我们自己分配的内存 . Delphi. RemObjects 尽力做到像本地调用一样实现远程调用 , 然而还是有些不同 , 稍候会讨论 . 这里有一条首要的规则 : 总在客户端释放内存不能在服务端释放 .
为了清晰 , 我们讨论的代码都是我们写过的 ;RO 在服务端会为我们自动释放对象 , 所以不会引起内存泄漏 .
已经有关于这个问答的 FAQ 在线问答了 . 但是我想在这里讨论更多的细节 , 因为有很多细节使我们必须熟悉理解的规则 , 我们还有特别小心的使用 in/out ( var) 参数 .
OK, 第一件事情是说明所有从 TROComplexType 继承来的 RO 结构体 . TROComplexType 可以将对象数据序列化在网上传输 ; 例如 RO 可以序列化从 TROComplexType 继承的对象 .
Use The Source, always use The Source.
查看代码发生了什么最好的方法是调用服务方法并跟踪 RO 结构体或 TROComplexType 对象的内存申请和释放 .
我已经建立了一个简单的项目 project 帮助我们查看这个过程 .
这里是我们要测试执行的服务端接口和方法 :
{ 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;
ITestStructsService 接口结合了所有要使用的参数类型 : in, out, 返回值和 in/out.
为了简单我们只考虑 SOAP 消息 , TROSOAPMessage, 当然对 TROBinMessage 也应用同样的规则 . 虽然我们只考虑结构体 , 但是执行规则同样适用于从 TROComplexType 继承来的 RO 数组 .
通过客户端代码跟踪 ...
Ok, 第一个要查看的方式是 ProcessStruct 方法 . 我们再客户端直接创建结构体填充数据并作为参数调用后释放 :
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 ;
客户端代码与我们在 Delphi 中调用需要常量参数的方法一样 . 客户端负责管理传输作为参数的对象生存期 .
RO 已经做了所有的繁琐工作 ; 我们看看实际发生了什么 . ROmemberLibrary_Intf.pas 文件中我们可以看到 TTestStructsService_Proxy 代理类为每个接口方法序列化结构体以便在网络传输 .
代理类代码中我们对调用 __Message.Read __Message.Write 的部分感兴趣 . 我们可以看到消息的序列号和反序列化 . 它们负责为调用格式化消息 .
我们看看 TROMessage.Read 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 ;
这里使用 TROMessage Serializer 属性 . 方法 :
function CreateSerializer : TROSerializer; virtual; abstract;
生成消息相对于的 TROSerializer.TROSerializer 实例化后在 TROMessage 构造时将其赋给 TROMessage.fSerializer 成员 . TROSOAPMessage 中生成 TROXMLSerializer.
function TROSOAPMessage.CreateSerializer : TROSerializer;
begin
  result := TROXMLSerializer.Create(pointer(fMessageNode));
end ;
Ok, 现在我们已经看到 TROMessage TROSerializer 直接的关系了 , 现在再考虑特定的 ProcessStruct 案例 . TROSerializer.Write 约定 MyStruct TypeInfo 信息指定了他的 tkClass 并以此来调用 TROSerializer.WriteObject.
WriteObject 是一个复杂的方法这里不再详述 , 但是它使用了 RTTI 读取 MyStruct 中的值并写出来 . 注意这里没有创建和释放代码 ,TROXMLSerializer.BeginWriteObject 中也没有创建和释放代码 . 所以客户端简单的序列化了我们传入的常量结构体参数 . 我们负责分配并做相应的释放 .
在服务端代码中跟踪 ...
服务端发生了什么 ? 服务端通过 ROmemberLibrary_Invk.pas 文件中的 TTestStructsService_Invoker.Invoke_ProcessStruct 方法处理客户端请求 .
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 ;
我们可以看到结构体从流中读出 , 因此读取也使用了序列化器 . 我们可以在 try..finally 块中发现 TROObjectDisposer; 这个逻辑接口释放了结构体引用的对象 . 猜测是正确的 . 这意味着什么 ?
这说明 __Message.Read 序列化器实例化 MyStruct. 序列化器的 Read 方法测定 MyStruct 实例是一个对象类型 , 所以调用了 ReadObject. 随后调用 BeginReadObject. TROXMLSerializer.BeginReadObject 中执行如下构造函数 :
procedure TROXMLSerializer.BeginReadObject
[...]
    anObject := TROComplexTypeClass(aClass).Create;
[...]
end ;
这里实例被构造出来 , 所以我们需要 TROObjectDisposer. RO 架构实例化了这个结构体让后又会为你在服务端释放它 .
所以我们看这个方法就已经知道 RO 实例化结构体的所有信息 , 使用这个实例后又为我们自动释放 . 序列化器是关键 . 当从流中读取了结构体后序列化器创建一个实例 ; 如果要向流中写入就使用已存在的实例 . 这有很大的意义 .

OutStruct
GetStruct
下一个方法我们看看 OutStruct. 我们已经认识了 RO 的序列化器 , 下面我们看看生成的代码 TTestStructsService_Proxy.OutStruct, 它明确了我们的职责 :
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 ;
这里序列化器要求读结构体 ; 这意味着它将要创建一个实例 . 注意它不会释放这个实例 , 所以这个客户端的责任 .
GetStruct 很像 OutStruct 方法 , 但是使用 aStruct 的方式不同 , 它用于返回 . 如果我们查看生成的 TTestStructsService_Proxy.GetStruct 代码可以发现 .

留心结构体做 var 参数 !!!
最后看 VarStruct 方法 . 这是非常重要的部分 , 我们看生成的代码 TTestStructsService_Proxy.VarStruct:
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 ;
代码使用 uses __Message.Write __Message.Read, 这以为着它使用 aStruct 参数传入的实例 , 同时也会创建一个新的实例 aStruct . 客户端如何写代码就很重要了 . 正确的书写客户端代码保证没有内存泄露的方式如下 :
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 ;
我们必须释放 aInStruct aOutStruct . 为给代理传入对象我们必须实例化 aInStruct. 代理将之写入流中并做调用 . 代理代码然后实例化一个新的 MyStruct, 重写 var 参数的引用 .

这意味着客户端必要跟踪什么传入了代理 , 什么又从代理中传出 , 并将两者都释放掉 .

为什么 RO 做这些呢 ? 想象一下如果 RO 为我们释放其中的一个 . 问题是我们可能还要在其他的地方使用这些对象的实例 . 最后 , 远程调用与我将在下一小节中介绍的局部调用有明显的不同 .

在我看来 , 理想的解决方案是使用接口应用计数器来管理生存期 . 所以我们使用 IROComplexType 替换 TROComplexType, 我们所有的结构体都基于接口 . 问题是 RO 为了支持没有 RTTI 接口支持的 delphi5. 这样无法使用 RTTI 序列化它们 ; 然而我们可以通过其它方法绕过 RTTI.

如果我们在服务端查看调用 VarStruct 的代码我们可以看到如下内容 :
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 ;
 
我们可以看到如何通过 TROObjectDisposer 释放 __in_aStruct the aStruct 实例 . 这正是我们要做客户端保证没有内存泄露的做法 .
抛开 Delphi 话题展示本地和远程调用的区别 ...
要真正的理解相对于本地调用的 RO 调用以及为什么必须释放两个结构体实例 , 我们需要看看一些使用 Var 参数的本地调用 . 如果你对我们在 RO Var 参数调用时为什么必须写那些代码的解释很满意 , 你可以越过本小节 . 如果你想去对比远程和本地调用请继续阅读 .
为什么你要在 RO 中使用 in/out var 参数呢 ? 这是我们向服务端传递结构体和从服务端得到更新修改的结构体的唯一方式 . 本地调用有更多弹性达到这个目标 , 我们将看到每一种选择 . 这对于在 RO 中不能传递非常量结构体参数的情况没有什么参考价值 .
如果要在本地实现 ITestStructsService.VarStruct 的功能 , 我将考虑使用结构体替代类 ( 记住 RO 中的结构体是从 TROComplexType 类继承下来的 ). 这是因为在本地调用时结构体与 RO 结构体提供了同样等级的功能 . 因而我们使用如下方法测试 :
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 ;
在本例中 , 由于使用了分配在客户端方法 ( TClientForm.ButtonLocalStackRecordClick) 栈上的本地记录结构体 ,Delphi 为我们处理所有的内存分配和释放 . 这里 ChangeTheRecord 方法的参数必须使用 var 否则过程就不能更新结构体 . 但是我们可以通过不同的方式使用结构体 ; 加入我们在堆上分配结构体 :
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 ;
三个方法都正常执行 : ChangeTheRecord, ChangeTheHeapRecord ChangeTheHeapRecordVar . 我们传递堆上的引用就可以改变值 ( 不管是否在参数前放置 var 标识 ), ChangeTheHeapRecord ChangeTheHeapRecordVar 实现了同等功能 . 或者我们可以给 ChangeTheRecord 传递忽略 var 标识的结构参数 , 而执行效果却和基于栈使用 var 时的相同 .
唯一不同的地方就是我们必须要在堆上分配和释放内存 . 但是注意我们只做一次分配和释放操作 .
更完整的 , 我们看在本地传递 MyStruct 对象的代码是什么样的 :
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 ;
又一次清楚的看到我们只做一次分配和释放结构体的操作 . 因为我们在本地调用并传递的是对象的引用 , 方法改变参数将直接修改对象本身 , 因此要达到我们目的 var 标识不是必须的 .
在对结构体做 var 标识的情况下我们可以传递 nil 或一个分配内存的实例 . 更多时候在方法中使用 var 对象引用参数的意图是作为 "Source" 方法 . 这时如果将 nil 值传递给方法 , 将为调用者分配一个新实例 . ChangeTheStructVar . 但是注意及时是这种情况我们以只释放一次实例 .
当然我们分配和释放对象的方法和原因完全依赖于我们调用的方法 / 过程 . 这里特别约定了我们将在方法中直接改变调用者提供的结构 / 对象 . 也可以将修改结果返回调用者 .
这里有多种约定 , ChangeTheStructVar "source" 方法不同 .RO var 参数的约定也有不同意义 . 它可以使用如下代码模仿本地调用 :
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 ;
正如你在代码中看到的 , 结构比必须在 TClientForm.ButtonLocalObjectLikeROClick 方法中释放两次 . 这时因为 ChangeTheStructVarLikeRO 执行的约定很像 RO 执行 var 参数的远程调用约定 .
结论
RO 是令人惊奇的架构 , 但是使用它是必须要知道一些告诫 . 为了不会写出内存泄漏的代码 , 理解其中隐含的内容是很重要的 . 我希望本文可以起到引导作用 , 并且学到我写作时学到的东西 .

你可能感兴趣的:(object,Integer,Delphi,attributes,Allocation,construction)