Search code examples
c#cmarshalling

Passing structure from native C library to C#


I am trying to fill structure containing array from native library to C#. My structure contains two int variables and array of elements which are structures of two integers.

Issue is that variables inside array are received correctly but variables in the outer structure are received as 0. If I initalize them with different values on the C# side they come unchanged.

C interface and implementation:

extern "C" {
struct Element
{
    int id;
    int uid;
};

struct ElementsGrid
{
    int width;
    int height;
    Element* elements;
};

__declspec(dllexport) int gridSize()
{
    return 20;
}
__declspec(dllexport) void getGrid(ElementsGrid* grid)
{
    grid->width = 5;
    grid->height = 4;
    for (int w = 0; w < grid->width; ++w) {
        for (int h = 0; h < grid->height; ++h) {
            int index = w * grid->height + h;
            grid->elements[index].uid = index;
            grid->elements[index].id = 42;
        }
    }
}
} // extern "C"

C# part

[StructLayout(LayoutKind.Sequential)]
public struct Element
{
    public int id;
    public int uid;
}

[StructLayout(LayoutKind.Sequential)]
public struct ElementsGrid
{
    public int width;
    public int height;
    public Element[] elements;
}
//...
[DllImport("my_plugin")]
static extern void getGrid([In,Out] ElementsGrid ptr);
//...
void getGrid()
{
    int field_size = gridSize();
    ElementsGrid grid = new ElementsGrid
    {
        elements = new Element[field_size]
    };
    getGrid(grid);
    print(grid.width);      // 0 here. Unchanged
    print(grid.height);     // 0 here. Unchanged
    for (int row = 0; row < 4; row++) {
        for (int column = 0; column < 5; column++) {
            int index = row * 5 + column;
            Element element = grid.elements[index];
            print(element.id);  // 42. Correct value
            print(element.uid); // ==index. Correct value
        }
    }
}


Solution

  • This is totally possible, but your code has some flaws. The getGrid method in C++ is expecting a pointer to an instance of ElementsGrid, whereas you're trying to pass the instance, not a pointer to it. The other issue is that the definition of ElementsGrid in C++ differs from the equivalent in C#; C++ wants a pointer to Element, whereas C# has an array of element.

    I didn't change your C++ code at all, but managed to get this working as follows.

    The structures:

        [StructLayout(LayoutKind.Sequential)]
        public struct Element
        {
            public int id;
            public int uid;
        }
    
        [StructLayout(LayoutKind.Sequential)]
        public struct ElementsGrid
        {
            public int width;
            public int height;
            public IntPtr elements; // Note that this is now a pointer, not an array of Element
        }
    

    The getGrid method works when I try it like this:

        public static void getGrid()
        {
            int field_size = gridSize();
            ElementsGrid grid = new ElementsGrid()
            {
                width = field_size,
                height = field_size,
                elements = Marshal.AllocHGlobal(Marshal.SizeOf(typeof(Element)) * field_size * field_size)  // Allocate enough memory to the pointer for the C++ method to populate it
            };
            IntPtr gridPtr = Marshal.AllocHGlobal(Marshal.SizeOf(grid));  // Allocate enough memory to a pointer to hold the contents of the ElementsGrid instance
    
            try
            {
                Marshal.StructureToPtr<ElementsGrid>(grid, gridPtr, false);  // Fill the pointer
                getGrid(gridPtr);  // Pass the pointer to C++
                grid = Marshal.PtrToStructure<ElementsGrid>(gridPtr);  // Re-assign grid to the contents of the pointer 
                
                Element[] elements = GetElements(grid.elements, grid.width * grid.height);  // Read the array from the grid.elements pointer using the new method below
    
                Console.WriteLine("Grid Width: " + grid.width.ToString());  // Works
                Console.WriteLine("Grid Height: " + grid.height.ToString());  // Works
                for (int row = 0; row < grid.height; row++)
                {
                    for (int column = 0; column < grid.width; column++)
                    {
                        int index = row * 5 + column;
                        Element element = elements[index];
                        Console.WriteLine("Element id: " + element.id.ToString());  // Works
                        Console.WriteLine("Element uid: " + element.uid.ToString());  // Works
                    }
                }
            }
            finally
            {
                // Always clear up the pointers in a finally block
                Marshal.FreeHGlobal(grid.elements);  
                Marshal.FreeHGlobal(gridPtr);
            }
        }
    

    The new method to read the grid.elements pointer into an array of Element. This is necessary as you can't marshall a pointer to an array of structure without reading the array directly.

        private static Element[] GetElements (IntPtr elementsPtr, int length)
        {
            int sizeInBytes = Marshal.SizeOf(typeof(Element));  // Get the size of one Element
            Element[] output = new Element[length];  // Create an array of Element with the right size
    
            for (int elCount = 0; elCount < length; elCount++)
            {
                IntPtr elPtr = new IntPtr((elementsPtr.ToInt64() + (elCount * sizeInBytes)));  // Read the pointer + offset from the element count
                output[elCount] = (Element)Marshal.PtrToStructure(elPtr, typeof(Element)); Assign the element to the contents of the pointer 
            }
    
            return output;
        }
    

    Also, with a blank canvas, you could achieve this in a totally different way by changing your code in C++ to accept the row and column count and return a pointer to the ElementsGrid struture. The code in C# could pass in the relevent row and column count and read the data from the pointer. That would save creating ElementsGrid in C#, getting a pointer to it then passing this to your C++ code.

    Hope this helps.