从DLL传递数据到应用程序出现的问题

4
我有些困惑在我的情况下如何正确地使用指针。我有一个带有一些嵌入式资源的DLL,我在这个DLL中公开了一个函数,将其中一个资源的二进制数据传递回其调用应用程序。在这种情况下,我嵌入了一个JPG图像文件。我的DLL确实将文件正确加载到资源流中。但是从那里开始将其传回应用程序变得混乱。
以下是我的DLL代码(使用已加载并命名为“SOMERESOURCE”的JPG):
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.

这是我的应用程序代码(只有一个TBitBtnTImage):

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;

看起来整个DLL调用是成功的,但是接收到的数据是空的(全是0)。我很好奇像Data这样的东西为什么需要被称为Data[0],以及在什么情况下我应该使用,以及在什么情况下需要使用@Data。我完全在DLL中编写了该代码,对这样的工作不熟悉,所以我肯定在某个地方搞砸了。我做错了哪些事情?

3个回答

10

在DLL方面,GetResource()将资源数据读入本地数组,并没有将其复制到传递给函数的缓冲区中。将本地数组分配给Buffer指针并不会复制指向的数据。

在应用程序方面,BitBtn1Click()未为GetResource()分配任何内存以写入资源数据。即使有,你也没有正确地将缓冲区写入TMemoryStream中。即使你这样做了,你也没有正确地将TMemoryStream加载到TPicture中。

你可以采取以下几种方法来解决缓冲区问题:

1) 让GetResource()分配一个缓冲区并将其返回给应用程序,然后当应用程序完成时让应用程序将缓冲区传回DLL以释放它:

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) 让应用程序查询DLL资源的大小,然后分配一个缓冲区并将其传递给DLL进行填充:

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.

或者:

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.

非常感谢您提供这些示例,我花了一整天的时间试图理清其中的问题。它总是在左右崩溃,但至少我已经成功解决了DLL调用的崩溃问题,只是仍然没有运行成功。至少我没有问一个毫无意义的“如何编写代码”的问题。更多的人应该首先尝试自己解决问题。 - Jerry Dodge
你个人会推荐哪种方法作为稳定性更好的选择呢?我的意思是,我知道它们都很稳定,但哪种方法更加可靠,你自己会使用呢? - Jerry Dodge
1
这实际上是个人选择的问题。要么1)应用程序查询DLL以获取资源大小,分配内存并将其传递给DLL进行填充,然后在完成后释放内存;或者2)让DLL分配并返回内存,然后在完成后让应用程序将内存传回DLL。两种方法都可以工作,并且都需要两次进入DLL。就我个人而言,我使用#1,因为许多API都是这样设置的。 - Remy Lebeau
Remy,我在你第一个示例代码的 DLL 中发现了一个内存泄漏问题。 发生了意外的内存泄漏。未预期泄漏的中型和大型块的大小为227628 - Jerry Dodge
除非您将错误的指针传递给 FreeBuffer(),否则该示例中不应该有泄漏。 您需要传递与 GetResource() 返回的相同指针,以便由分配它的同一内存管理器释放它。 - Remy Lebeau
显示剩余2条评论

8

你不需要从DLL中导出任何函数,只需直接从主机可执行文件中使用DLL的模块句柄即可。

你已经将一个模块句柄传递给了资源流构造函数。你正在传递可执行文件的模块句柄。相反,应传递库的模块句柄。

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

如果你不想调用DLL中的任何函数,而它只是一个资源DLL,那么请使用LoadLibraryExLOAD_LIBRARY_AS_IMAGE_RESOURCE代替:
hMod := LoadLibraryEx('ResDLL', 0, LOAD_LIBRARY_AS_IMAGE_RESOURCE);

也许你知道DLL已经被加载。例如,它已被隐式地链接到你的可执行文件中。在这种情况下,你可以更简单地使用GetModuleHandle,而不是LoadLibraryLoadLibraryEx

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

请注意,为了简化解释,我省略了所有的错误检查。

是的,我知道这个,但我没有使用它,因为这个 DLL 将会做比资源和图像更多的事情。它还将负责加载未在 DLL 中编译的外部资源以及安全性。我只是试图理解数据流概念。 - Jerry Dodge
好的,没问题。从问题中我并没有立刻明白这一点。 - David Heffernan
请注意:我已经提出了另一个问题,明确引用了这个答案:https://dev59.com/hm7Xa4cB1Zd3GeqPmir5 - Jerry Dodge

2

另一种将流从DLL传递到应用程序的方法可以使用接口流。

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;

IDelphiStream的代码可以在http://www.delphipraxis.net上找到。
我不会将MemoryStream_Interface的内容复制到此帖子中,因为提到页面的代码没有版权信息。


1
你的 TGetStream 应该使用 stdcall 约定吗? - kobik
你知道吗,我可能会考虑做这件事,我假设这不需要共享内存?还是需要呢? - Jerry Dodge

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接