【问题标题】:C++ dll and C# callC++ dll 和 C# 调用
【发布时间】:2011-10-24 07:20:33
【问题描述】:

我有一个用 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# 中它会因未知错误而崩溃。

【问题讨论】:

  • 您是否尝试过添加一些调试输出或日志记录并查看您的函数在哪里崩溃?

标签: c# c++ pinvoke


【解决方案1】:

我可以看到你有三个问题。

  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 根本不匹配。 ref 字符串参数在 C++ 中与此匹配:

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。有关详细信息,请参阅此answer。

【讨论】:

  • 谢谢,我无法想象你在第三点中写了什么。我希望 C++ 函数返回一个布尔值和一个对参数的引用,该参数在被调用后传入。如果它不这样做,那么我会发出一条警报消息,表示无法获得我想要的东西。我现在正在寻找另一种解决方案。
  • 既然我在一台真机上,我将用一些代码进行详细说明。但基本上函数签名根本不匹配。
【解决方案2】:

如果没有看到您的 C++ 方法的详细信息,我很难说出来......但是,我从来没有运气好使用带有 P/Invoke 的 String。

尝试使用IntPtr而不是String,并使用Marshal.PtrToStringUni作为传出字符串,并使用Marshal.StringToHGlobalUni将托管字符串编组到非托管区域,并在函数调用后确保释放非托管字符串Marshal.FreeHGlobal。

另外,来自 C99 领域,我的布尔值不适用于 .NET 布尔值...我不知道为什么。我必须使用字节并检查 == 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);

【讨论】:

  • 这在很大程度上是个坏建议。在这里使用 IntPtr 只会在 C# 端产生不必要的复杂性。这种复杂性很容易避免。
  • Hrm... 它对我来说一直有效.. 并且来自不受管理的土地很容易理解正在发生的事情,而不是其他一些“神奇”的可能性。我认为称其为“坏建议”可能有点过分。
  • 当然可以,但它是不必要的复杂性。没有 IntPtr 很容易编组字符串。您将其称为魔术这一事实仅表明您尚未掌握这样做。那是你的。
  • 如果它有效,那么它绝对不是“坏建议”。 “次优”也许......但远非“坏”。你怎么这么暴躁?
  • 不,我认为这是个坏建议。要正确编写代码,您确实需要尝试/最终您错过了。您对第二个参数的处理非常不完整。根本不需要这种复杂性。你所说的bool 是错误的。 C# bool 的默认编组为 BOOL。这里没有 C99 bool。我不暴躁。我只是相信尽可能提供最好的建议很重要。我不认为这个答案能做到这一点。
【解决方案3】:

我相信您的调用约定不匹配。 C++ 中的默认调用约定是 cdecl,而对于 .NET,它是 stdcall。试试

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

此外,您可能还必须使用 [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 简单得多。
  • 我不认为两者相等,将 Charset 设置为 Unicode 只会告诉它字符串是 Unicode,Bstr 和 LPWStr 都是 unicode,你仍然不知道它是哪一个。使用 MarshalAs 显式设置它更好恕我直言
  • 这两个选项是等价的。仅使用 CharSet 时默认为 LPTSTR。更重要的是,一个 ref LPWSTR 实际上是一个 BSTR,因为这就是编组器对返回的内存所做的事情。这与代码中的 C++ 声明不匹配。
  • msdn.microsoft.com/en-us/library/aa719692(v=VS.71).aspx 没有提到任何关于 CharSet 默认为任何内容的内容。在没有任何明确的文档的情况下,我不相信隐含的。
  • 至于第二部分,是的,这是一个问题,当我第一次阅读这个问题时,我没有看到。但是,从什么时候开始 LPWSTR(可能以 null 终止,可能以长度为前缀)和 BStr(始终以 null 终止并以长度为前缀)相等?
【解决方案4】:

在 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 是第二个参数的绝佳选择,尤其是在调用代码中无法确定缓冲区大小的情况下。
【解决方案5】:

对不起,我没有什么大的答案,只是从我的经验中记住一些事情。你是否尝试使用 StringBuilder,例如将你的 c# 导入函数签名更改为

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

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多