5

我一直在学习如何将非托管 DLL 导入编组到 C# 中......而且我遇到了一些我不太明白的事情。

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

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

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

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

这在 Windows 7 .NET 4.0 客户端配置文件中可以正常工作(或者正如 David 所说,“似乎可以工作”)。在 Windows 8 中,它具有不可预测的行为,这使我走上了这条道路。

所以我决定在 .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 回答“否”,那么第二个代码示例是否有效?

4

2 回答 2

11

文档中很难找到的关键信息涉及编组器如何处理具有字符串类型返回值的 ap/invoke 函数。返回值映射到一个以空字符结尾的字符数组,即 Win32 术语中的 LPCTSTR。到现在为止还挺好。

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

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

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

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

德尔福

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 用作互操作的函数返回值?


为什么您会在不同的 .net 版本中看到不同的行为?很难确定。您的代码在两者中都一样坏。也许较新的版本更擅长检测此类错误。也许还有一些其他的不同。您是否在同一台机器上运行 4.0 和 4.5,相同的操作系统。也许您的 4.0 测试在较旧的操作系统上运行,该操作系统不会因 COM 堆损坏而引发错误。

我的观点是,理解为什么损坏的代码似乎有效是没有意义的。代码被破坏了。修复它,然后继续。

于 2013-08-23T05:39:32.193 回答
1

我的几点:

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

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

    NewStr(const S: string): PString; 已弃用;

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

于 2013-08-22T20:34:18.923 回答