Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Marshalling C structures to C#

Suppose I have a structure:

typedef struct {
float x;
float y;
float z;
int   ID;
} Vertex;

and a C++ function:

float first(Vertex* ptr, int length){ //really silly function, just an example
    Vertex u,v;
    u.x = ptr[0].x; //...and so on, copy x,y,z,ID
    v.x = ptr[1].x; 
    return (u.x * v.x + u.y * v.y + u.z * v.z);
    }


Vertex* another(float a, int desired_size){
    Vertex v = (Vertex*)malloc(desired_size*sizeof(Vertex));
    v[0].x = a;
    v[1].x = -a; //..and so on.. make some Vertices.
    return v;
}

First - my IDE. I'm using Visual Studio 2010, building a C# (4.0) application; The C++ part is also built in VS2010.

I know how to build a DLL of C/C++ code and use it in C# application, but until today I used only primitive arguments and return values like:

    [DllImport("library.dll", CharSet = CharSet.Ansi, SetLastError = true, CallingConvention = CallingConvention.StdCall)]
    public static extern int simple(int a, int b);

Today I need to pass an array of structs (as in the example above).. and perhaps also receive one back..

How to I "translate" a C# class to a C struct (and vice versa) ??

like image 419
Queequeg Avatar asked Nov 30 '11 19:11

Queequeg


2 Answers

The struct can be declared like this:

[StructLayout(LayoutKind.Sequential)]
public struct Vertex {
    public float x;
    public float y;
    public float z;
    public int ID;
}

Next you need to settle on a calling convention. Your C++ code is almost certainly compiled with cdecl. Let's stick with that.

The function first is easy to call from C#:

[DllImport("library.dll", CallingConvention = CallingConvention.Cdecl)]
public static extern float first(Vertex[] vertices);

Note that you should not use SetLastError here–that's for Windows API functions. And there's no need to set the CharSet since there is no text here.


Now, for another things get more complex. If you can allocate the memory in the C# code then that is definitely the way to go.

void PopulateVertices(Vertex *vertices, int count)
{
    for (int i=0; i<count; i++)
    {
        vertices[i].x = ....
    }
}

On the C# side you declare it like this:

[DllImport("library.dll", CallingConvention = CallingConvention.Cdecl)]
public static extern void PopulateVertices(Vertex[] vertices, int count);

and call it like this

Vertex[] vertices = new Vertex[2];
PopulateVertices(vertices, vertices.Length);

If you don't want to allocate on the C# side of the fence then do it like this:

  1. Return a pointer from the C++ code and allocate it with CoTaskMemAlloc.
  2. In C# declare the return value of the imported function as IntPtr.
  3. Use Marshal.PtrToStructure and some pointer arithmetic to marshal the return array into a C# array.
  4. Call Marshal.FreeCoTaskMem to free the memory allocated in the native module.

But if you want my advice, try and stick to allocating the array in the managed code.

like image 54
David Heffernan Avatar answered Nov 08 '22 04:11

David Heffernan


It should be this easy:

[StructLayout(LayoutKind.Sequential)]
public struct Vertex {
    float x;
    float y;
    float z;
    int   ID;
}

[DllImport("library.dll", CallingConvention=CallingConvention.StdCall)]
public static extern float first(Vertex[] verticies, int arrLen);

The issues you may run into would be if there is any packing done on the C version of the struct, and possibly the struct layout. If the layout doesn't match, you may want to change it to LayoutKind.Explicit and use the [FieldOffset(0)] attribute on each field. C would also have no idea, the length of the verticies array passed, so if that changes, you'd want to pass that along to the method.

To get an array back:

[DllImport("library.dll", CallingConvention=CallingConvention.StdCall)]
public static extern Vertex[] another(float a);

The marshaler handles all of the memory issues when passing arguments in, but returning the array it can't do anything. Since the memory is allocated on the unmanaged heap, the GC has no idea about it, and you will wind up with a memory leak. The marshaller will simply copy the native structs to the managed struct array, but it can't free the memory you've allocated with malloc.

The easiest way to get around it, if you can change the C++ code, would be to change the signature of another to take in an array of verticies (and the length of the array) instead of returning one. I don't need to write out any code for you that does this, @DavidHeffernan has already done this in his answer, the part of the break.

like image 24
Christopher Currens Avatar answered Nov 08 '22 04:11

Christopher Currens