在Freepascal编译的DLL和Delphi编译的EXE之间交换字符串(PChar)

4

经过多次试验,我找到了一种方法来在FreePascal编译的DLL和Delphi编译的EXE之间交换PChar。我负责DLL和EXE源代码,但其中一个必须使用FreePascal,另一个必须使用Delphi。我的解决方案涉及以下方法:

function GetAString(): PChar;
var aString: string;
begin
  aString := 'My String';
  result := StrAlloc(length(aString) + 1);
  StrPCopy(result, aString); 
end;

procedure FreeString(aString: PChar);
begin
  StrDispose(aString); 
end;

从Delphi EXE调用GetAString方法,我需要调用GetAString方法,将PChar保存到实际的Delphi字符串中,并调用FreeString方法。

这是在FreePascal DLL和Delphi EXE之间交换字符串的最佳方法吗?我能否避免在Delphi中调用FreeString吗?

最后,如果这是正确的解决方案,它会如何与Delphi 2010和默认的WideString一起使用:我是否也需要在FreePascal中强制使用WidePChar?


出于好奇,为什么必须使用FreePascal? - David
我需要使用一些内置库,这可能会导致在 Delphi 中进行移植和维护的成本很高。这样,我只需要调用 DLL 中的几个函数即可。 - jonjbar
请参考此问题:https://dev59.com/UE3Sa4cB1Zd3GeqPsh8N - Alex
3个回答

8

如果您不想使用FreeString调用来在DLL和Delphi应用程序之间交换字符串,则一种方法是从调用应用程序获取一个字符串缓冲区作为PChar,并在DLL中填充该缓冲区。这就是Windows API函数在需要与调用应用程序交换字符串时的工作方式。

为此,调用应用程序创建一个字符串缓冲区,并将引用该缓冲区的PChar以及缓冲区大小发送到您的DLL函数。如果缓冲区大小小于DLL必须发送到应用程序的实际字符串,则您的DLL函数可以将所需缓冲区大小发送到调用应用程序。

Delphi 2010和默认的WideString行为如何:我是否也需要在FreePascal中强制使用WidePChar?

在Delphi 2009和Delphi 2010中,PChar等于PWideChar。在以前的Delphi版本中,以及据我所知,在FreePascal中,PChar等于PAnsiChar。因此,如果您从DLL返回PChar,则您的代码在Delphi 2010中将无法正常工作。您应该明确使用PAnsiCharPWideChar。您可以再次遵循Windows API函数的模式:它们提供了几乎每个API函数的两个版本-一个支持WideChar,其名称具有W字符作为后缀,另一个支持AnsiChar,其名称具有A字符作为后缀。

您的DLL函数声明应该像这样:

function AStringFuncW(Buffer: PWideChar; var BufferSize: Integer): Boolean;

function AStringFuncA(Buffer: PAnsiChar; var BufferSize: Integer): Boolean;

编辑:

这里是一个示例代码:

  1. Your DLL function for WideChar would be like this:

    function AStringFuncW(Buffer: PWideChar; var BufferSize: Integer): Boolean; stdcall;
    var
      MyOutputStr : WideString;
    begin
      Result := False;
    
      // Calculate your output string here.
      MyOutputStr := 'This is a sample output';
    
      // Check if buffer is assigned, and its given length is enough
      if Assigned(Buffer) and (BufferSize >= Length(MyOutputStr) + 1) then
      begin
        // Copy output string into buffer
        StrPCopy(Buffer,MyOutputStr);
        Result := True;
      end;
      // Return actual size of output string.
      BufferSize := Length(MyOutputStr) + 1;
    end;
    

    For the AnsiChar version you use PAnsiChar as parameter and either an almost similar code with either AnsiString as variable, or you convert the parameter to PWideChar and call your own AStringFuncW inside your AStringFuncA function, whose result you convert back to PAnsiChar.

  2. Here is how you can define these functions in your interface unit to be used by your DLL clients:

    unit TestDLLIntf;
    
    interface
    
    const
      TestDll = 'Test.dll';
    
      function AStringFuncW(Buffer: PWideChar; var BufferSize: Integer): Boolean; stdcall;
      function AStringFuncA(Buffer: PWideChar; var BufferSize: Integer): Boolean; stdcall;
      function AStringFunc(Buffer: PWideChar; var BufferSize: Integer): Boolean; stdcall;
    
    implementation
    
    function AStringFuncW; external TestDll name 'AStringFuncW';
    function AStringFuncA; external TestDll name 'AStringFuncA';
    {$IFDEF UNICODE}
     function AStringFunc; external TestDll name 'AStringFuncW';
    {$ELSE}
     function AStringFunc; external TestDll name 'AStringFuncA';
    {$ENDIF}
    
    end.
    

    In the above code, both AStringFuncW and AStringFuncA functions are declared as external. The AStringFunc function refers to the WideChar version in Delphi 2009 or 2010, and to AnsiChar in older versions.

  3. Here you can see how your DLL clients can use your function:

    procedure TForm1.Button1Click(Sender: TObject);
    var
      Str : string;
      Size : Integer;
    begin
      // Retrieve required buffer size
      AStringFunc(nil,Size);
      // Set buffer
      SetLength(Str,Size);
      // Retrieve output string from DLL function.
      if AStringFunc(PChar(Str),Size) then
        ShowMessage(Str);
    end;
    

    In the above code, the client application first gets the actual output size from AStringFunc, then sets a string buffer, and retrieves the output string from the DLL. Note that the same code should work in both Unicode and non-Unicode versions of Delphi, because AStringFunc refers to either AStringFuncA or AStringFuncW inside your DLL, depending on whether your compiler supports Unicode or not.


非常感谢您提供如此详细的答案。尽管它似乎比我的第一个版本更加复杂,但我可能会使用这种方法,因为这是Windows API中的标准。 - jonjbar
1
AStringFuncW() 函数中存在一个 bug。缓冲区大小少了 +1。最后一行 "BufferSize := Length(MyOutputStr);" 应该是:"BufferSize := Length(MyOutputStr) + 1;"。 - Jan Derk
是的,Jan,你是对的,BufferSize在字符串中漏掉了最后一个#0。我已经编辑了代码。谢谢。 - vcldeveloper

2

根据您传递的数据,您可以考虑使用WideStrings。它们分配在Windows堆上,因此如果您在DLL中分配一个并在EXE中释放它,它们都将通过相同的内存管理器。

您可以使用以下函数声明:

procedure GetAString(var Result: WideString); stdcall;

同时,Delphi和FreePascal都会自动处理内存的分配和释放。在启用Unicode的Delphi中,WideString与预Unicode版本相同,因此您无需更改任何内容。


谢谢Craig。正如我告诉Serg的那样,我在使用WideString时在DLL中遇到了AV问题,这就是为什么我选择了PChar的方式。 - jonjbar

2
在EXE和DLL中分配/释放内存的经验法则是:分配内存的模块负责释放相同的内存。在您的示例中,分配和释放内存的模块是DLL,因此是正确的。
总体规则有一个例外。Delphi和最新的Free Pascal版本都将WideStrings实现为BSTR - 一种由COM使用的字符串数据类型。WideStrings由Windows分配和释放,而不是由Delphi(或Free Pascal)内存管理器。因此,在EXE和DLL之间传递WideString参数时无需使用PWideChar - 可以直接传递WideStrings。
更新:
我已测试了以下代码:
Free Pascal(版本2.2.4)DLL:
library TestLib1;

{$mode objfpc}{$H+}

{$IFDEF WINDOWS}{$R TestLib1.rc}{$ENDIF}

procedure GetAStringProc(var S: WideString); stdcall;
begin
  S:= '12345';
end;

function GetAStringFunc: WideString; stdcall;
begin
  Result:= '12345';
end;

exports
  GetAStringProc, GetAStringFunc;

begin
end.

Delphi 2009 EXE(主窗体):


unit Unit1;

interface

uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
  Dialogs, StdCtrls;

type
  TForm1 = class(TForm)
    Button1: TButton;
    Button2: TButton;
    procedure FormCreate(Sender: TObject);
    procedure Button1Click(Sender: TObject);
    procedure Button2Click(Sender: TObject);
  private
    S: WideString;
  end;

var
  Form1: TForm1;

type
  TGetAStringProc = procedure(var S: WideString); stdcall;
  TGetAStringFunc = function: WideString; stdcall;

var
  GetAStringProc: TGetAStringProc;
  GetAStringFunc: TGetAStringFunc;

implementation

{$R *.dfm}

procedure TForm1.Button1Click(Sender: TObject);
begin
    GetAStringProc(S);
    Caption:= S;
    S:= '';
end;

procedure TForm1.Button2Click(Sender: TObject);
begin
    S:= GetAStringFunc;
    Caption:= S;
    S:= '';
end;

procedure TForm1.FormCreate(Sender: TObject);
var
  LibHandle: THandle;

begin
  LibHandle:= LoadLibrary('TestLib1.dll');
  if LibHandle <> 0 then begin
    @GetAStringProc:= GetProcAddress(LibHandle, 'GetAStringProc');
    @GetAStringFunc:= GetProcAddress(LibHandle, 'GetAStringFunc');
  end;
  if not Assigned(GetAStringProc) or not Assigned(GetAStringFunc) then
      ShowMessage('Error!');
end;

end.

看起来获取字符串的过程(GetAStringProc)能够正常工作,而获取字符串的函数(GetAStringFunc)会导致访问冲突。Delphi和Free Pascal似乎以不同的方式返回字符串类型的结果。


如果FreePascal和Delphi以相同的方式处理WideString返回值,那么这将起作用。 显然,对于其他语言并不保证:http://groups.google.com/group/borland.public.delphi.language.delphi.general/browse_frm/thread/477cd50d4e1e6667/e57692cc78ed2abd - Zoë Peterson
谢谢Serg。不幸的是,我使用这种方法仍然会出现AV错误。这就是为什么我尝试了我在原帖中提到的PChar方式。 - jonjbar
@John Riche:你说得对,这个函数会导致AV(Access Violation)——你应该使用过程而不是函数。我已经更新了帖子。 - kludg

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