1

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)
frugi
  • 605
  • 7
  • 26
  • 1
    String return value is questionable. Remove that and make the function void, or return int error code, or bool success flag. – David Heffernan Oct 21 '15 at 06:31
  • I will try that (eventually data1 will be transfered than). But i need a string as return value. Should I choose also use PWideChar as return value? – frugi Oct 21 '15 at 06:35
  • 1
    Think about the memory management. How can it work for callee to allocate memory? Who deallocate it. Callee would have to either export deallocator, or allocate off shared heap. Or have caller allocate and callee populate. – David Heffernan Oct 21 '15 at 06:37
  • FWIW, I find it hard to believe what you claim in the question. Seems more plausible that the fault is in the calling code. Which you did not show. Or that you aren't showing real code. The original version of the question clearly did not contain real code. I don't know why you didn't show a [mcve]. – David Heffernan Oct 21 '15 at 06:38
  • You should pass a string buffer to place the string to return into, as a parameter. The responsibility of allocating the space for the string should fall on the Delphi code, not on the .NET code, but returning a string will make it .NET's responsibility, and the responsibility to deallocate the buffer should be on the same side of the fence as the allocation. Pass in a buffer, fill it with text. – Lasse V. Karlsen Oct 21 '15 at 06:39
  • @LasseV.Karlsen the parameters are now transfered correctly if I change the return parameter. Thx! Now I have to think about the buffer you have mentioned to transfer the return value. – frugi Oct 21 '15 at 07:12
  • @frugi That's exactly what I told you! Caller allocated buffer is fine if you can predict its size. Otherwise BSTR is probably simplest. – David Heffernan Oct 21 '15 at 07:17

3 Answers3

4

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
Community
  • 1
  • 1
David Heffernan
  • 601,492
  • 42
  • 1,072
  • 1,490
  • Regarding your edit to the question, you can use a return value in C# of type `string`, so long as it is paired with `PWideChar` on the Delphi side, and you call `CoTaskMemFree` to deallocate once you are done with the pointer. My answer demonstrates that. Where you have to use an `out` parameter is if you want to return a COM `BStr`, aka Delphi `WideString`. The question I linked to in my answer has the details of why. – David Heffernan Oct 21 '15 at 08:10
  • Why not just define the Delphi function result string as an additional out parameter in C#? Sounds to be how it is implemented. See my edited answer. – Arnaud Bouchez Oct 22 '15 at 08:15
  • @ArnaudBouchez That's a perfectly good option. Nothing wrong with it at all. I didn't illustrate it since it is so easy to do. I picked the example I did because it is somewhat nuanced. – David Heffernan Oct 22 '15 at 08:21
3

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.

Arnaud Bouchez
  • 42,305
  • 3
  • 71
  • 159
  • This won't work: http://stackoverflow.com/questions/9349530/why-can-a-widestring-not-be-used-as-a-function-return-value-for-interop – David Heffernan Oct 21 '15 at 07:27
  • @DavidHeffernan Thanks for the feedback. We indeed have to pass the Delphi function result as an additional "out" parameter. For a reason of that, see http://docwiki.embarcadero.com/RADStudio/Seattle/en/Program_Control *For a string, dynamic array, method pointer, or variant result, the effects are the same as if the function result were declared as an additional var parameter following the declared parameters.* – Arnaud Bouchez Oct 22 '15 at 08:13
  • Yes, I understand the reason why it is so. That's all covered at the question I linked to. – David Heffernan Oct 22 '15 at 08:14
  • IMHO it is easier to use automatic marshalling of BSTR/WideString than relying on LPWStr and manual memory allocation. – Arnaud Bouchez Oct 22 '15 at 08:16
  • To avoid confusion I think it is clearer to declare the Delphi side as a three parameter procedure. – David Heffernan Oct 22 '15 at 08:24
  • @DavidHeffernan You are right: it is a cleaner approach. But since the "last out parameter" trick is only on C# side, it may be not so confusing. – Arnaud Bouchez Oct 22 '15 at 14:39
0

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)
Arnaud Bouchez
  • 42,305
  • 3
  • 71
  • 159
ceha
  • 50
  • 3
  • You need to do more. You have to free the memory that the managed code allocated for the string. My answer explains how to do it. – David Heffernan Oct 21 '15 at 07:27
  • no, if I with other function release this string. In C# return string must be global. – ceha Oct 21 '15 at 07:35
  • No. It's explained in my answer. `string` is marshalled as pointer to null terminated array of character, allocated on the COM heap. – David Heffernan Oct 21 '15 at 07:37
  • I will try test leter, C# is on other computer. – ceha Oct 21 '15 at 07:39
  • 1
    David, You have right... In my case work because return string do not change until end program and must exist until end program – ceha Oct 21 '15 at 08:03