1

I try to send a inno-setup string to a C# Dll like in this thread Returning a string from a C# DLL with Unmanaged Exports to Inno Setup script

My problem in this case is that i have the non unicode inno setup and so can't use the widestring. In some delphi forums i read that the string type i should use is widestring. In the above thread it also uses widestring. When i try it with a normal string i receive only one letter from my C# message. EDIT: the alert only shows one letter, the RTFText shows the message with spaces in between Is there a workaround that i could use so that i get the full message ?

Switching to the unicode inno setup would be nice but in the current stage of the development this sadly is not an option.

Since the comments say you need code here it is, it is nothing more than in the above thread mentioned, but maybe i am wrong here ;) .

function GetInformationEx(out message: String):Integer;
external 'GetInformationEx@{src}\data\tools\ZipLib.dll stdcall loadwithalteredsearchpath';

procedure ProgressCallback(progress:Integer);
var 
    AStr: String;
    returnCode : Integer;
begin
WriteDebugString('ProgressCallback called');
    if(progress > pbStateZip.position) then 
    begin
        pbStateZip.position := progress;
        lblState2.Caption  := IntToStr(progress)+' %';
        try
            returnCode := GetInformationEx(AStr);
            if returnCode = 0 then begin
                alert(AStr);
                revProgresses.UseRichEdit := True;
                revProgresses.RTFText := AStr;
            end
        except
            ShowExceptionMessage;
        end;
    end
    if(progress >= 100)then 
    begin
        KillTimer(0,m_timer_ID);
        WriteManufacturerTextFile(ExpandConstant('{app}\projects'));
        FileOperationsAfterExtraction();
    end
WriteDebugString('ProgressCallback leave');
end;

And the C# bit:

[DllExport("GetInformationEx", CallingConvention = System.Runtime.InteropServices.CallingConvention.StdCall)]
public static int GetInformationEx([MarshalAs(UnmanagedType.BStr)] out string strout)
{
    int returnCode = 0; //success
    try
    {
        string something = "ladida\0";
        strout = something;
    }
    catch 
    {
        strout = "";
        returnCode = 1; //Error
    }
    return returnCode; 
}
Community
  • 1
  • 1
Bongo
  • 2,933
  • 5
  • 36
  • 67

2 Answers2

2

Your use of AnsiBStr is wrong. Your C# code is using the COM heap, and the Delphi code is using its own private heap. That's wrong. You have to allocate and deallocate off the same heap.

I think I'd do it like this:

[DllExport("GetInformationEx", CallingConvention = CallingConvention.StdCall)]
public static void GetInformationEx(out IntPtr strout)
{
    try
    {
        strout = Marshal.StringToCoTaskMemAnsi(_setupInformation.ToString());
    }
    catch 
    {
        // not very keen on catch all exception handler, but it's your choice
        strout = IntPtr.Zero; 
    }
}

On the Inno side it would look like this:

procedure CoTaskMemFree(pv: PChar);
  external 'CoTaskMemFree@ole32.dll stdcall';
procedure GetInformationEx(out message: PChar);
  external 'GetInformationEx@{src}\data\tools\ZipLib.dll \
  stdcall loadwithalteredsearchpath';

procedure ProgressCallback(progress: Integer);
var 
  progressInfoPtr: PChar;
  progressInfo: string;
begin
  GetInformationEx(progressInfoPtr);
  if progressInfoPtr <> nil then begin
    progressInfo := progressInfoPtr;
    CoTaskMemFree(progressInfoPtr);

    // do something with progressInfo
  end;
end;

I stripped out lots of the logic to concentrate on the interop. I'm sure you get my point.

I don't know anything about Inno, so some of this may not work. I don't know about the use of pointers, or string conversions, or whether CoTaskMemFree is available. I've written this as if it were Delphi code. If CoTaskMemFree is not available, you can export another function from the DLL that exposes Marshal.FreeCoTaskMem.

TLama
  • 75,147
  • 17
  • 214
  • 392
David Heffernan
  • 601,492
  • 42
  • 1,072
  • 1,490
  • There's no `Pointer` support in Inno Setup (except `PChar`, `PAnsiChar` in Unicode version). But you could use `PChar` for the `CoTaskMemFree` parameter for import (ugly, but the only way). – TLama Feb 26 '15 at 12:08
  • @TLama So what do I need to do to make this work? Can I use `PChar` throughout? – David Heffernan Feb 26 '15 at 12:10
  • Yes, `PAnsiChar` is available (its naming is like in Delphi, `PChar` for ANSI, `PAnsiChar` for Unicode version). But when you'll be importing `CoTaskMemFree`, you will have to use that type for its parameter due to lack of `Pointer` support. – TLama Feb 26 '15 at 12:11
  • I will implement it as you described and see if everything works as expected. But from your explanation it sounds correct. I wasn't aware of the two different heaps and now i get why you stated that my option was incorrect. – Bongo Feb 26 '15 at 15:59
  • OK. What I said here is the same as what Gavin said in the Inno newsgroup. So that's good. – David Heffernan Feb 26 '15 at 16:50
  • @TLama when i use the PChar it won't work. I tried it now for some time but it seems something goes wrong. I put for testing purposes a MessageBox on the first position of GetInformationEx and it won't popup so i guess it won't even call the method properly. Is the PChar the correct choice for this ? In delphi the AnsiString is a pointer (if i can trust the page of the university of rostock) How does Pascal Script handle this ? could i use the AnsiString without any problems, because with the AnsiString the method is called – Bongo Feb 27 '15 at 14:49
1

Apart from the answer of Martin Prikryl i found a different approach, i don't know if this is somewhat problematic but it seems that when i change the unmanaged type to (UnmanagedType.AnsiBStr) it works.

    [DllExport("GetInformationEx", CallingConvention = System.Runtime.InteropServices.CallingConvention.StdCall)]
    public static int GetInformationEx([MarshalAs(UnmanagedType.AnsiBStr)] out string strout)
    {
        int returnCode = 0; //success
        try
        {
            string something = "ladida";
            strout = something;
        }
        catch 
        {
            strout = "";
            returnCode = 1; //Error
        }
        return returnCode; 
    }

If someone knows what the benefits of the different solutions is i would love to read it and give a positive rating

EDIT: I Changed the C# and Inno Code see the example below.

C# Code:

[DllExport("GetInformationEx", CallingConvention = System.Runtime.InteropServices.CallingConvention.StdCall)]
public static int GetInformationEx([MarshalAs(UnmanagedType.AnsiBStr)] out string strout, int length)
{
    int returnLength = length;
    try
    {
        string info = _setupInformation.ToString();
        if (info.Length < 1000) 
        {
            returnLength = info.Length;   
        }
        strout = info.Substring(0, returnLength);
    }
    catch 
    {
        strout = "";
        returnLength = 0;
    }
    return returnLength; 
}

Inno Setup Code:

function GetInformationEx(out message: string; length : Integer):Integer;
external 'GetInformationEx@{src}\data\tools\ZipLib.dll stdcall loadwithalteredsearchpath';

procedure ProgressCallback(progress:Integer);
var 
    progressInfo: String;
    returnCode : Integer;
begin
WriteDebugString('ProgressCallback called');
    if(progress > pbStateZip.position) then 
    begin
        pbStateZip.position := progress;
        lblState2.Caption  := IntToStr(progress)+' %';
        try
            progressInfo := StringOfChar('C', 1000);
            SetLength(progressInfo, GetInformationEx(progressInfo, 1000));
            if(Length(progressInfo) > 0) then begin
                revProgresses.UseRichEdit := True;
                revProgresses.RTFText := progressInfo;
            end
        except
            ShowExceptionMessage;
        end;
    end
    if(progress >= 100)then 
    begin
        KillTimer(0,m_timer_ID);
        WriteManufacturerTextFile(ExpandConstant('{app}\projects'));
        FileOperationsAfterExtraction();
    end
WriteDebugString('ProgressCallback leave');
end;

The difference here is that i allocate the memory on the Inno Setup side and just use the string and don't allocate memory on the C# side.

The reason why i use AnsiBStr is that the BStr uses unicode as far as i know. The non unicode Inno Setup doesn't provide a widestring

widestring

And if i fill my Inno Setup string with the BStr i get the following result

enter image description here

instead of

enter image description here

which is, from my little knowledge, due to the encoding.

I asked this question in the Inno Setup newsgroup(http://news.jrsoftware.org/read/article.php?id=29498&group=jrsoftware.innosetup.code#29498) and got the information how to handle the interop correctly.

Bongo
  • 2,933
  • 5
  • 36
  • 67
  • @Bongo You don't appear to be very clear on what is going on here. You've accepted an answer that cannot work and this answer is also surely wrong. You've still not shown enough code because we can only see one side of the interop. We cannot see the Pascal declaration of `GetInformationEx`. Anyway, since you've accepted an answer, it seems that you think you have solved the problem, and I'll leave you to it. – David Heffernan Feb 17 '15 at 11:56
  • It worked the first time when i used his C# approach but now it isn't ... maybe i forgot to copy the DLL afterwards, my bad. Unchecked it and leave it to him to delete or edit it. – Bongo Feb 17 '15 at 12:24
  • @DavidHeffernan i added the declaration and removed the wrongly 'accepted answer check'. so that this question may be helpfull to others. – Bongo Feb 17 '15 at 12:29
  • This answer is wrong too. How can you expect AnsiBStr to match up with Pascal string? – David Heffernan Feb 17 '15 at 12:38
  • @DavidHeffernan since it works i would say you are mistaken. I am able to get the string back into my inno setup. I could create a testproject and provide it to prove it. – Bongo Feb 17 '15 at 12:43
  • Ok, well I guess I'll have to bow to your superior knowledge of reverse p/invoke. I can't see how the string could possible be marshalled from AnsiBStr to pascal string, but I guess you understand that. – David Heffernan Feb 17 '15 at 12:49
  • @DavidHeffernan Is it plausible that this works by luck? He's not allocating the string before passing it in as he should, (it's a `nil` pointer), and he's not freeing the `out` parameter string before re-assigning it (as he should). `AnsiBStr` has only a single byte for length but assuming the string is less than 255 chars and the byte is null padded and aligned might it work by chance? There could be anything where the reference count should be, of course, so it would need luck that this is not valid and trashed and the string almost certainly wouldn't be freed, causing a memory leak... – J... Feb 17 '15 at 13:34
  • @DavidHeffernan not, of course, by any means suggesting that this is the correct approach, however - it is plainly wrong, I'm just digging for ways that it might *seem* to work while nevertheless being very wrong indeed. – J... Feb 17 '15 at 13:35
  • @DavidHeffernan I am sitting here with another developer and try to understand that. One of the tests i ran into which truely surprises me is that i can fill the string with 50kbytes and it doesn't crash. Since the inno setup uses AnsiString inside it seemed clear that i had to use AnsiBStr instead of BStr. That a widestring marshals to BStr is clear to me because of the size and the Null termination. My absence of writing the explanation to this "working" example is only because i don't understand it myself. – Bongo Feb 17 '15 at 13:53
  • @Bongo You can declare a `WideString` type in Delphi for interop purposes, even in non-unicode delphi. No problem casting it to/from a standard delphi `string` at the boundary (without data loss as long as you are sticking to standard ANSI characters if using non-unicode Delphi). – J... Feb 17 '15 at 15:36
  • @DavidHeffernan i provided a different example. It would be nice if you could look over it and tell me if i missed something else and tell me your opinion – Bongo Feb 26 '15 at 11:47