【问题标题】:Delphi DLL return string from C# ... .NET 4.5 Heap Corruption but .NET 4.0 works? Explain please?Delphi DLL 从 C# 返回字符串 ... .NET 4.5 堆损坏但 .NET 4.0 有效?请解释?
【发布时间】:2013-08-25 18:10:03
【问题描述】:

我一直在学习如何将非托管 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);

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

【问题讨论】:

    标签: c# .net delphi pinvoke


    【解决方案1】:

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

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

    结论是,如果你想在 C# p/invoke 端使用字符串返回值,你需要在 native 端匹配。为此,返回 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)。但是,在您这样做之前,请阅读以下内容:Why can a WideString not be used as a function return value for interop?


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

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

    【讨论】:

    • 为了记录,它是同一台机器,相同的操作系统,相同的编译器,相同的一切。只需右键单击项目-> 将框架更改为 4.5 和中提琴,它就会崩溃。无论如何,我很高兴我理解正确,并一如既往地感谢您的帮助。
    【解决方案2】:

    我的几点:

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

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

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

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

    【讨论】:

    • 为什么它总是在 .Net 4.0 而不是 4.5 中工作?是 NewStr() 以某种方式在共享堆上分配,还是 4.0 封送拆收器根本不知道发生了什么?
    • 它只是“看起来有效”。即使它适用于 .NET 4.0,也并不意味着它完全合法且正确。使用 FreeCoTaskMem 释放 Delphi 分配的字符串对象是 错误 方式。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2021-12-01
    • 2014-06-25
    • 2015-08-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多