Search code examples
c#c++structunmanageddllexport

Passing array/list of object/struct from C# Library to C++ MFC application


I'm trying to pass an array of object or struct from a C# library (.net 4.5) called by a C++ MFC application but I can't get correct data. An additional difficulty is that C++ application don't know the object count.

I have successfully passed simple type (int, string) and also a struct with the following code (using UnmanagedExports 1.2.7):

C#

[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode, Pack = 4)]
public struct ItemA
{
    // Size: 4
    public int Id;
    // Size: 100
    [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 50)]
    public string Name;
    // Size: 2(4)
    public bool IsEnabled;
}

[DllExport(CallingConvention = CallingConvention.Cdecl)]
static public IntPtr GetItemA(int itemId)
{
    // Check structure size
    Debug.Assert(Marshal.SizeOf(typeof(ItemA)) == 108);

    // Get ItemA from a WS
    var itemA = client.GetItemsA().First(i => i.Id == itemId);

    // Convert ItemA to structure pointer
    IntPtr buf = Marshal.AllocHGlobal(Marshal.SizeOf(itemA));
    Marshal.StructureToPtr(itemA, buf, false);
    return buf;
}

C++

#pragma pack(4)
typedef struct
{
    // Size: 4
    int Id;
    // Size: 100
    TCHAR Name[50];
    // Size: 2(4)
    bool IsEnabled;
} ItemA;

extern "C"
{
    ItemA* GetItemA(int itemId);
}

// (...)

void CMyAppDlg::OnBnClickedButtonGetItemA()
{
    // Check structure size
    static_assert(sizeof ItemA == 108, "Size does not match C# struct size");
    ItemA* structItemA = GetItemA(1);    
}

I have searched everywhere but I don't find any functionnal response to my specific problem. Can you help to write both C# and C++ codes for GetItemAList which can return an array of struct or another type to C++ application?

Thanks in advance!

Edit1 : I changed my code to solve packing/size struct issue (64 bits compilation).

Edit2 : I replace manual alignment by #pragma pack.

Edit3 : I'm always stuck with GetItemAList. Here is my actual c#/c++ codes with an MemoryException after calling c#. Can you help me with this sh**? Thanks in advance.

C#

[DllExport(CallingConvention = CallingConvention.Cdecl)]
static public void GetItemAList([In, Out, MarshalAs(UnmanagedType.LPArray, ArraySubType = UnmanagedType.Struct, SizeParamIndex = 1)] ref ItemA[] myArray, ref int length)
{
    // Get ItemAList from a WS
    var itemAArray = client.GetItemsAList().ToArray();

    // Set myArray values
    length = itemAArray.Length;
    for (int i = 0; i < length; i++)
    {
         myArray[i] = itemAArray[i];
    }
}

C++

extern "C"
{
    void GetItemAList(ItemA*& myArray, int& length);
}

// (...)

void CMyAppDlg::OnBnClickedButtonGetItemA()
{
    ItemA* myArray = new ItemA[100];
    int length = 100;
    GetItemAList(myArray, length);   
}

Solution

  • I have not done any checking, but your C++ version is probably compiled with 4 bytes packing (assuming your are compiling in 32 bits).

    In that case, you would have on C++ side:

    // Field,   Field size, Total size
    // -------------------------------
    // Id,               0,          4
    // Name,            50,         54
    // IsEnabled,        1,         55
    // (padding)         1,         56
    

    On that side, you should add a static_assert validating that you got the expected size.

    static_check(sizeof ItemA == 56, "Size does not match C# struct size")
    

    On the hand on C# side, you would have:

    // Field,   Field size, Total size
    // -------------------------------
    // Id,               0,          4
    // Name,            50,         54
    // IsEnabled,        4,         58
    // (no padding)      0,         58
    

    And if bool was 1 byte, you would have a size of 55 instead. In both case, it would not match the size on C++ side.

    As it is somewhat hard to know all rules and not make any mistake and also to ensure nothing is broken in future maintenance, you should always add validation in your code.

    On C# side, typical validation would be something like: Debug.Assert(Marshal.Sizeof(typeof(ItemA)) == 58.

    You could also validate the offset or size of some fields but generally if the size you compute by hand do match on both sides, you probably got it right particularily if you use 1 byte packing.