在 Delphi 中的多个应用程序之间共享一个对象

Posted

技术标签:

【中文标题】在 Delphi 中的多个应用程序之间共享一个对象【英文标题】:sharing an object between multiple applications in Delphi 【发布时间】:2016-12-09 05:55:37 【问题描述】:

我正在尝试弄清楚如何。我知道这样做的方法是通过 IPC/windows 共享内存调用(即 CreateFileMapping 等),但是在所有示例代码中,我发现它们使用像字符串这样的简单类型,而我需要共享一个对象。

我想知道这是否可能,因为我一直在使用一个只共享对对象的指针引用而不是对象内存本身的应用程序。当我尝试从我的其他应用程序检索然后访问该对象时,我遇到了访问冲突。我认为这是因为指针指向其他应用程序的受保护内存。

这是我迄今为止尝试过的代码(如您所见,我试图在多个应用程序之间共享一个 TADOConnection 对象,以便在应用程序之间只使用/共享一个数据库连接)。如果有更好/更简单的方法来做到这一点(共享 ADO 连接),我很想知道如何去做。

  TSharedData = record
    Connection: TAdoConnection;
  end;

  PSharedData = ^TSharedData;

var
  SharedData: PSharedData;
  hFileMapping: THandle;  
  Form1: TForm1;

implementation

$R *.dfm

function CreateNamedFileMapping(const Name: String): THandle;
begin
  Result := CreateFileMapping(INVALID_HANDLE_VALUE, nil, PAGE_READWRITE, 0,
    SizeOf(TSharedData)*8, PChar(Name));

  if Result > 0 then
    SharedData := MapViewOfFile(Result, FILE_MAP_ALL_ACCESS, 0, 0, 0);
end;

function GetSharedData: PSharedData;
begin
  result := nil;
  hFileMapping := OpenFileMapping(FILE_MAP_ALL_ACCESS, False, 'MySharedMemory');
  if (hFileMapping > 0) then
    Result := MapViewOfFile(hFileMapping, FILE_MAP_ALL_ACCESS, 0, 0, 0);
end;

procedure TForm1.createClick(Sender: TObject);
begin
   hFileMapping := CreateNamedFileMapping('MySharedMemory');
   if (hFileMapping > 0) and Assigned(SharedData) then
   begin
     SharedData^.Connection := TAdoConnection.Create(nil);
     // can't use Assign as it is not supported by _Connection
     SharedData^.Connection.ConnectionObject := AdoConnection1.ConnectionObject;
   end;
end;

procedure TForm1.retrieveClick(Sender: TObject);
begin
   SharedData := GetSharedData;
  if assigned(SharedData) then
    // should be set to true if everything was ok
    ShowMessage(BoolToStr( SharedData.Connection.Connected, true));
end;

procedure TForm1.FormDestroy(Sender: TObject);
begin
   AdoConnection1.Connected := False;

  if assigned(SharedData) then
    UnmapViewOfFile(SharedData);
  if hFileMapping > 0 then
    CloseHandle(hFileMapping);
end;


procedure TForm1.FormCreate(Sender: TObject);
begin
  AdoConnection1.Connected := true;
end;

end.

ADOConnection1 是我表单上的一个对象。我知道我需要使用“分配”之类的东西来复制整个对象内存,但这在 ADO ConnectionObject 上不存在。为了确保不仅仅是 ConnectionObject 的问题,我还尝试传递一个像 TStringlist 这样的简单对象,然后使用 assign 复制内存,但它仍然会在应用程序 #2 中获取 AV。

如果我在同一个应用程序中运行创建和检索,它工作正常。当我获取此应用程序的副本并在应用程序#1 中运行“创建”功能并在应用程序#2 中运行“检索”时,我得到了访问冲突。

【问题讨论】:

共享内存无能为力。分层数据库访问是您所需要的。中间层访问数据库,客户端与该中间层对话,而不是直接与数据库对话。 不可能跨进程边界共享对象实例。 IPC/共享内存/内存映射文件不会改变它。此外,即使您可以这样做,数据库连接也是基于每个线程进行的,并且这两个进程显然不会在同一个线程中执行。 您认为为什么需要共享 ADO 连接? 为什么需要共享数据库连接?这似乎不是一件想要或需要做的正常事情。你想达到什么目的?这将为您解决什么问题?这当然不是正确的做法。 @Arioch'The:它们都不是 Delphi 对象,特别是 TADOConnection。 IOW,您的评论不适用于所提出的问题,就像写特斯拉、法拉利和兰博基尼一样。 【参考方案1】:

在 cmets 中已经确定不能在不同进程之间共享对象。我稍微简化了您的测试用例,以便能够弄清楚为什么会这样。

在这个版本中,无法共享的对象是一个比 ADOConnection 简单得多的对象,它只有一个整数字段。应用程序的第一个实例创建一个映射并写入具有整数、对象和字符串字段的记录。应用程序的任何后续实例都会尝试检索和显示该信息。

unit Unit1;

interface

uses
  Winapi.Windows, System.SysUtils, System.Classes,
  Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.StdCtrls;

type
  TForm1 = class(TForm)
    procedure FormCreate(Sender: TObject);
    procedure FormDestroy(Sender: TObject);
  end;

var
  Form1: TForm1;

implementation

$R *.dfm

type
  TSimpleObject = class(TObject)
  private
    FInt: Integer;
  end;

  TSharedData = record
    Int: Integer;
    SimpleObject: TSimpleObject;
    Str: string;
  end;
  PSharedData = ^TSharedData;

var
  SharedData: PSharedData;
  hFileMapping: THandle;

const
  MapName = 'MySharedMemory';

procedure TForm1.FormCreate(Sender: TObject);
var
  MapExists: Boolean;
begin
  hFileMapping := CreateFileMapping(INVALID_HANDLE_VALUE, nil, PAGE_READWRITE,
      0, SizeOf(TSharedData), MapName);
  Win32Check(hFileMapping <> 0);
  MapExists := GetLastError = ERROR_ALREADY_EXISTS;

  SharedData := MapViewOfFile(hFileMapping, FILE_MAP_ALL_ACCESS, 0, 0, 0);
  Win32Check(SharedData <> nil);

  if MapExists then begin
    ShowMessage(IntToStr(SharedData.Int));
    ShowMessage(IntToStr(SharedData.SimpleObject.FInt));
    ShowMessage(SharedData.Str);
  end else begin
    SharedData.Int := 555;
    SharedData.SimpleObject := TSimpleObject.Create;
    SharedData.SimpleObject.FInt := 666;
    SharedData.Str := 'test string';
  end;
end;

procedure TForm1.FormDestroy(Sender: TObject);
begin
  Win32Check(CloseHandle(hFileMapping));
  Win32Check(UnmapViewOfFile(SharedData));
end;

应用程序的第二个实例将能够正确显示第一个值,即记录的整数字段。但是,它会因对象的整数字段或记录的字符串字段而失败。它会引发访问冲突或显示不正确的值。

我在表单上放了一个备忘录并转储了一些内存地址,以便能够查看发生了什么。

procedure TForm1.FormCreate(Sender: TObject);
var
  MapExists: Boolean;
begin
  Memo1.Clear;
  hFileMapping := CreateFileMapping(INVALID_HANDLE_VALUE, nil, PAGE_READWRITE,
      0, SizeOf(TSharedData), MapName);
  Win32Check(hFileMapping <> 0);
  MapExists := GetLastError = ERROR_ALREADY_EXISTS;

  SharedData := MapViewOfFile(hFileMapping, FILE_MAP_ALL_ACCESS, 0, 0, 0);
  Win32Check(SharedData <> nil);

  if MapExists then begin
    Memo1.Lines.Add(Format('Found at %p', [SharedData]));
//    ShowMessage(IntToStr(SharedData.Int));
//    ShowMessage(IntToStr(SharedData.SimpleObject.FInt));
//    ShowMessage(SharedData.Str);
  end else begin
    Memo1.Lines.Add(Format('Created at %p', [SharedData]));
    SharedData.Int := 555;
    SharedData.SimpleObject := TSimpleObject.Create;
    SharedData.SimpleObject.FInt := 666;
    SharedData.Str := 'test string';
  end;
  Memo1.Lines.Add('');
  Memo1.Lines.Add(Format('@SharedData.Int: %p', [@SharedData.Int]));
  Memo1.Lines.Add(Format('PInteger(@SharedData.Int)^: %d', [PInteger(@SharedData.Int)^]));
  Memo1.Lines.Add('');
  Memo1.Lines.Add(Format('@SharedData.SimpleObject: %p', [@SharedData.SimpleObject]));
  Memo1.Lines.Add(Format('@SharedData.SimpleObject.FInt: %p', [@SharedData.SimpleObject.FInt]));
  Memo1.Lines.Add(Format('PInteger(@SharedData.SimpleObject.FInt)^: %d', [PInteger(@SharedData.SimpleObject.FInt)^]));
  Memo1.Lines.Add('');
  Memo1.Lines.Add(Format('Pointer(@SharedData.Str)^: %p', [Pointer(Pointer(@SharedData.Str)^)]));
  Memo1.Lines.Add('character payload');
  Memo1.Lines.Add('PChar(Pointer(SharedData.Str)): ' + PChar(Pointer(SharedData.Str)));
end;

下面,左边是应用程序的第一个实例,右边是第二个实例。请注意,第二个实例可能需要几次尝试才能显示而不会出现访问冲突。

第一行是地图的起始地址。请注意,它们在两种情况下都不相同(但是它们可能相同)。这与我们的案例无关,但这是documentation of MapViewOfFile 警告的原因:

不要在内存映射文件中存储指针;存储偏移量 文件映射的基础,以便可以在任何位置使用映射 地址。

第二行是共享记录的整数字段的地址。在这两种情况下,这都等于映射内存的起始地址。这是预期的,因为它是记录的第一个字段。以下行将该地址中保存的值转储为整数,它们看起来很好。

第四行转储记录中包含的对象的地址。对于这两种情况,这是从映射开头偏移的四个字节。看起来对吗?继续以下行,转储对象中包含的整数字段的地址。

类的整数字段的地址有两个令人震惊的事情。首先,它们在两种情况下都是相等的。这不应该是这种情况,因为地图文件根本不是从相同的地址开始的。其次,它们与地图文件的地址相去甚远。毕竟我们保留了 12 个字节,而不是 34 兆字节。

原因是,一个对象引用(在前一行中转储了地址)实际上是一个指向位于其他地方的对象实例的指针,并且该指针指向映射内存段之外的位置。这对于第一个实例来说不是问题,因为它是分配内存的进程。然而,对于第二种情况,该地址具有完全不同的含义。不用管该地址中没有对象,该地址甚至可能不可读。转储为整数的值与预期值不同也就不足为奇了。

同样的解释也适用于最后一段中转储的记录的字符串字段。字符串payload应该是的地址与第二个进程的含义完全不同。

Assign 对对象字段没有帮助,它将指定源对象的属性和其他属性复制到当前对象,它不会重定位或复制一个东西。但是有人可能想知道是否可以使用在已知地址上定义的实例内存来创建对象,例如通过覆盖NewInstance。不幸的是,对于比这个例子中的对象更复杂一点的东西,这也是不可能的。不可能在单个内存块中包含对象运行所需的所有内存。例如,对于您尝试共享的 ADOConnection,ConnectionObject 是另一个指针。

【讨论】:

以上是关于在 Delphi 中的多个应用程序之间共享一个对象的主要内容,如果未能解决你的问题,请参考以下文章

探讨下在Delphi里面进程之间的数据共享

Delphi XE2:在两个不同的进程之间共享一个变量?

如何在多个视图控制器之间全局共享数据

嵌套在 Delphi 框架中的私有组件字段

Delphi 与 VC 共享接口和对象

python中的多处理-在多个进程之间共享大对象(例如pandas数据框)