【发布时间】:2015-08-20 05:27:18
【问题描述】:
场景
这应该是一件容易的事,但由于某种原因,我无法按预期进行。我必须在反向 P/Invoke 调用(非托管调用托管代码)期间编组一个基本的 C++ struct。
只有在结构中使用bool 时才会出现此问题,因此我将 C++ 端向下修剪为:
struct Foo {
bool b;
};
由于 .NET 默认将布尔值编组为 4 字节字段,因此我将本机布尔值显式编组为 1 字节长度字段:
public struct Foo {
[MarshalAs(UnmanagedType.I1)] public bool b;
}
当我使用以下签名和正文调用导出的托管静态方法时:
public static void Bar(Foo foo) {
Console.WriteLine("{0}", foo.b);
}
我打印了正确的布尔 alpha 表示。如果我用更多字段扩展结构,则对齐正确并且编组后数据不会损坏。
问题
由于某种原因,如果我不将这个编组的 struct 作为参数传递,而是作为返回值类型传递:
public static Foo Bar() {
var foo = new Foo { b = true };
return foo;
}
应用程序崩溃并显示以下错误消息:
如果我将托管结构更改为保存 byte 而不是 bool
public struct Foo {
[MarshalAs(UnmanagedType.I1)] public byte b;
}
public static Foo Bar() {
var foo = new Foo { b = 1 };
return foo;
}
返回值被正确编组到非托管布尔值,没有错误。
这里有两点我不明白:
- 为什么如上所述使用
bool编组的参数可以工作,但作为返回值会产生错误? - 为什么
byte编组为UnmanagedType.I1用于退货,而bool也编组为UnmanagedType.I1却不能?
我希望我的描述有道理——如果没有,请告诉我,以便我更改措辞。
编辑: 我当前的解决方法是托管结构,例如:
public struct Foo {
private byte b;
public bool B {
get { return b != 0; }
set { b = value ? (byte)1 : (byte)0; }
}
老实说,我觉得这很荒谬......
EDIT2:这是一个几乎是 MCVE。已使用正确的符号导出重新编译托管程序集(在 IL 代码中使用 .export 和 .vtentry 属性),但应该与 C++/CLI 调用没有区别。因此,如果不手动执行导出,此代码将无法“按原样”运行:
C++ (native.dll):
#include <Windows.h>
struct Foo {
bool b;
};
typedef void (__stdcall *Pt2PassFoo)(Foo foo);
typedef Foo (__stdcall *Pt2GetFoo)(void);
int main(int argc, char** argv) {
HMODULE mod = LoadLibraryA("managed.dll");
Pt2PassFoo passFoo = (Pt2PassFoo)GetProcAddress(mod, "PassFoo");
Pt2GetFoo getFoo = (Pt2GetFoo)GetProcAddress(mod, "GetFoo");
// Try to pass foo (THIS WORKS)
Foo f1;
f1.b = true;
passFoo(f1);
// Try to get foo (THIS FAILS WITH ERROR ABOVE)
// Note that the managed method is indeed called; the error
// occurs upon return. If 'b' is not a 'bool' but an 'int'
// it also works, so there must be something wrong with it
// being 'bool'.
Foo f2 = getFoo();
return 0;
}
C# (managed.dll):
using System;
using System.Runtime.InteropServices;
public struct Foo {
[MarshalAs(UnmanagedType.I1)] public bool b;
// When changing the above line to this, everything works fine!
// public byte b;
}
/*
.vtfixup [1] int32 fromunmanaged at VT_01
.vtfixup [1] int32 fromunmanaged at VT_02
.data VT_01 = int32(0)
.data VT_02 = int32(0)
*/
public static class ExportedFunctions {
public static void PassFoo(Foo foo) {
/*
.vtentry 1:1
.export [1] as PassFoo
*/
// This prints the correct value, and the
// method returns without error.
Console.WriteLine(foo.b);
}
public static Foo GetFoo() {
/*
.vtentry 2:1
.export [2] as GetFoo
*/
// The application crashes with the shown error
// message upon return.
var foo = new Foo { b = true; }
return foo;
}
}
【问题讨论】:
-
我们可以有一个 MCVE 吗?所以我们也可以看到函数调用。
-
@DavidHeffernan 我尽我所能添加了一个 MCVE。由于我使用反向 P/Invoke 机制并直接操作 IL 代码,因此如果不手动更改程序集,它不会开箱即用。
-
操纵 IL?唔。这肯定是相关的!
-
@DavidHeffernan 这里描述了基本概念(codeproject.com/Articles/37675/…)。您可以在 inernet 上的几篇文章中找到这个机制,但在这里简单地解释太多了。但是这个应该(希望)不是这里的问题,因为它直接反映了 C++/CLI 行为。这是 IMO 问题的唯一机会是
ilasm.exe生成错误的编组 thunk - 这将是报告给 Microsoft Connect 进行修复的候选对象。 -
你能避免使用 C++ 进行互操作吗?在互操作结构和函数指针中坚持使用纯 C 语言,我得到了更好的结果。
标签: c# interop pinvoke marshalling