如何从我的应用程序 to/from DLL 传递和检索内存流?

How do I pass and retrieve memory stream from my Application to/from DLL?

假设我有一个 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 = 对我来说是奖励)

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

1.使用共享堆

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

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

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

2。使用被调用者的堆并导出一个 deallocator

这里使用的是被调用者的native heap:

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 是指向缓冲区的指针。

The InBuff is easy (I think). I pass TMemoryStream.Memory and TMemoryStream.Size.

是的。

Question is how do I allocate the OutBuff in the DLL, and the caller application can convert it back to TMemoryStream and later free that memory (By the caller application)?

鉴于您显示的 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;

一种略有不同的方法是将每个内存流包装为一个 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;