views:

846

answers:

3

After a lot of experimentations, I found a way to exchange PChar from a FreePascal compiled DLL with a Delphi compiled EXE. I'm in charge of both the DLL and EXE source code but one MUST BE in FreePascal and the other one in Delphi. My solution involves the following methods in the DLL:

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;

And from the Delphi EXE, to call the GetAString method, I need to Call the GetAString method, save the PChar to an actual Delphi String and call the FreeString method.

Is this the best way of exchanging a string from a FreePascal DLL with a Delphi EXE ? Can I avoid the call to FreeString from Delphi ?

And finally, if that's the correct solution, how will it behave with Delphi 2010 and the WideString by default: do I need to force WidePChar in FreePascal too ?

+7  A: 

One way to exchange strings between your DLL and Delphi application without using FreeString call is to take string buffer from the calling application as a PChar, and fill the buffer in the DLL. That is how Windows API functions work when they need to exchange strings with calling applications.

To do so, your calling application creates a string buffer, and sends a PChar referring to that buffer along with buffer size to your DLL function. If buffer size is smaller than the actual string DLL has to send to the application, your DLL function can send the actual required size for the buffer to the calling application.

how will it behave with Delphi 2010 and the WideString by default: do I need to force WidePChar in FreePascal too ?

In Delphi 2009 and Delphi 2010, PChar equals to PWideChar. In previous versions of Delphi, and as far as I know, in FreePascal, PChar equals to PAnsiChar. So If you return PChar from your DLL, your code won't work correctly in Delphi 2010. You Should explicitly use PAnsiChar or PWideChar. You can again follow Windows API functions. They provide two versions of many API functions, one with WideChar support which its name has a W character as a suffix, and the other one with ANSI support which its name has an A character as a suffix.

Your DLL function declaration would be something like this:

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

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

EDIT:

Here is a sample code:

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 AnsiChar version, you can use either the same code with AnsiString and PAnsiChar, or convert the ANSI string parameter to Unicode, and call AStringFuncW inside your AStringFuncA function, then convert the return string from AStringFuncW 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 functions. AStringFunc function refers to WideChar version in Delphi 2009 - 2010, and refers to AnsiChar version in other 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, client application first gets actual output size from AStringFunc, then sets a string buffer, and retrieves output string from DLL. Take 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.

vcldeveloper
Thank you very much for the very detailed answer. Even though it seems even more complicated than my first version, I might use this method as this is a standard in Windows API.
John Riche
There is a bug in the AStringFuncW() function. The buffer size misses a +1. The last line " BufferSize := Length(MyOutputStr);" should be: "BufferSize := Length(MyOutputStr) + 1;"
Jan Derk
Yes Jan, you are right, BufferSize misses the last #0 in the string. I edited the code. Thanks.
vcldeveloper
+2  A: 

Depending on what data you're passing, you may be able to use WideStrings instead. They're allocated on the Windows heap, so if you allocate one in the DLL and free it in the EXE they'll both go through the same memory manager.

You should be able to use a function declaration like this:

procedure GetAString(var Result: WideString); stdcall;

And Delphi and FreePascal will both handle the allocation and freeing automatically. WideString is the same in the Unicode-enabled Delphis as the pre-Unicode ones, so you won't need to change things for that either.

Craig Peterson
Thanks Craig. As I mentioned to Serg, I had AV in the DLL using WideString, that's why I went the PChar way.
John Riche
+2  A: 

The rule of thumb for allocating/deallocating memory in EXE and DLL is: the module that allocates memory is responsible for deallocation the same memory. In your example it is DLL who allocates and deallocates memory, so it is correct.

There is one exception from the general rule. Both Delphi and the latest Free Pascal versions implement WideStrings as BSTR - a string data type that is used by COM. WideStrings are allocated and freed by Windows, not by Delphi (or Free Pascal) memory manager. So there is no need to use PWideChar to pass WideString arguments between EXE and DLL - WideStrings can be passed directly.


Updated:

I have tested the following code:

Free Pascal (version 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 (main form):

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.

It appeared that the procedure GetAStringProc works fine while the function GetAStringFunc leads to access violation. Delphi and Free Pascal seems to return the string-type result differently.

Serg
If FreePascal and Delphi handle WideString return values the same way this will work. Apparently it's not guaranteed for other languages: http://groups.google.com/group/borland.public.delphi.language.delphi.general/browse_frm/thread/477cd50d4e1e6667/e57692cc78ed2abd
Craig Peterson
Thanks Serg. Unfortunately, I keep getting an AV using this method. That's why I tried the PChar way I mentioned in the original post.
John Riche
@John Riche: You are right, the function leads to AV - you should use procedure instead of function. I have updated the post.
Serg