54

我正在尝试编写一些从非托管 DLL 调用方法的 C# 代码。dll中函数的原型是:

extern "C" __declspec(dllexport) char *foo(void);

在 C# 中,我首先使用:

[DllImport(_dllLocation)]
public static extern string foo();

它似乎在表面上工作,但我在运行时遇到内存损坏错误。我想我指的是恰好是正确的但已经被释放的内存。

我尝试使用名为“P/Invoke Interop Assistant”的 PInvoke 代码生成实用程序。它给了我输出:

[System.Runtime.InteropServices.DLLImportAttribute(_dllLocation, EntryPoint = "foo")]
public static extern System.IntPtr foo();

它是否正确?如果是这样,如何将此 IntPtr 转换为 C# 中的字符串?

4

3 回答 3

86

您必须将此作为 IntPtr 返回。从 PInvoke 函数返回 System.String 类型需要非常小心。CLR 必须将内存从本机表示转移到托管表示。这是一个简单且可预测的操作。

但问题在于如何处理从foo(). CLR 假设以下两项关于直接返回字符串类型的 PInvoke 函数

  1. 需要释放本机内存
  2. 本机内存是使用 CoTaskMemAlloc 分配的

因此,它将编组字符串,然后调用CoTaskMemFree(...)本机内存 blob。除非您实际上使用 CoTaskMemAlloc 分配了此内存,否则这充其量只会导致您的应用程序崩溃。

为了在此处获得正确的语义,您必须直接返回 IntPtr。然后使用 Marshal.PtrToString* 以获取托管字符串值。您可能仍然需要释放本机内存,但这将取决于 foo 的实现。

于 2008-12-16T05:18:26.393 回答
31

您可以使用 Marshal.PtrToStringAuto 方法。

IntPtr ptr = foo();
string str = Marshal.PtrToStringAuto(ptr);
于 2008-12-15T23:46:22.353 回答
2

目前的答案并不完整,所以我只想在一个地方发布完整的解决方案。返回 IntPtr 而不是 string 根本不能解决任何问题,因为您仍然必须释放 C 脚本中分配的本机内存。最好的解决方案是在托管端分配字节缓冲区并将内存传递给 C 脚本,该脚本将字符串写入该缓冲区而不分配内存。

从 C 中返回一个字符串就像这样:

//extern "C" __declspec(dllexport) uint32_t foo(/*[out]*/ char* lpBuffer, /*[in]*/ uint32_t uSize)
uint32_t __stdcall foo(/*[out]*/ char* lpBuffer, /*[in]*/ uint32_t uSize)
{
  const char szReturnString[] = "Hello World";
  const uint32_t uiStringLength = strlen(szReturnString);

  if (uSize >= (uiStringLength + 1))
  {
    strcpy(lpBuffer, szReturnString);
    // Return the number of characters copied.
    return uiStringLength;
  }
  else
  {
    // Return the required size
    // (including the terminating NULL character).
    return uiStringLength + 1;
  }
}

C#代码:

[DllImport(_dllLocation, CallingConvention=CallingConvention.StdCall, CharSet=CharSet.Ansi)]
private static extern uint foo(IntPtr lpBuffer, uint uiSize);

private static string foo()
{
    // First allocate a buffer of 1 byte.
    IntPtr lpBuffer = Marshal.AllocHGlobal(1);
    // Call the API. If the size of the buffer
    // is insufficient, the return value in
    // uiRequiredSize will indicate the required
    // size.
    uint uiRequiredSize = foo(lpBuffer, 1);

    if (uiRequiredSize > 1)
    {
        // The buffer pointed to by lpBuffer needs to be of a
        // greater size than the current capacity.
        // This required size is the returned value in "uiRequiredSize"
        // (including the terminating NULL character).
        lpBuffer = Marshal.ReAllocHGlobal(lpBuffer, (IntPtr)uiRequiredSize);
        // Call the API again.
        foo(lpBuffer, uiRequiredSize);
    }

    // Convert the characters inside the buffer
    // into a managed string.
    string str = Marshal.PtrToStringAnsi(lpBuffer);

    // Free the buffer.
    Marshal.FreeHGlobal(lpBuffer);
    lpBuffer = IntPtr.Zero;

    // Display the string.
    Console.WriteLine("GetString return string : [" + str + "]");

    return str;
}

使用 StringBuilder 在 C# 端管理内存分配/释放有更简单的方法:

[DllImport("TestDLL.dll", CallingConvention = CallingConvention.StdCall, CharSet = CharSet.Ansi)]
private static extern uint foo(StringBuilder lpBuffer, UInt32 uiSize);

private static string foo()
{
    StringBuilder sbBuffer = new StringBuilder(1);
    uint uiRequiredSize = foo(sbBuffer, (uint)sbBuffer.Capacity);

    if (uiRequiredSize > sbBuffer.Capacity)
    {
        // sbBuffer needs to be of a greater size than current capacity.
        // This required size is the returned value in "uiRequiredSize"
        // (including the terminating NULL character).
        sbBuffer.Capacity = (int)uiRequiredSize;
        // Call the API again.
        foo(sbBuffer, (uint)sbBuffer.Capacity);
    }

    return sbBuffer.ToString();
}

有一个很好的主题解释了从 C/C++ 代码返回字符串的不同方式。

于 2020-04-09T11:27:57.120 回答