Search code examples
c#delphidlldelphi-xe2dllexport

Using a C# DLL in Delphi only uses the first function parameter


I use C# DLL Export (UnmanagedExports - https://www.nuget.org/packages/UnmanagedExports) to make my managed C# DLL Accessible to unmanged Code like Delphi. My problem is that only first function parameter is transfered from delphi to the C# dll:

The C# DLL Part

   [DllExport("SomeCall", CallingConvention.StdCall)]
   public static String SomeCall([MarshalAs(UnmanagedType.LPWStr)] String data1, [MarshalAs(UnmanagedType.LPWStr)] String data2)
    { 
             //Data1 is never filled with some string data. 
             String result = WorkWithData(data1);                   
             //Data2 is filled with some string data.
             result += WorkWithData(data2) 
             return result;
    }

The Delphi Part (Calling part):

SomeCall: function(data1: PWideChar; data2: PWideChar;): String StdCall;

procedure DoSomeDLLWork(data1: PWideChar; data2: PWideChar);
var 
 dllCallResult: String;
begin
  dllCallResult := SomeCall(data1,data2);
end

The problem in this case is that only data2 is filled. data1 is never filled. I already tried StdCall and Cdecl.

Edit:

The following thing works (data1 and data2 ist transfered correctly) - return value changed from string to boolean:

C# (DLL Part):

   [DllExport("SomeCall", CallingConvention.StdCall)]
   public static bool SomeCall([MarshalAs(UnmanagedType.LPWStr)] String data1, [MarshalAs(UnmanagedType.LPWStr)] String data2)

Delphi (Caller):

 SomeCall: function(data1: PWideChar; data2: PWideChar;): boolean StdCall;

Now I have to think about a return value or a a buffer to return the result string back to delphi.

Edit2:

I went with David Heffernan's suggestion of using an out parameter:

Delphi:

SomeCall: procedure(data1: PWideChar; data2: PWideChar; var result: PWideChar)StdCall;

C#

   [DllExport("SomeCall", CallingConvention.StdCall)]
   public static bool SomeCall([MarshalAs(UnmanagedType.LPWStr)] String data1, [MarshalAs(UnmanagedType.LPWStr)] String data2, [MarshalAs(UnmanagedType.LPWStr)] out String result)

Solution

  • The problem is the string return value. In Delphi a string is a managed type. Furthermore, such types are given somewhat unusual treatment. They are actually passed as an extra implicit var parameter, after all other parameters. The C# code passes the return value through a register.

    What this means is that the C# function has 2 paramaters but the Delphi function has 3 parameters. That's the mismatch that explains the behaviour.

    In any case returning a string from C# results in a pointer to null terminated array of characters being marshalled. It certainly does not marshal as a Delphi string.

    You've got a few solutions available:

    1. Leave the C# alone and change the Delphi return type to PAnsiChar. Or PWideChar if you marshal the C# return value as LPWStr. You'll need to free the pointer by calling CoTaskMemFree
    2. Change the C# to accept a caller allocated buffer which it populates. That would require StringBuilder on the C# side. And passing the length of the buffer.
    3. Change the C# to use an out parameter of type string, marshalled as UnmanagedType.BStr. That maps to WideString in Delphi.

    The problem with caller allocated buffer is that requires the caller to know how large a buffer to allocate.

    The nuance with BStr/WideString is that Delphi's ABI is not compatible with Microsoft's, see Why can a WideString not be used as a function return value for interop? You can work around this by returning the string as an out parameter rather than the function return value.

    Returning a C# string, marshalled as LPWStr, mapped to PWideChar, leaves you with the task of calling CoTaskMemFree to free the memory. On balance I think I'd select this option. Here is an example of that approach.

    C#

    using System.Runtime.InteropServices;
    using RGiesecke.DllExport;
    
    namespace ClassLibrary1
    {
        public class Class1
        {
            [DllExport]
            [return: MarshalAs(UnmanagedType.LPWStr)]
            public static string Concatenate(
                [MarshalAs(UnmanagedType.LPWStr)] string str1, 
                [MarshalAs(UnmanagedType.LPWStr)] string str2
            )
            {
                return str1 + str2;
            }
        }
    }
    

    Delphi

    {$APPTYPE CONSOLE}
    
    uses
      Winapi.ActiveX; // for CoTaskMemFree
    
    const
      dllname = 'ClassLibrary1.dll';
    
    function Concatenate(str1, str2: PWideChar): PWideChar; stdcall; external dllname;
    
    procedure Main;
    var
      res: PWideChar;
      str: string;
    begin
      res := Concatenate('foo', 'bar');
      str := res;
      CoTaskMemFree(res);
      Writeln(Str);
    end;
    
    begin
      Main;
      Readln;
    end.
    

    Output

    foobar