Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

C# Marshalling bool

Scenario

This should be an easy task, but for some reason I can't get it going as intended. I have to marshal a basic C++ struct during a reversed-P/Invoke call (unmanaged calling managed code).

The issue only arises when using bool within the struct, so I just trim the C++ side down to:

struct Foo {
    bool b;
};

Since .NET marshals booleans as 4-byte fields by default, I marshal the native boolean explicitly as a 1 byte-length field:

public struct Foo {
    [MarshalAs(UnmanagedType.I1)] public bool b;
}

When I call an exported managed static method with the following signature and body:

public static void Bar(Foo foo) {
    Console.WriteLine("{0}", foo.b);
}

I get the correct boolean alpha-representation printed. If I extend the structure with more fields, the alignment is correct and the data is not corrupt after marshalling.

Problem

For some reason, if I do not pass this marshalled struct as an argument but rather as a return type by value:

public static Foo Bar() {
    var foo = new Foo { b = true };
    return foo;
}

The application crashes with the following error message:

enter image description here

If I change the managed structure to hold a byte instead of a bool

public struct Foo {
    [MarshalAs(UnmanagedType.I1)] public byte b;
}

public static Foo Bar() {
    var foo = new Foo { b = 1 };
    return foo;
}

the return value is marshalled properly without an error to an unmanaged bool.

I don't unterstand two things here:

  1. Why does a paramter marshalled with bool as described above work, but as a return value give an error?
  2. Why does a byte marshalled as UnmanagedType.I1 work for returns, but a bool also marshalled with UnmanagedType.I1 does not?

I hope my description makes sense -- if not, please let me know so I can change the wording.

EDIT: My current workaround is a managed struct like:

public struct Foo {
    private byte b;
    public bool B {
        get { return b != 0; }
        set { b = value ? (byte)1 : (byte)0; }
}

which honestly, I find quite ridiculous...

EDIT2: Here is an almost-MCVE. The managed assembly has been recompiled with proper symbol exports (using .export and .vtentry attributes in IL code), but there should be no difference to C++/CLI calls. So this code is not working "as-is" without doing the exports manually:

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;
    }
}
like image 605
PuerNoctis Avatar asked Aug 20 '15 05:08

PuerNoctis


1 Answers

The underlying problem is the same as with this question - Why DllImport for C bool as UnmanagedType.I1 throws but as byte it works The exception you're getting is MarshalDirectiveException - getting the remaining information about the exception is a bit trickier, but unnecessary.

In short, marshalling for return values only works for blittable structures. When you specify use a boolean field, the structure is no longer blittable (because bool isn't blittable), and will no longer work for return values. This is simply a limitation of the marshaller, and it applies for both DllImport and your attempts at "DllExport".

Quoting the relevant piece of documentation:

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

It's not said outright, but the same thing applies when being invoked.

The simplest workaround is to stick with your "byte as a backing field, bool as a property" approach. Alternatively, you could use the C BOOL instead, which will work just fine. And of course, there's always the option of using a C++/CLI wrapper, or even just hiding the real layout of the structure in your helper methods (in this case, your export methods will call another method that deals with the real Foo type, and handle the proper conversion to the Foo++ type).

It's also possible to use a ref argument instead of a return value. This is in fact a common pattern in unmanaged interop:

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

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

on the C++ side, and

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

on the C# side.

You could also make your own boolean type, a simple struct with a single byte field, with implicit cast operators to and from bool - it's not going to work exactly as a real bool field, but it should work just fine most of the time.

like image 100
Luaan Avatar answered Sep 24 '22 08:09

Luaan