Search code examples
c#delphipinvoke

PInvokeStackImbalance when calling Delphi function from C# application


I am forced to work with unmanaged delphi dll. I dont have an access to the source code. Only vague documentation:

type
  TServiceData = packed record 
    DBAlias: PChar; 
    LicKey: PChar; 
    Pass: PChar; 
  end; 
  PServiceData = ^TServiceData; 

function CreateRole(SrvData: PServiceData; var UserName: PChar): byte; stdcall;

UserName is supposed to be an out param.

My C# code:

[StructLayout(LayoutKind.Sequential, Pack = 1)]
public struct SERVICE_DATA
{
    public string DBALias;
    public string LicKey;
    public string Pass;
}

[DllImport(dllname, CallingConvention = CallingConvention.StdCall)]
public static extern byte CreateRole(SERVICE_DATA data, out string str);    

I have no idea what can cause the stack imbalance (except calling convention which seems to be correct). I dont know if strings in my structure are marshalled correctly but according to other threads this would not cause PStackImbalanceException. Any help will be much appreciated:)

EDIT. I have implemented suggestions from David and now I am getting access violation exception:

"Unhandled Exception: System.AccessViolationException: Attempted to read or write protected memory. This is often an indication that other memory is corrupt”

My structure and method declaration is just copy-pasted from the answer, there is nothing fancy in the way I am calling it:

        string str;
        var data = new SERVICE_DATA();
        data.DBALias = "test";
        data.LicKey = "test";
        data.Pass = "test";
        var result = CreateRole(ref data, out str);

Solution

  • There are a couple of things wrong with the translation:

    1. The Delphi code receives a pointer to the record. The C# code passes it by value. This is the reason for the stack imbalance warning.
    2. The user name parameter is probably incorrectly handled on the Delphi side. It would need to be a pointer to dynamically allocated memory, allocated on the COM heap by a call to CoTaskMemAlloc. I'd guess that you aren't doing that and so you'll hit problems when the marshaller attempts to deallocate the pointer with a call to CoTaskMemFree.

    I'd probably use the COM string type for the strings. I would also avoid packing records because that is bad practise as a general rule.

    I'd write it like this:

    Delphi

    type
      TServiceData = record
        DBAlias: WideString;
        LicKey: WideString;
        Pass: WideString;
      end;
    
    function CreateRole(const SrvData: TServiceData; out UserName: WideString): Byte; 
      stdcall;
    

    C#

    [StructLayout(LayoutKind.Sequential)]
    public struct SERVICE_DATA
    {
        [MarshalAs(UnmanagedType.BStr)]
        public string DBALias;
    
        [MarshalAs(UnmanagedType.BStr)]
        public string LicKey;
    
        [MarshalAs(UnmanagedType.BStr)]
        public string Pass;
    }
    
    [DllImport(dllname, CallingConvention = CallingConvention.StdCall)]
    public static extern byte CreateRole(
        [In] ref SERVICE_DATA data, 
        [MarshalAs(UnmanagedType.BStr)] out string str
    );    
    

    Here is a complete test project to show that this works as expected:

    Delphi

    library Project1;
    
    type
      TServiceData = record
        DBAlias: WideString;
        LicKey: WideString;
        Pass: WideString;
      end;
    
    function CreateRole(const SrvData: TServiceData; out UserName: WideString): Byte;
      stdcall;
    begin
      UserName := SrvData.DBAlias + SrvData.LicKey + SrvData.Pass;
      Result := Length(UserName);
    end;
    
    exports
      CreateRole;
    
    begin
    end.
    

    C#

    using System;
    using System.Runtime.InteropServices;
    
    namespace ConsoleApplication1
    {
        class Program
        {
            const string dllname = @"...";
    
            [StructLayout(LayoutKind.Sequential)]
            public struct SERVICE_DATA
            {
                [MarshalAs(UnmanagedType.BStr)]
                public string DBALias;
    
                [MarshalAs(UnmanagedType.BStr)]
                public string LicKey;
    
                [MarshalAs(UnmanagedType.BStr)]
                public string Pass;
            }
    
            [DllImport(dllname, CallingConvention = CallingConvention.StdCall)]
            public static extern byte CreateRole(
                [In] ref SERVICE_DATA data,
                [MarshalAs(UnmanagedType.BStr)] out string str
            ); 
    
            static void Main(string[] args)
            {
                SERVICE_DATA data;
                data.DBALias = "DBALias";
                data.LicKey = "LicKey";
                data.Pass = "Pass";
                string str;
                var result = CreateRole(ref data, out str);
                Console.WriteLine(result);
                Console.WriteLine(str);
            }
        }
    }
    

    Output

    17
    DBALiasLicKeyPass