C# 编组布尔

C# Marshalling bool

场景

这应该是一项简单的任务,但由于某种原因我无法按预期进行。我必须在 reversed-P/Invoke 调用(非托管调用托管代码)期间编组基本 C++ struct

只有在结构中使用 bool 时才会出现此问题,所以我只是 trim 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 作为参数传递,而是作为 return 按值类型传递:

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;
}

return 值被正确编组到非托管布尔值,没有错误。

这里有两件事我不明白:

  1. 为什么如上所述用 bool 编组的参数有效,但作为 return 值却给出错误?
  2. 为什么 byte 编组为 UnmanagedType.I1 可以工作 return 秒,而 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 调用没有区别.因此,如果不手动执行导出,此代码将无法工作 "as-is":

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;
    }
}

根本问题与这个问题相同 - Why DllImport for C bool as UnmanagedType.I1 throws but as byte it works 你得到的异常是 MarshalDirectiveException - 获取有关异常的剩余信息有点棘手,但没有必要。

简而言之,return 值的编组仅适用于 blittable 结构。当您指定使用布尔字段时,该结构不再是 blittable(因为 bool 不可 blittable),并且将不再适用于 return 值。这只是编组器的一个限制,它适用于 DllImport 和您在 "DllExport".

的尝试

引用相关文档:

Structures that are returned from platform invoke calls must be blittable types. Platform invoke does not support non-blittable structures as return types.

没有直接说,调用的时候也是一样。

最简单的解决方法是坚持使用您的 "byte as a backing field, bool as a property" 方法。 或者,您可以改用 C BOOL,它会工作得很好。当然,始终可以选择使用 C++/CLI 包装器,甚至只是隐藏您的辅助方法中结构的真实布局(在这种情况下,您的导出方法将调用另一个处理真实 Foo 类型的方法,并处理到 Foo++ 类型的正确转换)。

也可以使用 ref 参数代替 return 值。这实际上是非托管互操作中的常见模式:

typedef void(__stdcall *Pt2GetFoo)(Foo* foo);

Foo f2 = Foo();
getFoo(&f2);

在 C++ 方面,

public static void GetFoo(ref Foo foo)
{
    foo = new Foo { b = true };
}

在 C# 端。

你也可以制作你自己的布尔类型,一个简单的 struct 和一个 byte 字段,带有隐式转换运算符和 bool - 它不会完全工作作为一个真正的 bool 字段,但它在大多数时间应该都可以正常工作。