【问题标题】:How do I pass and retrieve memory stream from my Application to/from DLL?如何从我的应用程序向/从 DLL 传递和检索内存流?
【发布时间】:2015-12-10 21:19:50
【问题描述】:

假设我有一个TMemoryStream,我需要传递给我的 DLL 并从 DLL 中取回 TMemoryStream(位图流)。

我在想我的 DLL 会有:

procedure Process(
  InBuff: Pointer; 
  InBuffSize: Integer; 
  var OutBuff: Pointer; 
  var OutBuffSize: Integer
); stdcall;

InBuff 很简单(我认为)。我通过TMemoryStream.MemoryTMemoryStream.Size

问题是我如何在 DLL 中分配OutBuff,调用者应用程序可以将其转换回TMemoryStream,然后释放该内存(由调用者应用程序)?

调用者将使用动态LoadLibrary/FreeLibrary每个DLL调用。

我非常想要一个示例代码。希望我不会太粗鲁。

注意 1:调用者应用程序不知道输出大小,并假设它不能指定 MAX buff 大小。

注意 2:我不确定我的 DLL 签名。如果我做错了,请原谅我。我正在寻找一种可以很好地工作的模式(可能不仅适用于 Delphi,而且适用于 C++/C# Caller = 我的奖励)

【问题讨论】:

  • @Remy 请不要更改问题。它指出,“问题是我如何在 DLL 中分配 OutBuff”,这很清楚。意图与当前的问题一样,就像在有人回答之前一样。
  • @DavidHeffernan:我不是更改问题代码的人。你做到了,我只是把它恢复到原来的样子。如果问题的意图与提供的实际代码不匹配,则 OP 应该相应地修复了问题。如果代码中存在错误,请在评论或答案中指出,不要盲目假设您编写的任何代码都是 OP 在其项目中实际使用的代码。
  • @Remy 这不是这个网站的运作方式。我所做的编辑是有效的。
  • @RemyLebeau,对不起。我最初并不确定 DLL 签名是否正确。我不知道var 指针。

标签: delphi dll delphi-7


【解决方案1】:

一种稍微不同的方法是将每个内存流包装为 IStream,并传递生成的接口引用。所以,从 DLL 的角度来看:

uses
  System.SysUtils, System.Classes, Vcl.AxCtrls;

procedure DoProcess(InStream, OutStream: TStream);
begin
  //...do the actual processing here
end;

//wrapper export
procedure Process(AInStream: IStream; out AOutStream: IStream); safecall;
var
  InStream, OutStream: TStream;
begin
  InStream := TOleStream.Create(AInStream);
  try
    OutStream := TMemoryStream.Create;
    try
      DoProcess(InStream, OutStream);
      AOutStream := TStreamAdapter.Create(OutStream, soOwned);
    except
      OutStream.Free;
      raise;
    end;
  finally
    InStream.Free;
  end;
end;

我个人也喜欢使用 safecall,因为它是一种简单的异常安全方法,但我想这是一个品味问题。

编辑

上面的一个变体是让调用者同时提供要读取的流要写入的流:

//wrapper export
procedure Process(AInStream, AOutStream: IStream); safecall;
var
  InStream, OutStream: TStream;
begin
  InStream := TOleStream.Create(AInStream);
  try
    OutStream := TOleStream.Create(AOutStream);
    try
      DoProcess(InStream, OutStream);
    finally
      OutStream.Free;
    end;
  finally
    InStream.Free;
  end;
end;

EXE 端可能看起来像这样:

//wrapper import
type
  TDLLProcessProc = procedure(AInStream, AOutStream: IStream); safecall;

procedure Process(AInStream, AOutStream: TStream);
var
  InStream, OutStream: IStream;
  DLLProc: TDLLProcessProc;
  Module: HMODULE;
begin
  InStream := TStreamAdapter.Create(AInStream, soReference);
  OutStream := TStreamAdapter.Create(AOutStream, soReference);
  Module := LoadLibrary(MySuperLib);
  if Module = 0 then RaiseLastOSError;
  try
    DLLProc := GetProcAddress(Module, 'Process');
    if @DLLProc = nil then RaiseLastOSError;
    DLLProc(InStream, OutStream);
  finally
    FreeLibrary(Module);
  end;
end;

【讨论】:

  • soOwned 不危险吗?调用者如何保证在调用 FreeLibrary 之前释放引用?
  • 调用者必须确保返回的IStream 超出范围并在DLL 被卸载之前被释放。如果 IStream 在调用 Process() 的站点上用作局部变量,则几乎可以保证(假设 DLL 未在另一个线程中卸载)。如果您开始传递 IStream 以延长其生命周期一段时间,您只需要担心这一点。
  • @RemyLebeau,通过在 FreeLibrary 之前将 AOutStream 设置为 nil
  • @zig - 查看我的编辑。如果这确实是一个问题,只需让调用者同时传递要读取的流和要写入的流。
  • @ChrisRolliston,您在OutStream := TStreamAdapter.Create(AInStream, soReference); 中有错字AInStream -> AOutStream
【解决方案2】:

两个明显的选择,假设被调用者是分配内存:

1.使用共享堆

例如,您可以使用 COM 堆。在被调用者中你写:

OutBuffSize := ...; // you know what this value is
OutBuff := CoTaskMemAlloc(OutBuffSize);
// populate the buffer

调用者用CoTaskMemFree 销毁它。如果您愿意,可以使用LocalAllocHeapAlloc,这并不重要。

2。使用被调用者的堆并导出一个释放器

这里你使用被调用者的原生堆:

OutBuffSize := ...; // you know what this value is
GetMem(OutBuff, OutBuffSize);
// populate the buffer

你还需要导出一个释放器:

procedure DeallocateMemory(Ptr: Pointer); stdcall;
begin
  FreeMem(Ptr);
end;

我拒绝的另一个选择是使用共享内存管理器。我倾向于避免这种情况,因为它将调用者限制为 Delphi 程序。

从缓冲区调用 WriteBuffer 填充流:

Stream.WriteBuffer(Buff^, BuffSize);

其中Buff 是指向缓冲区的指针。

【讨论】:

  • 在 #1 和 #2 中,您必须将 OutBuff 更改为 var 参数,以便 DLL 可以更改调用者的输入变量以指向分配的内存。
  • @Remy 它是一个 var 参数
  • 那不是我写那条评论的时候。您编辑了问题以添加 var,而不是单独留下原始问题并在答案中提及 var
  • @Remy 这就像一个明显的错误,与问题中的文字相矛盾,所以我修复了它。该问题在我的答案发布之前进行了编辑,因此在您发布评论之前。
  • @DavidHeffernan,我如何“填充缓冲区”?在 CoTaskMemAlloc 之后。
【解决方案3】:

InBuff 很简单(我认为)。我通过了 TMemoryStream.Memory 和 TMemoryStream.Size。

是的。

问题是我如何在 DLL 中分配 OutBuff,调用者应用程序可以将其转换回 TMemoryStream 并随后释放该内存(由调用者应用程序)?

鉴于您展示的 DLL 函数的签名,您根本不会在 DLL 中分配内存。调用者必须分配它。调用者可以调用Process() 一次以获得所需的大小,然后分配它,然后再次调用Process() 来填充它。这样,调用者负责分配和释放内存。例如:

procedure Process(InBuff: Pointer; InBuffSize: Integer; OutBuff: Pointer; var OutBuffSize: Integer); stdcall;
begin
  //...
  if (OutBuf <> nil) then
  begin
    // copy no more than OutBuffSize bytes into OutBuf, and then
    // update OutBuffSize with the number of bytes actually copied...
    Move(..., OutBuf^, ...);
    OutBuffSize := ...;
  end else begin
    // update OutBuffSize with the number of bytes needed for OutBuff...
    OutBuffSize := ...;
  end;
  //...
end;

var
  InStream: TMemoryStream;
  OutStream: TMemoryStream;
  BuffSize: Integer;
begin
  InStream := TMemoryStream.Create;
  try
    // fill InStream as needed...

    BuffSize := 0;
    Process(InStream.Memory, InStream.Size, nil, BuffSize);

    OutStream := TMemoryStream.Create;
    try
      OutStream.Size := BuffSize;
      Process(InStream.Memory, InStream.Size, OutStream.Memory, BuffSize);
      // use OutStream as needed...
    finally
      OutStream.Free;
    end;
  finally
    InStream.Free;
  end;
end;

如果您确实希望 DLL 分配内存,则必须更改 DLL 函数的签名以使 OutBuff 成为 var 参数。您还必须导出一个附加函数,以便 DLL 可以释放 DLL 分配的内存。这种方法的好处是调用者只需调用Process() 一次,DLL 可以决定它要如何分配和释放内存。例如:

procedure Process(InBuff: Pointer; InBuffSize: Integer; var OutBuff: Pointer; var OutBuffSize: Integer); stdcall;
begin
  //...
  OutBuffSize := ...;
  GetMem(OutBuf, OutBuffSize);
  Move(..., OutBuf^, OutBuffSize);
  //...
end;

procedure FreeProcessBuff(InBuff: Pointer); stdcall;
begin
  FreeMem(InBuff);
end;

type
  TMemoryBufferStream = class(TCustomMemoryStream)
  public
    constructor Create(APtr: Pointer; ASize: NativeInt);
  end;

procedure TMemoryBufferStream.Create(APtr: Pointer; ASize: NativeInt);
begin
  inherited Create;
  SetPointer(APtr, ASize);
end;

...

var
  InStream: TMemoryStream;
  OutStream: TMemoryBufferStream;
  Buff: Pointer;
  BuffSize: Integer;
begin
  InStream := TMemoryStream.Create;
  try
    // fill InStream as needed...

    Buff := nil;
    BuffSize := 0;
    Process(InStream.Memory, InStream.Size, Buff, BuffSize);
    try
      OutStream := TMemoryBufferStream.Create(Buff, BuffSize);
      try
        // use OutStream as needed...
      finally
        OutStream.Free;
      end;
    finally
      FreeProcessBuff(Buff);
    end;
  finally
    InStream.Free;
  end;
end;

【讨论】:

  • "The caller has to allocate it." 为什么?只有 DLL 过程完成后才具有该信息。 DLL 事先并不知道输出大小。为什么 DLL 最初处理只是为了知道输出大小?然后再次处理...
  • @zig:很多自定义 API,甚至 Win32 API 本身,在许多情况下都被设计为以这种方式工作。它允许调用者决定如何管理内存,从而使内存分配更加灵活。此外,我的评论与显示的 original DLL 声明有关(在 David 编辑问题之前)。如果OutBuff 不是var 参数,则DLL 无法返回新分配的指向调用者变量的指针,因此调用者必须分配内存并将其传递给DLL。
  • 我不介意另一个 DLL 签名。 "If OutBuff is not a var parameter, the DLL cannot return a newly-allocated pointer to the caller's variable"我不知道。感谢您指出这一点。
  • 感谢您提供非常有用的代码。这让我明白了 DLL 如何分配内存并将其分配给 TMemoryStream。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2014-01-25
  • 1970-01-01
  • 2017-04-05
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多