使用PInvoke调用返回char *的C函数

54

我正在尝试编写一些C#代码来调用一个非托管DLL中的方法。该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();

这个是否正确?如果是,那么如何在C#中将这个IntPtr转换为字符串?

3个回答

88

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

问题在于要如何处理从foo()返回的本地内存。CLR假定直接返回字符串类型的PInvoke函数具有以下两个特性:

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

因此,它将编组字符串,然后对本地内存块调用CoTaskMemFree(...)。除非您实际上使用CoTaskMemAlloc分配了该内存,否则最多会导致应用程序崩溃。

为了在这里获得正确的语义,您必须直接返回一个IntPtr。然后使用Marshal.PtrToString*转换为托管String值。您可能仍然需要释放本地内存,但这取决于foo的实现。


5
提供一个示例会非常有帮助。 - kofifus

33
您可以使用Marshal.PtrToStringAuto方法。
IntPtr ptr = foo();
string str = Marshal.PtrToStringAuto(ptr);

9
这对我有用。我唯一需要做的更改是将PtrToStringAuto更改为PtrToStringAnsi,否则我会得到一些中文字符。 - 3vts

6

当前答案不完整,所以我想在一个地方发布完整的解决方案。返回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++代码返回字符串的不同方法。(链接)


strcpy(lpBuffer, szReturnString); 之后,你不应该将最后一个字符设置为 \0 吗? - kofifus
还有,TestAPIAnsiUsingStringBuilder 是什么? - kofifus
1
.NET 保证为数组新分配的内存将填充为 null,因此无需显式地将最后一个字符设置为 \0。但是,如果您从托管代码接受的数组是脏的,则可能需要这样做。 关于方法名称 - 那是我的错),当然是 extern foo 方法。 - Shpand
1
在cpp中,两种情况都可以/应该返回uiStringLength + 1;,所以if语句是不必要的。此外,我会取消注释extern。感谢您提供有用的代码! - kofifus

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接