ElektroStudios
ElektroStudios

Reputation: 20474

How to handle a string returned by a .NET exported function in Pascal Script (Inno Setup)?

I’ve created a .NET DLL with exported functions using DllExport, and I would like to call one of these exported functions from the Pascal Script in Inno Setup to retrieve their return value.

Please note that I am using Inno Setup v6.3.3 Unicode. Also, note that I don't have any inconvenient or difficulties if someone prefers to write a code written in C# to explain a solution or required changes to realize in the following .NET code, unless that code does use pointers.

Here's my VB.NET code for the exported functions:

Imports System.Runtime.InteropServices

Public Class InnoHelperAPI

    Private Sub New()
    End Sub

    <DllExport(CallingConvention:=CallingConvention.StdCall, ExportName:=NameOf(GetInnoHelperVersionStr))>
    Public Shared Function GetInnoHelperVersionStr() As String
        Dim version As String = My.Application.Info.Version.ToString(4)
        Return version
    End Function

    <DllExport(CallingConvention:=CallingConvention.StdCall, ExportName:=NameOf(GetInnoHelperVersionPtrA))>
    Public Shared Function GetInnoHelperVersionPtrA() As IntPtr
        Return Marshal.StringToHGlobalAnsi(GetInnoHelperVersionStr)
    End Function

    <DllExport(CallingConvention:=CallingConvention.StdCall, ExportName:=NameOf(GetInnoHelperVersionPtrW))>
    Public Shared Function GetInnoHelperVersionPtrW() As IntPtr
        Return Marshal.StringToHGlobalUni(GetInnoHelperVersionStr)
    End Function

End Class

As shown, I've made three different functions:

Function Name Return Type
GetInnoHelperVersionStr Unicode String (UTF-16 Little Endian)
GetInnoHelperVersionPtrA Address in memory that points to Ansi String
GetInnoHelperVersionPtrW Address in memory that points to Unicode String

I've made these three variants just because I'm not sure which one would be the proper / most convenient to call from Pascal-Script, but once this doubt is solved I pretend to keep only that one and delete the other two.

Now, I am trying to use any of these functions in Inno Setup, but I'm unsure how to properly handle the return type of any of them. I'm always getting errors trying different aproachs.

Specifically, I would like to achieve just one of these objectives:

The following code is one of the simplest approaches I've tried:

[Code]
function GetInnoHelperVersionStr: String;
  external 'GetInnoHelperVersionStr@files:InnoHelper.dll stdcall setuponly';

procedure CurPageChanged(CurPageID: Integer);
var
version: String;

begin
  version := GetInnoHelperVersionStr();
  MsgBox('Version: ' + version, mbInformation, MB_OK);

end;

However, this results in a Runtime Error describing a memory read/write access violation when I call the function at line: version := GetInnoHelperVersionStr();.

enter image description here

In short, I am stuck on converting the return type to a Pascal string. I am also not sure if I should use AnsiString type because InnoSetup is Unicode, and how to properly free the memory allocated by Marshal.StringToHGlobalAnsi or Marshal.StringToHGlobalUni after using the string in Pascal Script.

Could someone please guide me on the correct way to handle the type returned by the .NET exported functions (just one of them)?. Thank you in advance for your help!.

RESEARCH


In this commentary someone wrote:

The correct way for a function to "return" string data is the same way as windows api functions do it:

function ReturnsAString(aStr: P(Ansi)Char; var aLength: integer): LongBool;

On entry, aStr must point to an existing externally allocated buffer and aLength must be the length of that buffer in characters.

If the string you want to return fits into the passed buffer (remember the additional 1 character for a terminating #0), return true and set aLength to the length of the string written into aStr.

If the string is too large, return false and set aLength to the length that would be required to hold the full string.

In case of this is correct, and once I've adapted my .NET function to accomplish what that commentary describes, it could look like this:

<DllExport(CallingConvention:=CallingConvention.StdCall, ExportName:=NameOf(GetInnoHelperVersionStr))>
Public Shared Function GetInnoHelperVersionStr(buffer As IntPtr, ByRef bufferLength As Integer) As Boolean

    Dim version As String = My.Application.Info.Version.ToString(4)
    If bufferLength < version.Length + 1 Then
        ' Set bufferLength to the length that would be required to hold the full string.
        bufferLength = version.Length
        Return False
    End If

    Dim versionBytes As Byte() = Encoding.Unicode.GetBytes(version)
    Marshal.Copy(versionBytes, 0, buffer, versionBytes.Length)

    ' Write the null char at the end of the string.
    Marshal.WriteByte(buffer, versionBytes.Length, 0)
    Return True
End Function

However, I don't know how to call a function like this within Pascal-Script, I mean a exported function that takes a memory address as parameter to fill it with a (Unicode?) string.

If I try that .NET code adaptation with what @Martin Prikryl suggested in this answer, like this:

[code]
function GetInnoHelperVersionStr(Buffer: String; BufferLength: Integer): Boolean;
  external 'GetInnoHelperVersionStr@files:InnoHelper.dll stdcall setuponly';

procedure CurPageChanged(CurPageID: Integer);
var
  Buffer: String;
  BufferLength: Integer;

begin
  BufferLength := 128;
  SetLength(Buffer, BufferLength);
  GetInnoHelperVersionStr1(Buffer, BufferLength);
  MsgBox(Buffer, mbInformation, mb_Ok);
end;

It ends throwing a same memory access (read) error.

There is also this answer that explains many details, however both the OP and the person who answered are exposing their points about a code in C/C++, so I'm lost when the person who answered says the function must return const char * type.

Lastly, this answer it seems to explain very important things related to a similar question like mine, however I'm not sure how to put all that in action for my scenario. Also I'm not sure to have understand it right: I really need to create a custom type definition like this person does with PWideChar to be able handle an address in memory that points to a Unicode string, under a Unicode Inno Setup?.

Anyway, I've tried to do it like this:

[code]
type
  PWideChar = Cardinal;

function lstrlenW(str: PWideChar): Cardinal;
  external '[email protected] stdcall';

function lstrcpyW_ToInnoString(strDest: String; strSrc: PWideChar): Integer;
  external '[email protected] stdcall';

function GetInnoHelperVersionStr(): PWideChar;
  external 'GetInnoHelperVersionStr@files:InnoHelper.dll stdcall setuponly';

function GetInnoHelperVersionPtrW: Cardinal;
  external 'GetInnoHelperVersionPtrW@files:InnoHelper.dll stdcall setuponly';

procedure CurPageChanged(CurPageID: Integer);
var
  returnedPointer: PWideChar;
  stringLength: Cardinal;
  innoString: String;

begin
  returnedPointer := GetInnoHelperVersionStr();
  stringLength := lstrlenW(returnedPointer);
  innoString := '';
  SetLength(innoString, stringLength);
  lstrcpyW_ToInnoString(innoString, returnedPointer);
  MsgBox('Version: ' + innoString, mbInformation, MB_OK);
end;

With this approach at least now I don't have an error anymore when calling the exported function, however the string (in innoString variable) is filled with unintelligible characters, probably due a string marshaling issue that I'm still not capable to solve with this approach, which at first glance seems promising.

Upvotes: 1

Views: 71

Answers (1)

ElektroStudios
ElektroStudios

Reputation: 20474

This is a provisional answer containing a string conversion function that does all the job. Credits for the experienced users linked through the url references in my question, I just adapted some code to have some kind of universal function, and added some commentary lines with explanation and example.

// https://learn.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-lstrlenw
Function lstrlenW(str: Cardinal): Cardinal;
  external '[email protected] stdcall';

// https://learn.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-lstrcpyw
Function lstrcpyW(strDest: String; strSrc: Cardinal): Cardinal;
  external '[email protected] stdcall';

// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - //
// This function converts a COM BStr string into a usable string in Inno Setup.          //
//                                                                                       //
// A .NET function must return a COM BStr string, as shown in the following examples:    //
//                                                                                       //
// VB.NET:                                                                               //
// <DllExport(CallingConvention:=CallingConvention.StdCall)>                             //
// Public Shared Function MyFunction() As <MarshalAs(UnmanagedType.BStr)> String         //
//     Return "Hello World"                                                              //
// End Function                                                                          //
//                                                                                       //
// C#:                                                                                   //
// [DllExport(CallingConvention = CallingConvention.StdCall)]                            //
// [return: MarshalAs(UnmanagedType.BStr)]                                               //
// public static string MyFunction() {                                                   //
//     return "Hello World";                                                             //
// }                                                                                     //
//                                                                                       //
// The function can then be imported in Inno Setup as follows:                           //
//                                                                                       //
// function MyFunction(): Cardinal;                                                      //
//   external 'MyFunction@files:MyNetAPI.dll stdcall';                                   //
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - //
Function ConvertBStrToInnoString(BStr: Cardinal): String;
var
  strLength: Cardinal;
  innoString: String;
  
begin
  strLength := lstrlenW(BStr);
  SetLength(innoString, strLength);
  if lstrcpyW(innoString, BStr) <> 0 then begin
    Result := innoString;
  end else begin
    MsgBox('Failed to convert BStr to Inno Setup string.', mbError, MB_OK);
  end;

end;

Upvotes: 0

Related Questions