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)
7
  • 1
    String return value is questionable. Remove that and make the function void, or return int error code, or bool success flag. Commented Oct 21, 2015 at 6: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? Commented Oct 21, 2015 at 6: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. Commented Oct 21, 2015 at 6: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 minimal reproducible example. Commented Oct 21, 2015 at 6: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. Commented Oct 21, 2015 at 6:39

3 Answers 3

5

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
Sign up to request clarification or add additional context in comments.

3 Comments

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.
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.
@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.
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.

6 Comments

@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 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.
Yes, I understand the reason why it is so. That's all covered at the question I linked to.
IMHO it is easier to use automatic marshalling of BSTR/WideString than relying on LPWStr and manual memory allocation.
To avoid confusion I think it is clearer to declare the Delphi side as a three parameter procedure.
|
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)

5 Comments

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.
no, if I with other function release this string. In C# return string must be global.
No. It's explained in my answer. string is marshalled as pointer to null terminated array of character, allocated on the COM heap.
I will try test leter, C# is on other computer.
David, You have right... In my case work because return string do not change until end program and must exist until end program

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.