从PInvoke返回一个字符串?

12

我正在使用PInvoke技术实现本机代码(C++)和托管代码(C#)之间的互操作。我编写了一个简单的函数,该函数从C++代码中获取字符串。我的代码如下:

C# 代码:

[DllImport("MyDll.dll")]
private static extern string GetSomeText();
public static string GetAllValidProjects() {
    string s = GetSomeText();
    return s;
}

C++ 代码

char* GetSomeText() {
    std::string stri= "Some Text Here";
    char * pchr = (char *)stri.c_str();
    return pchr;
}

在C++方面一切正常,即变量pchr 包含 "Some Text Here",但在C#中,字符串s中没有任何内容。我不知道哪里出了问题。欢迎提供任何帮助。


可能是重复的问题 https://dev59.com/enVC5IYBdhLWcg3w2lGI - Stecya
完全可以理解为什么您没有找到原始内容,因为标题没有重叠。这篇文章不应该被删除,这样其他人在未来的搜索中就可以找到它(以及它现在链接到的原始内容)。 - Bill the Lizard
4个回答

22
首先,正如其他人所指出的那样,在尝试交互之前,你的 C++ 代码就已经有问题了。你正在返回一个指向 stri 的缓冲区的指针。但是因为 stri 在函数返回时被销毁,返回值无效。
更重要的是,即使你修复了这个问题,你还需要做更多的工作。在你的 C++ 代码中分配内存是行不通的,因为你需要让 C# 代码来释放它。
有几种正确的方法可以解决这个问题。
你的 C# 代码可以询问 C++ 代码字符串的长度。然后创建一个 C# StringBuilder 对象,并将其分配到适当的大小。接下来,将 StringBuilder 对象传递给 C++ 代码,其默认的 marshalling 是 LPWSTR。在这种方法中,C# 代码分配字符串,而你的 C++ 代码接收到一个 C 字符串,必须将其复制到缓冲区中。
另一种方法是从 C++ 返回 BSTR,允许在本地 C++ 代码中分配内存,并在 C# 代码中释放。
我会选择使用 BSTR 方法。代码如下:
#include <comutil.h>
BSTR GetSomeText()
{
    return ::SysAllocString(L"Greetings from the native world!");
}

C#(读作C sharp)
[DllImport(@"test.dll", CallingConvention = CallingConvention.Cdecl)]
[return: MarshalAs(UnmanagedType.BStr)]
private static extern string GetSomeText();

更新

在评论区,Hans Passant提供了几个有用的观察。首先,大多数P/Invoke互操作都是针对一个无法更改且您无法选择首选互操作接口的现有接口进行的。看起来这里并非如此,那么应选择哪种方法?

选项1是在托管代码中分配缓冲区,在询问本地代码需要多少空间后再进行。也许使用双方都同意的固定大小缓冲区就足够了。

选项1失败的地方在于当组装字符串很昂贵而且您不想做两次时(例如一次返回其长度,一次获取其内容)。这就是选项2 BSTR 的作用所在。

Hans指出了BSTR的一个缺点,即它携带着UTF-16有效负载,但您的源数据很可能是char*,这是“有点麻烦”的问题。

为了克服这个问题,您可以这样封装从char*转换为BSTR

BSTR ANSItoBSTR(char* input)
{
    BSTR result = NULL;
    int lenA = lstrlenA(input);
    int lenW = ::MultiByteToWideChar(CP_ACP, 0, input, lenA, NULL, 0);
    if (lenW > 0)
    {
        result = ::SysAllocStringLen(0, lenW);
        ::MultiByteToWideChar(CP_ACP, 0, input, lenA, result, lenW);
    } 
    return result;
}

那个最难的已经解决了,现在很容易添加其他包装器来将 LPWSTRstd::stringstd::wrstring 等转换为 BSTR

嘿,你没有被踩。你可能想要提一下这在C++中是未定义行为。 - Hans Passant
@Hans 谢谢。我已经更新了。不仅没有被踩,另一个答案还被赞了! - David Heffernan
@Hans 谢谢。毫无疑问,这不是正常情况,因为所呈现的代码甚至在纯本地环境下都无法工作。关于BSTR和C++的问题在于char*是ANSI而BSTR是宽字符? - David Heffernan
@Hans,不管怎样,你的提示激励我提供了一个克服“BSTR”问题的解决方案! - David Heffernan
如果您从中获得连接器错误,请尝试改用 #include <comdef.h> - Cameron MacFarland
显示剩余3条评论

2

从C++获取字符串的另一种方法。如果您无法修改您的C++ dll,则可以使用IntPtr而不是string声明DllImport。当调用函数时,您可以将Ptr转换回String。

[DllImport("MyDll.dll")]
private static extern IntPtr GetSomeText();
public static string GetAllValidProjects()
{
    string s = Marshal.PtrToStringAnsi(GetSomeText());
    return s;
}

注意:如前一篇文章所述,“Some Text Here”被分配在堆栈上,因此一旦函数返回,堆栈将解除。因此数据有可能被覆盖。因此,在调用后应立即使用Marshal.PtrToStringAnsi。不要保持IntPtr。
char* GetSomeText()    
{
    std::string stri= "Some Text Here"; 
    char * pchr = (char *)stri.c_str();
    return pchr;
} 

4
请不要主张在释放内存后继续使用它。您正在使用托管语言,并且一旦从此函数返回,垃圾回收器可能会决定收集内存、移动数据等操作。如果在您的 Marshal 调用之前发生这种情况,您将读取到垃圾数据,而您无能为力。请注意,在这种情况下,请勿修改原始意思。 - anisoptera

-1

这是因为

std::string stri= "Some Text Here";

这是一个堆栈对象,在 GetSomeText() 调用后被销毁。调用后,您返回的 pchr 指针无效。

您可能需要动态分配空间来访问文本。
将 C++ 函数定义更改为:

char* GetSomeText()    
{
   std::string stri = "Some Text Here";
   return strcpy(new char[stri.size()], stri.c_str());
}

就像上面那样。你明白的...


1
@David 谢谢你的回复。返回本地 std::string 的 c_str() 结果是无效的做法。http://www.cplusplus.com/reference/string/string/c_str/ - Hovhannes Grigoryan
我不是 .net 专家。但它应该可以工作,你肯定会遇到内存管理问题,而且这不是一个“好”的解决方案,但我期望你 在 C# 代码中得到文本!希望 @Jame 很快能确认它。 - Hovhannes Grigoryan
4
它无法正常工作,XP中存在无法诊断的内存泄漏,在Vista及以上版本中会崩溃。希望原贴作者没有使用XP,否则他会确认它"可以"工作。 P/Invoke驱动无法释放字符串,因为它不使用相同的CRT。尽管如此,它还是会尝试使用CoTaskMemFree()来释放。 - Hans Passant
没问题,伙计们。感谢提供信息。我从C++的角度回答了问题,因为我从未使用过C#,以免返回无效指针。你最好知道这里的C#方面问题。所以这是一个内存泄漏,但它在XP上可以工作? :) - Hovhannes Grigoryan
不,它在任何地方都不能工作。它只是可能在XP上运行。 - David Heffernan
显示剩余3条评论

-4

如果您声明一个字符串变量来存储它,那么GetSomeText()返回char指针不起作用。

尝试一下:

[DllImport("MyDll.dll")]
private static extern string GetSomeText();
public static string GetAllValidProjects()
{
    string s = new string(GetSomeText());
    return s;
}

有一个接受char*参数的构造函数


The pinvoke marshaller 无法处理 C++ 对象。 - Hans Passant
泄漏内存(而且不工作)。 - mheyman

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