Reputation: 20474
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:
Call GetInnoHelperVersionPtrA
or GetInnoHelperVersionPtrW
to convert the returned memory address to a usable string within Pascal Script.
Call GetInnoHelperVersionStr
to directly use the returned Unicode string.
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();
.
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!.
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
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