【问题标题】:Win32 api call via C# fails!通过 C# 调用 Win32 api 失败!
【发布时间】:2011-01-03 11:38:21
【问题描述】:

我有一个像这样导出为 api 的 C++ 函数:

#define WIN322_API __declspec(dllexport)
WIN322_API char* Test(LPSTR str);
WIN322_API char* Test(LPSTR str)
{
 return "hello";
}

函数被 .DEF 文件正确导出为 API,因为我可以在 Dependency Walker 工具中看到它。 现在我有一个 C# 测试程序:

[DllImport("c:\\win322.dll")]

public static extern string Test([MarshalAs(UnmanagedType.LPStr)] String str);

private void Form1_Load(object sender, EventArgs e)
        {
string _str = "0221";
Test(_str); // runtime error here!

}

在调用 Test() 方法时出现错误:

“对 PInvoke 函数 'MyClient!MyClient.Form1::Test' 的调用导致堆栈不平衡。这可能是因为托管 PInvoke 签名与非托管目标签名不匹配。请检查 PInvoke 的调用约定和参数签名匹配目标非托管签名。”

我尝试了许多其他数据类型和封送处理,但一无所获! 请帮帮我!

【问题讨论】:

  • 也许您需要将返回类型设置为类似于 char[]/IntPtr 的内容?我不是 P/Invoke 专家,所以我真的不知道。
  • P/Invoke Signature 工具包将您的 C 代码转换为 IntPtr 返回类型。
  • 我的返回值没有问题。如果我删除输入参数,它会正确返回字符串。输入参数是错误点!
  • 这个问题已经被问了 100 次了。请搜索。

标签: c# c++ winapi pinvoke


【解决方案1】:

这是由于调用约定不匹配造成的,[DllImport] 的默认值为 Stdcall,但 C 编译器的默认值为 Cdecl。在声明中使用 CallingConvention 属性。

这不是唯一的问题,这段代码会在 Vista 和 Win7 上崩溃。从 C 函数返回字符串相当麻烦,存在内存管理问题。目前尚不清楚谁负责释放字符串缓冲区。您现在正在返回一个文字,但这很快就会停止使用。下一站是使用 malloc() 作为返回字符串,目的是让调用者调用 free()。这是行不通的,pinvoke marshaller 无法调用它,因为它不知道 C 代码正在使用什么堆。

它将调用 Marshal.FreeCoTaskMem()。错了,字符串不是由 CoTaskMemAlloc() 分配的。这在 XP 和更早版本上没有被注意到,除了很难诊断出这导致的内存泄漏。并且在 Vista 和 Win7 上大放异彩,它们有一个更严格的内存管理器。

你需要像这样重写 C 函数:

extern "C" __declspec(dllexport) 
void __stdcall Test(const char* input, char* output, int outLen);

现在调用者通过 output 参数提供缓冲区,不再猜测谁拥有内存。您在 C# 声明中使用 StringBuilder。

[DllImport("foo.dll")]
private static extern void Test(string input, StringBuilder output, int outLen);
...
    var sb = new StringBuilder(666);
    test("bar", sb, sb.Capacity);
    string result = sb.ToString();

小心在 C 代码中使用 outLen 参数,这样可以确保不会溢出缓冲区。这会破坏垃圾收集堆,导致应用程序因致命的执行引擎错误而崩溃。

【讨论】:

  • +1 用于覆盖调用约定返回char *的陷阱。
  • 此外,托管代码没有直接的方法可以在通过 malloc() 分配的内存上调用 free() (或者至少我还没有找到它)。唯一的方法是在您的 C/C++ 应用程序中添加一个额外的 DLL 导出,该应用程序在给定句柄上调用 free()。
  • 是的。如果您在 C 代码中使用 CoTaskMemAlloc(),它确实有效。
【解决方案2】:

将宏定义更改为

#define WIN322_API __declspec(dllexport) __stdcall

作为替代方案,导入时使用CallingConvention.Cdecl

例如,阅读here,了解有关调用约定的更多信息。

【讨论】:

    【解决方案3】:

    确保您有正确的调用约定。您的 DLL 可能使用 Cdecl,而 C# 默认使用 StdCall。最好始终明确定义调用约定。

    尤其是在使用存在于 ANSI 和宽字符版本(带有 A 或 W 前缀的那些)中的 Windows 函数时,显式指定 CharSet 以便使用正确的版本。

    当函数返回一个值时,显式地编组返回值。否则,编译器会选择一个默认值,这可能是错误的。 我怀疑这(也是)这里的问题。

    所以改成这样,例如:

    [DllImport("c:\\win322.dll", CharSet = CharSet.Ansi, CallingConvention = CallingConvention.StdCall)]
    [return: MarshalAs(UnmanagedType.LPStr)]
    public static extern string Test([MarshalAs(UnmanagedType.LPStr)] String str);
    

    【讨论】:

    • 感谢丹尼尔。我在我的 C++ 函数 workc 上使用的“__stdcall”约定,导致我可以向它发送输入参数。但我仍然面临一些新问题。我也会试试你说的。 tnx
    • @user434186 除非另有说明,否则 Visual Studio 项目的默认设置是使用 Cdecl。因此,您还可以删除 C++ 函数中的 __stdcall 部分,并在 P/Invoke 代码中指定 Cdecl。
    【解决方案4】:

    尝试返回 LPSTR,而不是 char*。您可能还需要指定 stdcall 调用约定,这是 .NET 中的默认设置,但我不确定您的非托管项目。

    【讨论】:

    • 如果我删除输入参数,它会正确返回字符串。输入参数是错误点!
    • 那一定是调用约定。
    【解决方案5】:

    使用 Unmanagedtype.LPTStr 作为输入。注意额外的 T

    [MarshalAs(UnmanagedType.LPStr)] String str  // your current code
    [MarshalAs(UnmanagedType.LPTStr)] String str // try this code
    

    【讨论】:

    • 我试过了,我没有看到任何变化。我认为有一个基本和根本性的错误。因为我尝试了许多其他数据类型作为输入参数,但得到了同样的错误!
    【解决方案6】:

    从 .Net 传递一个 StringBuilder 作为参数而不是返回一个字符串(在这种情况下,它就像一个输出参数)

    【讨论】:

    • 也就是说,如果您能够对您尝试调用的 DLL 进行更改...
    猜你喜欢
    • 1970-01-01
    • 2016-07-11
    • 1970-01-01
    • 1970-01-01
    • 2021-08-09
    • 2021-06-30
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多