0

I call a function from DLL-file in Inno Setup Script and its return type is PAnsiChar. In order to get the whole string I need to dereference the pointer but the standard pascal syntax doesn't work here. Is it even possible to do that?

function SQLDLL : PAnsiChar;
external 'GetSQLServerInstances@files:IsStartServer.dll stdcall setuponly';

function NextButtonClick(CurPage: Integer): Boolean;
var
  hWnd: Integer;
  Str : AnsiString;
begin
  if CurPage = wpWelcome then begin
    hWnd := StrToInt(ExpandConstant('{wizardhwnd}'));

    MessageBox(hWnd, 'Hello from Windows API function', 'MessageBoxA', MB_OK or MB_ICONINFORMATION);

    MyDllFuncSetup(hWnd, 'Hello from custom DLL function', 'MyDllFunc', MB_OK or MB_ICONINFORMATION);

    Str := SQLDLL;
    try
      { if this DLL does not exist (it shouldn't), an exception will be raised }
      DelayLoadedFunc(hWnd, 'Hello from delay loaded function', 'DllFunc', MB_OK or MB_ICONINFORMATION);
    except
      { handle missing dll here }
    end;
  end;
  Result := True;
end;

I have only the DLL-file. The original language is Delphi.

I updated to the latest version of Inno Setup 6.0.3 and tested this code on my home Windows 10 Pro machine:

[Setup]
AppName=My Program
AppVersion=1.5
WizardStyle=modern
DefaultDirName={autopf}\My Program
DisableProgramGroupPage=yes
DisableWelcomePage=no
UninstallDisplayIcon={app}\MyProg.exe
OutputDir=userdocs:Inno Setup Examples Output

[Files]
Source: "MyProg.exe"; DestDir: "{app}"
Source: "MyProg.chm"; DestDir: "{app}"
Source: "Readme.txt"; DestDir: "{app}"; Flags: isreadme
Source: "IsStartServer.dll"; Flags: dontcopy

[Code]
function SQLDLL : PAnsiChar;
external 'GetSQLServerInstances@files:IsStartServer.dll stdcall';

function NextButtonClick(CurPage: Integer): Boolean;
var
  Str : PAnsiChar;
begin
  Str := SQLDLL;
  Result := True;
end; 

and now I'm having this kind of error: enter image description here

I don't understand why does it have to look into my 'temp' directory? I've also heard that this problem may somehow be connected with the group policies in Windows 10 UAC, but I'm not really sure what should I do here to get rid of this error.

JConstantine
  • 1,020
  • 7
  • 19

2 Answers2

3

If I understand correctly, your SQLDLL manages some memory buffer itself and returns a pointer to a Unicode string (not ANSI, that's why you got only one character when you tried PAnsiChar, according to your comment).

Inno Setup doesn't support this directly and doesn't even have a PWideChar type. However, we can handle it ourselves. We just have to allocate a Inno string with the right size and copy the data manually.

Here is a working example how to do that. It uses GetCommandLineW as an example function that returns a PWideChar, but you can do the same with your SQLDLL function.

  • Get the pointer from the external function and store it in a variable (a Cardinal - in my example I created a typedef PWideChar for it).
  • Get the string length using lstrlenW.
  • Create an empty regular String, but set it to the right length using SetLength. This will reserve enough capacity that we can write the actual contents into it in the next step.
  • Use lstrcpyW to copy the string that's referenced by the pointer to your regular String variable.
    • (In case you use the ANSI version of Inno Setup: Use WideCharToMultiByte instead, see my update at the end of this post.)

The trick is to import lstrcpyW in such a way that the destination pointer is declared as String but the source pointer is declared as Cardinal (or my typedef PWideChar here).

type
  PWideChar = Cardinal; { Inno doesn't have a pointer type, so we use a Cardinal instead }

{ Example of a function that returns a PWideChar }
function GetCommandLineW(): PWideChar;
external 'GetCommandLineW@kernel32.dll stdcall';

{ This function allows us to get us the length of a string from a PWideChar }
function lstrlenW(lpString: PWideChar): Cardinal;
external 'lstrlenW@kernel32.dll stdcall';

{ This function copies a string - we declare it in such a way that we can pass a pointer
  to an Inno string as destination
  This works because Inno will actually pass a PWideChar that points to the start of the
  string contents in memory, and internally the string is still null-terminated
  We just have to make sure that the string already has the right size beforehand! }
function lstrcpyW_ToInnoString(lpStringDest: String; lpStringSrc: PWideChar): Integer;
external 'lstrcpyW@kernel32.dll stdcall';

function InitializeSetup(): Boolean;
var
  returnedPointer: PWideChar; { This is what we get from the external function }
  stringLength: Cardinal; { Length of the string we got }
  innoString: String; { This is where we'll copy the string into }
begin
  { Let's get the PWideChar from the external function }
  returnedPointer := GetCommandLineW();

  { The pointer is actually just a renamed Cardinal at this point: }
  Log('String pointer = ' + IntToStr(returnedPointer));

  { Now we have to manually allocate a new Inno string with the right length and
    copy the data into it }

  { Start by getting the string length }
  stringLength := lstrlenW(returnedPointer);
  Log('String length = ' + IntToStr(stringLength));

  { Create a string with the right size }
  innoString := '';
  SetLength(innoString, stringLength);

  { This check is necessary because an empty Inno string would translate to a NULL pointer
    and not a pointer to an empty string, and lstrcpyW cannot handle that. }
  if StringLength > 0 then begin
    { Copy string contents from the external buffer to the Inno string }
    lstrcpyW_ToInnoString(innoString, returnedPointer);
  end;

  { Now we have the value stored in a proper string variable! }
  Log('String value = ' + innoString);

  Result := False;
end;

If you put this into an installer and run it, you see output like this:

[15:10:55,551]   String pointer = 9057226
[15:10:55,560]   String length = 106
[15:10:55,574]   String value = "R:\Temp\is-9EJQ6.tmp\testsetup.tmp" /SL5="$212AC6,121344,121344,Z:\Temp\testsetup.exe" /DEBUGWND=$222722 

As you can see, the command line string (which we get as a PWideChar) is copied to a regular string variable correctly and can be accessed normally at the end.


Update: In case you are using the ANSI version of Inno Setup and not Unicode, this code alone won't work. The change needed is this: Instead of using lstrcpyW, you'd use WideCharToMultiByte:

function WideCharToMultiByte_ToInnoString(CodePage: Cardinal; dwFlags: Cardinal; lpWideCharStr: PWideChar; cchWideChar: Cardinal; lpMultiByteStr: String; cbMultiByte: Cardinal; lpDefaultChar: Cardinal; lpUsedDefaultChar: Cardinal): Integer;
external 'WideCharToMultiByte@kernel32.dll stdcall';

{ Later on: Instead of calling lstrcpyW_ToInnoString, use this:
  Note: The first parameter 0 stands for CP_ACP (current ANSI code page), and the
  string lengths are increased by 1 to include the null terminator }
WideCharToMultiByte_ToInnoString(0, 0, returnedPointer, stringLength + 1, innoString, stringLength + 1, 0, 0);
CherryDT
  • 25,571
  • 5
  • 49
  • 74
  • 1
    Good answer. Assuming the OP is really wrong about the function declaration or implementation of the DLL (I actually believe so too). And also assuming OP uses Unicode version of Inno Setup (what I'm not sure, as OP uses ages old version of Inno Setup). – Processing `PWideChar` is also covered in my answer to [Upgrading from Ansi to Unicode version of Inno Setup](https://stackoverflow.com/q/43158677/850848). – Martin Prikryl Mar 09 '20 at 13:45
  • Thanks for the hint, I added info how to do it in the ANSI version. – CherryDT Mar 09 '20 at 13:55
0

You cannot dereference a pointer in Inno Setup Pascal Script.

But there are numerous hacks that allow that. Those hacks are very specific, so it depends on particular use case.


Though in your specific case, as pointers to character arrays are widespread in APIs, Inno Setup Pascal Script (similarly to Delphi) can assign a pointer to a character array to a string.

So, you should be able to simply assign the PChar to AnsiString:

function ReturnsPAnsiChar: PAnsiChar; extern '...';

var
  Str: AnsiString;
begin
  Str := ReturnsPAnsiChar;
end;

See How to return a string from a DLL to Inno Setup?

Martin Prikryl
  • 188,800
  • 56
  • 490
  • 992
  • I've done that but it still gives me only the first character of the string. https://imgur.com/FaeqNG3 – JConstantine Mar 05 '20 at 13:57
  • This sounds to me like you are actually getting a pointer to a unicode string and not an ANSI string. What happens if you declare the function with return type `String`? As far as I know, in the context of an external function, `String` is marshalled to `PWideChar` (which otherwise isn't accessible in Inno). See also https://stackoverflow.com/a/45349898/1871033 – CherryDT Mar 08 '20 at 22:16
  • @CherryDT How would `string` be marshaled to `PWideChar`? Where would the buffer the pointer points to be stored to? – Martin Prikryl Mar 09 '20 at 06:20
  • Actually the other way round since it's a return value. If this function _returns_ a string pointer, as I understood, it's the library which has already allocated a buffer (and hopefully has some logic to later free it...) and Inno would just wstrcpy it into a string of the same size. So I'd think. – CherryDT Mar 09 '20 at 08:45
  • @CherryDT That's it. Delphi imo cannot automatically marshal `String` to `PWideChar`, as it would have to automatically allocate a buffer for each call to the function behind the scenes. What will lead to memory leaks. Such things have to be done explicitly by the function implementation, not implicitly by marshalling. – Martin Prikryl Mar 09 '20 at 08:56
  • That doesn't sound logical. What is the difference between `myStringVariable := "A string which it has to allocate memory for"` and `myStringVariable := externalFunctionThatReturnsAPointerToAPreallocatedBufferThatCanBeCopied()`? – CherryDT Mar 09 '20 at 11:31
  • On Inno's side it should be fine. It's the same as converting `PAnsiChar` to `AnsiString` which also works. – CherryDT Mar 09 '20 at 11:32
  • The issue how the original buffer is supposed to be freed is not Inno's concern at that point. – CherryDT Mar 09 '20 at 11:32
  • @CherryDT Of course, a user of the function (Inno Setup code) does not care. I'm just questioning your assumption that Delphi marshalls `string` result value to `PWideChar`. I do not think that Delphi would do marshaling that will at least result in a memory leak if not even to a crash. – Martin Prikryl Mar 09 '20 at 12:25
  • You are right, I tested it and the comment in the other thread is wrong. The pointer passed to a `String` parameter in an external function is the actual Delphi string pointer (which is also a `PWideChar` because Delphi stores reference counter and string length _before_ the string, and null-terminates the string), so passing wide strings _in_ works. However it crashes when doing the opposite, and I can see why. So we need a different approach here. – CherryDT Mar 09 '20 at 12:50
  • I added an answer which addresses this. – CherryDT Mar 09 '20 at 13:15