【问题标题】:Using a C# DLL in Delphi only uses the first function parameter在 Delphi 中使用 C# DLL 只使用第一个函数参数
【发布时间】:2015-10-21 06:18:52
【问题描述】:

我使用 C# DLL 导出 (UnmanagedExports - https://www.nuget.org/packages/UnmanagedExports) 使我的托管 C# DLL 可以访问像 Delphi 这样的非托管代码。我的问题是只有第一个函数参数从 delphi 传输到 C# dll:

C# DLL 部分

   [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;
    }

Delphi 部分(调用部分):

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

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

这种情况下的问题是只填充了data2。 data1 永远不会被填充。我已经尝试过 StdCall 和 Cdecl。

编辑:

以下事情有效(data1 和 data2 传输正确) - 返回值从字符串更改为布尔值:

C#(DLL 部分):

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

Delphi(调用者):

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

现在我必须考虑一个返回值或一个缓冲区来将结果字符串返回给 delphi。

编辑2:

我同意 David Heffernan 的使用 out 参数的建议:

德尔福:

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)

【问题讨论】:

  • 字符串返回值有问题。删除它并使函数无效,或返回 int 错误代码,或 bool 成功标志。
  • 我会试试看(最终data1会被传输)。但我需要一个字符串作为返回值。我应该选择也使用 PWideChar 作为返回值吗?
  • 想想内存管理。被调用者如何分配内存?谁解除分配。被调用者必须要么导出释放器,要么从共享堆中分配。或者让调用者分配和被调用者填充。
  • FWIW,我很难相信你在问题中声称的内容。似乎更合理的是错误出在调用代码中。你没有显示。或者你没有显示真正的代码。该问题的原始版本显然不包含真实代码。我不知道你为什么不显示minimal reproducible example
  • 您应该传递一个字符串缓冲区来放置要返回的字符串,作为参数。为字符串分配空间的责任应该落在 Delphi 代码上,而不是在 .NET 代码上,但是返回一个字符串会使它成为 .NET 的责任,并且解除分配缓冲区的责任应该在栅栏的同一侧作为分配。传入一个缓冲区,用文本填充它。

标签: c# delphi dll delphi-xe2 dllexport


【解决方案1】:

问题在于string 返回值。在 Delphi 中,string 是托管类型。此外,这些类型被给予了一些不寻常的处理。在所有其他参数之后,它们实际上是作为额外的隐式 var 参数传递的。 C# 代码通过寄存器传递返回值。

这意味着 C# 函数有 2 个参数,但 Delphi 函数有 3 个参数。这就是解释行为的不匹配。

在任何情况下,从 C# 返回一个字符串都会导致一个指向以空字符结尾的字符数组的指针被编组。它当然不会编组为 Delphi 字符串。

您有几个可用的解决方案:

  1. 不理会 C# 并将 Delphi 返回类型更改为 PAnsiChar。或者 PWideChar,如果您将 C# 返回值编组为 LPWStr。您需要通过调用 CoTaskMemFree 来释放指针
  2. 更改 C# 以接受它填充的调用方分配的缓冲区。这将需要 C# 端的StringBuilder。并传递缓冲区的长度。
  3. 更改 C# 以使用类型为 string 的输出参数,编组为 UnmanagedType.BStr。这映射到 Delphi 中的WideString

调用者分配缓冲区的问题在于,调用者需要知道要分配多大的缓冲区。

BStr/WideString 的细微差别在于 Delphi 的 ABI 与 Microsoft 的不兼容,请参阅 Why can a WideString not be used as a function return value for interop? 您可以通过将字符串作为 out 参数而不是函数返回值返回来解决此问题。

返回一个 C# string,编组为 LPWStr,映射到 PWideChar,让您完成调用 CoTaskMemFree 以释放内存的任务。总的来说,我想我会选择这个选项。这是该方法的一个示例。

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;
        }
    }
}

德尔福

{$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.

输出

富吧

【讨论】:

  • 关于您对问题的编辑,您可以在 C# 中使用 string 类型的返回值,只要它在 Delphi 端与 PWideChar 配对,并且您调用 CoTaskMemFree完成指针后释放。我的回答证明了这一点。如果您想返回 COM BStr,也就是 Delphi WideString,则必须使用 out 参数。我在回答中链接到的问题详细说明了原因。
  • 为什么不在 C# 中将 Delphi 函数结果字符串定义为附加输出参数?听起来就是它的实现方式。请参阅我编辑的答案。
  • @ArnaudBouchez 这是一个非常好的选择。一点问题都没有。我没有说明它,因为它很容易做到。我选择了我所做的示例,因为它有些细微差别。
【解决方案2】:

不要使用string 作为结果类型:该类型是Delphi 私有的。

最简单的方法是使用BSTR marshaling,它映射Delphi WideString 类型。

所以你定义

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

可以这样映射:

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

(答案编辑后)技巧是将 Delphi 函数转换为附加的 out 参数,如 Delphi low-level information of parameter marshalling 所述。

【讨论】:

  • @DavidHeffernan 感谢您的反馈。我们确实必须将 Delphi 函数结果作为附加的“out”参数传递。为此,请参阅docwiki.embarcadero.com/RADStudio/Seattle/en/Program_Control 对于字符串、动态数组、方法指针或变体结果,其效果与将函数结果声明为声明参数之后的附加 var 参数一样。
  • 是的,我明白为什么会这样。我链接到的问题都涵盖了这些内容。
  • 恕我直言,使用 BSTR/WideString 的自动编组比依赖 LPWStr 和手动内存分配更容易。
  • 为避免混淆,我认为将 Delphi 端声明为三参数过程会更清楚。
【解决方案3】:

你必须编组返回值

像这样:

[DllExport(CallingConvention = CallingConvention.StdCall)]

    [return: MarshalAs(UnmanagedType.LPWStr)]

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

【讨论】:

  • 您需要做的更多。您必须释放托管代码为字符串分配的内存。我的回答解释了如何做到这一点。
  • 不,如果我用其他函数释放这个字符串。在 C# 中,返回字符串必须是全局的。
  • 没有。我的回答中对此进行了解释。 string 被编组为指向以空字符结尾的字符数组的指针,分配在 COM 堆上。
  • 我试试测试信,C#在其他电脑上。
  • 大卫,你是对的......在我的情况下工作,因为返回字符串在结束程序之前不会改变并且必须存在直到结束程序
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2013-03-12
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多