frugi
frugi

Reputation: 605

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)

Upvotes: 1

Views: 3843

Answers (3)

Arnaud Bouchez
Arnaud Bouchez

Reputation: 43053

Do not use string as a result type: this type is private to Delphi.

The easiest is to use BSTR marshaling, which maps the Delphi WideString type.

So you define

SomeCall: function(const aFileName, data2: WideString): WideString; StdCall;

Which may be mapped as such:

[DllExport(CallingConvention = CallingConvention.StdCall)]
public static void AddCertificate([MarshalAs(UnmanagedType.BStr)] string data1, [MarshalAs(UnmanagedType.BStr)] string data2,  [MarshalAs(UnmanagedType.BStr)] out string Result);

The (after answer edition) trick is to convert the Delphi function returned as an additional out parameter, as stated by Delphi low-level information of parameter marshalling.

Upvotes: 3

David Heffernan
David Heffernan

Reputation: 613572

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

Upvotes: 5

ceha
ceha

Reputation: 50

You have to marshal return value

Like this:

[DllExport(CallingConvention = CallingConvention.StdCall)]

    [return: MarshalAs(UnmanagedType.LPWStr)]

    public static string AddCertificate([MarshalAs(UnmanagedType.LPWStr)] string aFileName, [MarshalAs(UnmanagedType.LPWStr)] string aPassword)

Upvotes: 0

Related Questions