Issues passing data from DLL to Application

喜夏-厌秋 提交于 2020-01-01 03:30:14

问题


I'm a bit puzzled as to how Pointers should be properly used in my scenario. I have a DLL with some embedded resources in it. I expose a function in this DLL which passes binary data of one of those resources back to its calling app. In this case, I've embedded a JPG image file. My DLL does properly load the file into a resource stream. However from there, the passing it back to the app gets messy.

Here's my DLL's code (with a JPG loaded and named SOMERESOURCE):

library ResDLL;

{$R *.dres}

uses
  System.SysUtils,
  System.Classes,
  Winapi.Windows;

{$R *.res}

function GetResource(const ResName: PChar; Buffer: Pointer;
  var Length: Integer): Bool; stdcall;
var
  S: TResourceStream;
  L: Integer;
  Data: array of Byte;
begin
  Result:= False;
  try
    S:= TResourceStream.Create(HInstance, UpperCase(ResName), RT_RCDATA);
    try
      S.Position:= 0;
      L:= S.Size;
      Length:= L;
      SetLength(Data, L);
      S.Read(Data[0], L);
      Buffer:= @Data[0];
      Result:= True;
    finally
      S.Free;
    end;
  except
    Result:= False;
  end;
end;

exports
  GetResource;

begin
end.

And here's my app's code (with just a TBitBtn and TImage):

function GetResource(const ResName: PChar; Buffer: Pointer;
  var Length: Integer): Bool; stdcall; external 'ResDLL.dll';

procedure TForm1.BitBtn1Click(Sender: TObject);
var
  Buffer: array of Byte;
  Size: Integer;
  S: TMemoryStream;
  P: TPicture;
begin
  if GetResource('SOMERESOURCE', @Buffer[0], Size) then begin
    S:= TMemoryStream.Create;
    try
      SetLength(Buffer, Size);
      S.Write(Buffer, Size);
      S.Position:= 0;
      P:= TPicture.Create;
      try
        P.Graphic.LoadFromStream(S);
        Image1.Picture.Assign(P);
      finally
        P.Free;
      end;
    finally
      S.Free;
    end;
  end else begin
    raise Exception.Create('Problem calling DLL');
  end;
end;

It appears as if the whole DLL call is successful, however the data which was received is empty (full of 0's). I am full of curiosity as to how something like Data would need to be called as Data[0], and in what scenarios I should, and also in what scenarios I need to use @Data. I wrote that code in the DLL entirely, and I'm not familiar with such work, so I'm sure I botched it up somewhere. Where am I going wrong?


回答1:


On the DLL side, GetResource() is reading the resource data into a local array and not copying it into the buffer that is passed to the function. Assigning the local array to the Buffer pointer does not copy the data being pointed at.

On the app side, BitBtn1Click() is not allocating any memory for GetResource() to write the resource data into. Even if it were, you are not writing the buffer into the TMemoryStream correctly. Even if you were, you are not loading the TMemoryStream into the TPicture correctly.

You have a couple of approaches you can take to fix the Buffer issue:

1) have GetResource() allocate a buffer and return it to the app, then have the app pass the buffer back to the DLL when finished to free it:

library ResDLL;

{$R *.dres}

uses
  System.SysUtils,
  System.Classes,
  Winapi.Windows;

{$R *.res}

function GetResourceData(const ResName: PChar; var Buffer: Pointer;
  var Length: Integer): Bool; stdcall;
var
  S: TResourceStream;
  L: Integer;
  Data: Pointer;
begin
  Result := False;
  try
    S := TResourceStream.Create(HInstance, UpperCase(ResName), RT_RCDATA);
    try
      L := S.Size;
      GetMem(Data, L);
      try
        S.ReadBuffer(Data^, L);
        Buffer := Data;
        Length := L;
      except
        FreeMem(Data);
        raise;
      end;
      Result := True;
    finally
      S.Free;
    end;
  except
  end;
end;

procedure FreeResourceData(Buffer: Pointer); stdcall;
begin
  try
    FreeMem(Buffer);
  except
  end;
end;

exports
  GetResourceData,
  FreeBufferData;

begin
end.

.

unit uMain;

interface

uses
  Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, Vcl.Graphics,
  Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.StdCtrls, Vcl.Buttons, Vcl.ExtCtrls;

type
  TForm1 = class(TForm)
    BitBtn1: TBitBtn;
    Image1: TImage;
    procedure BitBtn1Click(Sender: TObject);
  private
  public
  end;

var
  Form1: TForm1;

implementation

uses
  Vcl.Imaging.jpeg;

{$R *.dfm}

function GetResourceData(const ResName: PChar; var Buffer: Pointer;
  var Length: Integer): Bool; stdcall; external 'ResDLL.dll';

procedure FreeResourceData(Buffer: Pointer); stdcall; external 'ResDLL.dll';

procedure TForm1.BitBtn1Click(Sender: TObject);
var
  Buffer: Pointer;
  Size: Integer;
  S: TMemoryStream;
  JPG: TJPEGImage;
begin
  if GetResourceData('SOMERESOURCE', Buffer, Size) then
  begin
    try
      S := TMemoryStream.Create;
      try
        S.WriteBuffer(Buffer^, Size);
        S.Position := 0;
        JPG := TJPEGImage.Create;
        try
          JPG.LoadFromStream(S);
          Image1.Picture.Assign(JPG);
        finally
          JPG.Free;
        end;
      finally
        S.Free;
      end;
    finally
      FreeResourceData(Buffer);
    end;
  end else begin
    raise Exception.Create('Problem calling DLL');
  end;
end;

end.

2) have the app query the DLL for the size of the resource, then allocate a buffer and pass it to the DLL to fill in:

library ResDLL;

{$R *.dres}

uses
  System.SysUtils,
  System.Classes,
  Winapi.Windows;

{$R *.res}

function GetResourceData(const ResName: PChar; Buffer: Pointer;
  var Length: Integer): Bool; stdcall;
var
  S: TResourceStream;
  L: Integer;
  Data: Pointer;
begin
  Result := False;
  try
    S := TResourceStream.Create(HInstance, UpperCase(ResName), RT_RCDATA);
    try
      L := S.Size;
      if Buffer <> nil then
      begin
        if Length < L then Exit;
        S.ReadBuffer(Buffer^, L);
      end;
      Length := L;
      Result := True;
    finally
      S.Free;
    end;
  except
  end;
end;

exports
  GetResourceData;

begin
end.

.

unit uMain;

interface

uses
  Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, Vcl.Graphics,
  Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.StdCtrls, Vcl.Buttons, Vcl.ExtCtrls;

type
  TForm1 = class(TForm)
    BitBtn1: TBitBtn;
    Image1: TImage;
    procedure BitBtn1Click(Sender: TObject);
  private
  public
  end;

var
  Form1: TForm1;

implementation

uses
  Vcl.Imaging.jpeg;

{$R *.dfm}

function GetResourceData(const ResName: PChar; Buffer: Pointer;
  var Length: Integer): Bool; stdcall; external 'ResDLL.dll';

procedure TForm1.BitBtn1Click(Sender: TObject);
var
  Buffer: array of Byte;
  Size: Integer;
  S: TMemoryStream;
  JPG: TJPEGImage;
begin
  if GetResourceData('SOMERESOURCE', nil, Size) then
  begin
    SetLength(Buffer, Size);
    if GetResourceData('SOMERESOURCE', @Buffer[0], Size) then
    begin
      S := TMemoryStream.Create;
      try
        S.WriteBuffer(Buffer[0], Size);
        S.Position := 0;
        // alternatively, use TBytesStream, or a custom
        // TCustomMemoryStream derived class, to read
        // from the original Buffer directly so it does
        // not have to be copied in memory...

        JPG := TJPEGImage.Create;
        try
          JPG.LoadFromStream(S);
          Image1.Picture.Assign(JPG);
        finally
          JPG.Free;
        end;
      finally
        S.Free;
      end;
      Exit;
    end;
  end;
  raise Exception.Create('Problem calling DLL');
end;

end.

Or:

library ResDLL;

{$R *.dres}

uses
  System.SysUtils,
  System.Classes,
  Winapi.Windows;

{$R *.res}

function GetResourceData(const ResName: PChar; Buffer: Pointer;
  var Length: Integer): Bool; stdcall;
var
  S: TResourceStream;
  L: Integer;
  Data: Pointer;
begin
  Result := False;
  if (Buffer = nil) or (Length <= 0) then Exit;
  try
    S := TResourceStream.Create(HInstance, UpperCase(ResName), RT_RCDATA);
    try
      L := S.Size;
      if Length < L then Exit;
      S.ReadBuffer(Buffer^, L);
      Length := L;
      Result := True;
    finally
      S.Free;
    end;
  except
  end;
end;

function GetResourceSize(const ResName: PChar): Integer; stdcall;
var
  S: TResourceStream;
begin
  Result := 0;
  try
    S := TResourceStream.Create(HInstance, UpperCase(ResName), RT_RCDATA);
    try
      Result := S.Size;
    finally
      S.Free;
    end;
  except
  end;
end;

exports
  GetResourceData,
  GetResourceSize;

begin
end.

.

unit uMain;

interface

uses
  Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, Vcl.Graphics,
  Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.StdCtrls, Vcl.Buttons, Vcl.ExtCtrls;

type
  TForm1 = class(TForm)
    BitBtn1: TBitBtn;
    Image1: TImage;
    procedure BitBtn1Click(Sender: TObject);
  private
  public
  end;

var
  Form1: TForm1;

implementation

uses
  Vcl.Imaging.jpeg;

{$R *.dfm}

function GetResourceData(const ResName: PChar; Buffer: Pointer;
  var Length: Integer): Bool; stdcall; external 'ResDLL.dll';

function GetResourceSize(const ResName: PChar): Integer; stdcall; external 'ResDLL.dll';

procedure TForm1.BitBtn1Click(Sender: TObject);
var
  Buffer: array of Byte;
  Size: Integer;
  S: TMemoryStream;
  JPG: TJPEGImage;
begin
  Size := GetResourceSize('SOMERESOURCE');
  id Size > 0 then
  begin
    SetLength(Buffer, Size);
    if GetResourceData('SOMERESOURCE', @Buffer[0], Size) then
    begin
      S := TMemoryStream.Create;
      try
        S.WriteBuffer(Buffer[0], Size);
        S.Position := 0;
        JPG := TJPEGImage.Create;
        try
          JPG.LoadFromStream(S);
          Image1.Picture.Assign(JPG);
        finally
          JPG.Free;
        end;
      finally
        S.Free;
      end;
      Exit;
    end;
  end;
  raise Exception.Create('Problem calling DLL');
end;

end.



回答2:


You don't need to export any functions at all from your DLL. You can just use the DLL's module handle directly from your host executable.

You are already passing a module handle to the resource stream constructor. You are passing the module handle of the executable. Instead, pass the module handle of the library.

var
  hMod: HMODULE;
....
hMod := LoadLibrary('ResDLL');
try
  S:= TResourceStream.Create(hMod, ...);
  ....
finally
  FreeLibrary(hMod);
end;

If you don't want to call any functions in the DLL, if it is a resource only DLL, then use LoadLibraryEx and LOAD_LIBRARY_AS_IMAGE_RESOURCE instead:

hMod := LoadLibraryEx('ResDLL', 0, LOAD_LIBRARY_AS_IMAGE_RESOURCE);

Perhaps you know that the the DLL is already loaded. For example, it is linked to your executable implicitly. In that case you can more simply use GetModuleHandle rather than LoadLibrary or LoadLibraryEx.

hMod := GetModuleHandle('ResDLL');
S:= TResourceStream.Create(hMod, ...);

Note that I omitted all error checking for the sake of a simple exposition.




回答3:


Another way passing the stream from a DLL to the application could be using interfaced streams.

implementation
uses MemoryStream_Interface;
{$R *.dfm}

Type
TGetStream = Procedure(var iStream:IDelphiStream);stdcall;

procedure TForm1.Button1Click(Sender: TObject);
var
 h:THandle;
 p:TGetStream;
 ms :IDelphiStream;
 j:TJpegImage;
begin
   ms := TInterfacedMemoryStream.Create;
   h := LoadLibrary('ShowStream.dll');
   if h <> 0 then
      try
      @p := GetProcAddress(h,'GetJpegStream');
      p(ms);
      ms.Position := 0;
      j := TJpegImage.create;
      Image1.Picture.Assign(j);
      j.Free;
      Image1.Picture.Graphic.LoadFromStream(TInterfacedMemoryStream(ms));
      finally
      FreeLibrary(h);
      end;
end;

The code for IDelphiStream can be found on http://www.delphipraxis.net.
I won't copy the content of MemoryStream_Interface to this post, because there are no copyright informations on the code from the mentioned page.



来源:https://stackoverflow.com/questions/14681364/issues-passing-data-from-dll-to-application

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!