C++动态链接库与C#调用

4

我有一个用C++编写的函数,调用了一个COM接口的函数。

函数签名:

BOOL func(LPWSTR strIn, __out LPWSTR strOut)
{
  //initcom
  //do something
  // release pointers
}

在C#中:

[DllImport("funcdll.dll")]
static extern bool func(String strIn, ref String strOut);

// use it

for(int i=0;i<10;i++)
{
   if(func(strin, strout))
   {
      //do something with strout
   }
}

我已经在C++的控制台应用程序中测试了我的dll,它可以正常运行,但在C#中却出现了一个未知错误导致崩溃。


你尝试过添加一些调试输出或日志,看看在函数的哪个位置它崩溃了吗? - Tommy Andersen
5个回答

7
您有三个问题需要解决:
  1. 调用约定不匹配。您的C++代码使用cdecl,而您的C#代码使用stdcall。
  2. C++代码使用宽字符串,但C#代码使用ANSI字符串进行编组。
  3. 第二个参数不匹配。您的C#代码假设C++代码返回一个指向新的C字符串的指针,然后C#代码使用COM分配器释放该指针。但是您的C++代码没有这样做。
现在,让我们更详细地处理这些问题。 调用约定 这很容易解决。只需将C++代码更改为stdcall或将C#代码更改为cdecl即可。但不要两者都更改。我会更改C#代码:
[DllImport("funcdll.dll"), CallingConvention=CallingConvention.Cdecl]

Unicode/ANSI字符串

我认为您想要使用Unicode字符串,因为您在C++代码中明确选择了它们。但是P/invoke默认使用ANSI字符串进行封送。您可以在DllImport中再次更改如下:

[DllImport("funcdll.dll"), CallingConvention=CallingConvention.Cdecl, 
    CharSet=CharSet.Unicode]

将字符串从C++返回给C#

您当前的C++函数声明如下:

BOOL func(LPWSTR strIn, __out LPWSTR strOut)
__out装饰器除了记录您希望修改由strOut指向的缓冲区并将这些修改返回给调用者外,没有任何实际作用。
您的C#声明如下:
static extern bool func(String strIn, ref String strOut);

现在,ref String strOut与之不匹配。在C++中,ref字符串参数与此匹配:
BOOL func(LPWSTR strIn, LPWSTR *strOut)

换句话说,C#代码期望你返回一个新指针。实际上,它会通过调用CoTaskMemFree来释放你在strOut中返回的缓冲区。我相信这不是你想要的。
你原始的C++代码只能通过修改传递给它的缓冲区来将字符串返回给C#代码。代码如下:
BOOL func(LPWSTR strIn, __out LPWSTR strOut)
{
    ...
    wcscpy(strOut, L"the returned string");
    ...
}

如果您想要这样做,那么您应该在C#中的一个StringBuilder对象中分配足够的缓冲区。
[DllImport("funcdll.dll"), CallingConvention=CallingConvention.Cdecl, 
    CharSet=CharSet.Unicode]
static extern bool func(string strIn, StringBuilder strOut);
...
StringBuilder strOutBuffer = new StringBuilder(128);
bool res = func("input string", strOutBuffer);
string strOut = StringBuilder.ToString();

如果您在 C# 代码中无法确定需要多大的缓冲区,那么最好使用 BSTR 来转换 strOut。详情请参见此答案


谢谢,我无法想象你在第三点中写了什么。我期望C++函数在被调用后返回一个布尔值和传入的参数的引用。如果它没有这样做,那么我会发出警报消息,因为我无法得到我想要的东西。现在我正在寻找另一个解决方案。 - Aussay Marshal
现在我在一台真正的机器上,我将用一些代码来详细说明。但基本上函数签名仅仅不匹配。 - David Heffernan

3

没有看到您的C++方法的细节,很难给出准确的答案。但是,在使用P/Invoke时,我从来没有用String取得过什么好结果。

尝试使用IntPtr代替String,并使用Marshal.PtrToStringUni将输出字符串转换为托管代码,并使用Marshal.StringToHGlobalUni将托管字符串转换为非托管代码。在函数调用后,一定要使用Marshal.FreeHGlobal释放非托管字符串。

此外,由于我的布尔值在C99中无法与.NET布尔值兼容...我不知道为什么。 我必须使用byte并检查== 1。不知道您是否会在C++中遇到这个问题...但是,如果您想听听我的建议,请参考以下内容:

[DllImport("funcdll.dll")]
static extern byte func(IntPtr strIn, out IntPtr strOut);

// use it
string myString = "Testing";

IntPtr stringOut;
IntPtr stringIn = Marshal.StringToHGlobalUni(myString);

   if(func(stringIn, out stringOut) == 1)
   {
      //do something with strout
      string stringOutValue = Marshal.PtrToStringUni(stringOut);
      // Depending on how you dealt with stringOut in your
      // unmanaged code, you may have to: Marshal.FreeCoTaskMem(stringOut);
      // or Marshal.FreeHGlobal(stringOut) if you're returning
      // an owning reference to a copied string.
   }

Marshal.FreeHGlobal(stringIn);

3
这基本上是错误的建议。在这里使用IntPtr只会在C#方面创建不必要的复杂性。避免这种复杂性很容易。 - David Heffernan
嗯...对我来说,它一直很可靠, 并且从非托管的角度来看,很容易理解发生了什么,而不像其他一些“神奇”的可能性。我认为称其为“错误建议”可能有点过分了。 - Steve
当然它能工作,但这是不必要的复杂性。很容易在没有IntPtr的情况下编排字符串。你把这称为魔法只是表明你还没有掌握这样做的技巧。这取决于你自己。 - David Heffernan
如果它能够正常工作,那么它绝对不是“错误的建议”。也许有些“次优”,但远非“错误”。你为什么这么暴躁? - Steve
不,我会说那是错误的建议。要正确编写代码,你真的需要使用try/finally,而你没有这样做。你对第二个参数的处理非常不完整。根本没有必要增加这种复杂性。关于bool的说法也是错误的。C# bool 的默认封送是BOOL。这里没有 C99 的 bool。我并不易怒,我只是认为给出最好的建议非常重要。我认为这个回答并没有达到这一目的。 - David Heffernan
样例代码常为简洁和明晰而省略了try/catch。确保理解并且按照标准严格应用这些概念,是样例代码使用者的责任。当我回答时,没有其他人回答 - 所以我只是想帮忙...如果你对此有问题,那太糟了 - 我很高兴你做了你自己的回答。那看起来是适当的反馈。就我个人而言,我非常乐意使用我的IntPtrs。我认为它们并没有增加多少复杂性 - 而且它总是在一个互操作层中的一个方法内完成的。没什么了不起的。 - Steve

2

我认为你遇到了调用约定不匹配的问题。C++的默认调用约定是cdecl,而.NET的默认调用约定是stdcall。尝试:

[DllImport("funcdll.dll", CallingConvention = CallingConvention.Cdecl)] 
static extern bool func(String strIn, ref String strOut); 

同时,您可能需要明确告诉 marshallar 您希望使用 [MarshalAs(UnmanagedType.LPWStr)] 属性将字符串作为 LPWSTR 进行编组。

[DllImport("funcdll.dll", CallingConvention = CallingConvention.Cdecl)] 
static extern bool func([MarshalAs(UnmanagedType.LPWStr)]String strIn
                       ,[MarshalAs(UnmanagedType.LPWStr)]ref String strOut);

请查看http://msdn.microsoft.com/en-us/library/s9ts558h.aspx

在 DllImport 属性中使用 CharSet 要比单独使用 MarshalAs 更简单。 - David Heffernan
2
我认为这两者并不相等,将字符集设置为Unicode只是告诉它该字符串是Unicode编码,Bstr和LPWStr都是Unicode编码,但你仍然不知道具体是哪一个。在我的看法中,使用MarshalAs显式设置会更好。 - Phyx
1
至于第二部分,是的,那是个问题。当我第一次阅读这个问题时,我没有看到它。但是自从什么时候LPWSTR(可能是以null结尾,可能是长度前缀)和BStr(始终以null结尾和长度前缀)相等了呢? - Phyx
感谢Phyx的帮助,但它仍然无法正常工作。我遇到了相同的崩溃和无法调试的问题。"您的...exe遇到了问题,需要停止运行...是否向MS发送消息?是或否?等等" - Aussay Marshal
@Assay 这是因为David对于ref变量是正确的,当marshaller在尝试释放字符串时会崩溃。这个问题有点难以解决,取决于你想在哪里进行分配,就像他所说的那样。如果你在C#中分配所有内容,那么可以尝试使用StringBuilder替代。 - Phyx
显示剩余3条评论

2
在C++中,请确保你有类似这样的内容(至少对于第二个参数)
extern "C" BOOL __stdcall func( BSTR * pBstr )
{
    *pBstr = SysAllocString( L"Foobar" );
    return 0;
}

在C#中,可以像这样编写代码(对于第二个参数):
static extern bool func( [MarshalAs(UnmanagedType.BStr)] ref String strOut); 

+1 BSTR是第二个参数的绝佳选择,特别是在调用代码中无法确定缓冲区大小时。 - David Heffernan

1

很抱歉我没有一个大的答案,但是我从我的经验中记得一些东西.. 你是否尝试使用StringBuilder,例如改变你的C#导入函数签名?

   [System.Runtime.InteropServices.DllImport("funcdll.dll")]
static extern bool func(String strIn, System.Text.StringBuilder strOut);

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