How do I pass and retrieve memory stream from my A

2020-06-25 04:56发布

Suppose I have a TMemoryStream I need to pass to my DLL and get back TMemoryStream (Bitmap stream) from the DLL.

I was thinking my DLL would have:

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

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)?

The caller will use dynamic LoadLibrary/FreeLibrary each DLL call.

I would very much like a sample code. Hope I'm not being too rude.

Note 1: The caller application dose not know the output size, and assume it can not specify a MAX buff size.

Note 2: I am not sure about my DLL signature. Please forgive me if I did it wrong. I am Looking for a pattern that will work well (maybe not only for Delphi but for C++/C# Caller as well = Bonus for me)

3条回答
地球回转人心会变
2楼-- · 2020-06-25 05:24

Two obvious options, assuming the callee is to allocate the memory:

1. Use a shared heap

For instance you can use the COM heap. In the callee your write:

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

The caller destroys this with CoTaskMemFree. You can use LocalAlloc, or HeapAlloc if you prefer, it doesn't really matter.

2. Use the callee's heap and export a deallocator

Here you use the native heap of the callee:

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

You also need to export a deallocator:

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

Another option that I rejected is to use a shared memory manager. I tend to avoid that because it constrains the caller to be a Delphi program.

To fill a stream from a buffer call WriteBuffer:

Stream.WriteBuffer(Buff^, BuffSize);

where Buff is a pointer to the buffer.

查看更多
闹够了就滚
3楼-- · 2020-06-25 05:25

A slightly different approach is to wrap each memory stream up as a IStream, and pass around the resulting interface references. So, from the DLL's side:

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;

Personally I like using safecall as well because it's an easy way to be exception-safe, but I guess that's a matter of taste.

Edit

A variant of the above is to have the caller provide both the stream to read and a stream to write to:

//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;

The EXE side might then look something like this:

//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;
查看更多
萌系小妹纸
4楼-- · 2020-06-25 05:27

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

Yes.

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)?

Given the signature of the DLL function you showed, you would not allocate the memory in the DLL at all. The caller has to allocate it. The caller can call Process() once to get the needed size, then allocate it, then call Process() again to fill it. This way, the caller is responsible for both allocating and freeing the memory. For example:

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;

If you actually want the DLL to allocate the memory, you have to change the signature of your DLL function to make OutBuff be a var parameter. You also have to export an additional function so the DLL can free the memory that the DLL allocated. The benefit of this approach is that the caller would only have to call Process() once, and the DLL can decide how it wants to allocate and free the memory. For example:

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;
查看更多
登录 后发表回答