【问题标题】:PInvoke for C function that returns char *PInvoke 用于返回 char * 的 C 函数
【发布时间】:2010-09-27 01:43:31
【问题描述】:

我正在尝试编写一些从非托管 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# 中的字符串?

【问题讨论】:

    标签: c# pinvoke


    【解决方案1】:

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

    问题在于如何处理从foo() 返回的本机内存。对于直接返回字符串类型的 PInvoke 函数,CLR 假设以下两项

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

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

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

    【讨论】:

    • 这里有一个样本会很有帮助
    【解决方案2】:

    您可以使用 Marshal.PtrToStringAuto 方法。

    IntPtr ptr = foo();
    string str = Marshal.PtrToStringAuto(ptr);
    

    【讨论】:

    • 这对我有用。我必须做的唯一改变是 PtrToStringAutoPtrToStringAnsi 否则我会得到一些中文字符
    【解决方案3】:

    当前的答案并不完整,所以我只想在一个地方发布完整的解决方案。返回 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();
    }
    

    good topic 解释了从 C/C++ 代码返回字符串的不同方式。

    【讨论】:

    • strcpy(lpBuffer, szReturnString); 之后你不应该将最后一个字符设置为 \0 吗?
    • 还有TestAPIAnsiUsingStringBuilder是什么?
    • .net 保证为数组新分配的内存将用空值填充,因此无需将最后一个字符显式设置为 \0。尽管您可能需要这样做,以防您从托管代码接受的数组是脏的。关于方法名——那是我的错)那当然是 extern foo 方法
    • 在 cpp 中,这两种情况都可以/应该return uiStringLength + 1; 所以不需要 if .. 我也不会注释 extern .. 感谢有用的代码!
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-02-18
    • 1970-01-01
    • 1970-01-01
    • 2023-02-16
    相关资源
    最近更新 更多