Delphi DLL从C#返回字符串.NET 4.5堆损坏,但.NET 4.0可以工作?请解释

本文关键字:NET 工作 解释 损坏 DLL 返回 字符串 Delphi 5堆 | 更新日期: 2023-09-27 18:28:38

我一直在学习如何将非托管DLL导入封送至C#。。。我遇到了一些我不太理解的事情。

在Delphi中,有一个函数从Procedure SomeFunc() : PChar; Stdcall; 返回Result := NewStr(PChar(somestring))

根据我的理解,NewStr只是在本地堆上分配了一个缓冲区。。。SomeFunc正在返回指向它的指针。

在.NET 4.0(客户端配置文件)中,通过C#我可以使用:

[DllImport("SomeDelphi.dll", EntryPoint = "SomeFunc", CallingConvention = CallingConvention.StdCall)]
public static extern String SomeFunc(uint ObjID);

这在Windows7.NET4.0客户端配置文件中运行良好(或者正如David所说,"似乎可以运行")。在Windows8中,它有不可预测的行为,这让我走上了这条路。

所以我决定在.NET 4.5中尝试相同的代码,结果出现了堆损坏错误。好吧,现在我知道这不是正确的做事方式。所以我进一步挖掘:

仍在.NET 4.5 中

[DllImport("SomeDelphi.dll", EntryPoint = "SomeFunc", CallingConvention = CallingConvention.StdCall)]
public static extern IntPtr _SomeFunc();
public static String SomeFunc()
{
    IntPtr pstr = _SomeFunc();
    return Marshal.PtrToStringAnsi(pstr);
}

这很顺利。我(新手)担心的是NewStr()已经分配了这个内存,它只是永远坐在那里。我的担心是无效的吗?

在.NET 4.0中,我甚至可以做到这一点,而且它从不抛出异常:

[DllImport("SomeDelphi.dll", EntryPoint = "SomeFunc", CallingConvention = CallingConvention.StdCall)]
public static extern IntPtr _SomeFunc();
public static String SomeFunc()
{
    String str;
    IntPtr pstr = _SomeFunc();
    str = Marshal.PtrToStringAnsi(pstr);
    Marshal.FreeCoTaskMem(pstr);
    return str;
}

然而,这段代码在4.5中抛出了相同的堆异常。这让我相信问题在于这样一个事实:在.Net 4.5中,封送拆收器正在尝试FreeCoTaskMem(),而这正是抛出异常的原因。

所以问题:

  1. 为什么这在.Net 4.0中有效,而在4.5中无效?

  2. 我应该关心NewStr()在本机DLL中的分配吗?

  3. 如果对#2的回答是"否",那么第二个代码示例是否有效?

Delphi DLL从C#返回字符串.NET 4.5堆损坏,但.NET 4.0可以工作?请解释

在文档中很难找到的关键信息涉及编组器如何处理返回值为字符串类型的p/invoke函数。返回值被映射到一个以null结尾的字符数组,即Win32术语中的LPCTSTR。到目前为止还不错。

但是marshaller也知道字符串一定是在某个堆上分配的。而且由于本机函数已经完成,它不能期望本机代码解除分配它。所以封送器将其释放。它还假设使用的共享堆是COM堆。因此,marshaller对本机代码返回的指针调用CoTaskMemFree。这就是导致你犯错的原因。

结论是,如果您希望在C#p/invoke端使用字符串返回值,则需要在本机端匹配该值。为此,返回PAnsiChar或PWideChar,并通过调用CoTaskMemAlloc来分配字符数组。

你绝对不能在这里使用NewStr。事实上,您永远不应该调用该函数。您现有的代码被全面破坏,您对NewStr的每次调用都会导致内存泄漏。

一些简单的示例代码将起作用:

Delphi

function SomeFunc: PAnsiChar; stdcall;
var
  SomeString: AnsiString;
  ByteCount: Integer;
begin
  SomeString := ...
  ByteCount := (Length(SomeString)+1)*SizeOf(SomeString[1]);
  Result := CoTaskMemAlloc(ByteCount);
  Move(PAnsiChar(SomeString)^, Result^, ByteCount);
end;

C#

[DllImport("SomeDelphi.dll")]
public static extern string SomeFunc();

为了方便起见,您可能希望将本机代码封装在助手中。

function COMHeapAllocatedString(const s: AnsiString): PAnsiChar; stdcall;
var
  ByteCount: Integer;
begin
  ByteCount := (Length(s)+1)*SizeOf(s[1]);
  Result := CoTaskMemAlloc(ByteCount);
  Move(PAnsiChar(s)^, Result^, ByteCount);
end;

另一种选择是返回一个BSTR并在C#端使用MarshalAs(UnmanagedType.BSTR)。但是,在这样做之前,请阅读以下内容:为什么WideString不能用作interop的函数返回值?


为什么你在不同的.net版本中看到不同的行为?很难说。你的代码在这两个方面都被破坏了。也许较新的版本更善于检测此类错误。也许还有其他区别。您是否在同一台机器、同一操作系统上同时运行4.0和4.5。也许您的4.0测试运行在一个旧的操作系统上,该操作系统不会因COM堆损坏而引发错误。

我的观点是,理解为什么损坏的代码似乎有效是没有意义的。代码已损坏。修复它,继续前进。

我的几点:

  1. 首先,Marshal.FreeCoTaskMem用于释放COM分配的内存块!它不能保证适用于Delphi分配的其他内存块。

  2. NewStr被弃用(我在谷歌上搜索后得到了这个):

    NewStr(const S:string):PString;不赞成;

我的建议是,您还可以导出一个DLL函数来执行字符串释放,而不是使用FreeCoTaskMem。