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)
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:
PAnsiChar
. Or PWideChar
if you marshal the C# return value as LPWStr
. You'll need to free the pointer by calling CoTaskMemFree
StringBuilder
on the C# side. And passing the length of the buffer. 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