C# – calling C functions – marshalling structs

A previous post explained how to marshal simple parameters so that C# code can call C functions. The marshalling ensures that all the conversions to get from C# to C types on calling the function, then back to C# types on return or out parameters, is done for you.

The next step in complexity is to be able to call C functions that take structs as arguments. In this case, each field in the struct needs to be marshalled in order to map it onto the correct C type and to get the conversions to/from the C type. For example, a char[] C field in the struct can be mapped onto a C# string.

To start off, here’s a C struct:

struct MessageStruct
  char MsgID[11];
  char MsgText[501];
  char IsAlert;
  char Status;

And here’s the function that takes such a struct as an argument:

int GetUsrMsg(int Index, struct MessageStruct* UserMsg);

In this case, an uninitialised struct is passed into the function, that then fills it in.

To call this function from C#, first we need to define a C# struct that is marshalled to have the same fields and field sizes as the C version. This takes some attention to details because it is very easy to get a size wrong.

First, create a C# struct with fields defined using C# types that are suitable for mapping onto the C types.

public unsafe struct MessageStruct
    public string MsgID;
    public string MsgText;
    public char IsAlert;
    public char Status;

Note that this is declared unsafe because it is used with an unsafe C function.

So, char arrays are mapped onto strings and char is mapped onto char. However, a char in C# is a Unicode char 16-bits wide whereas in C it is an ASCII char 8-bits wide.

To ensure that the char types are mapped correctly, an attribute is attached to the struct to control the mapping.

[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi, Pack = 1)]
public unsafe struct MessageStruct

This attribute declares a number of parameters that control the mapping of C# types onto the bare bytes of the C type.

The Sequential value tells the marshaller that the fields in the C struct are laid out in the same order as the C# struct. An alternative is to specify the exact byte position of each field.

Then the CharSet value indicates that the underlying C struct uses Ansi characters, so the marshaller must convert from the C# unicode characters to Ansi on in parameters and vice-versa on out parameters.

Finally, the Pack value indicates how the C types are packed into words. Each field is packed to a multiple of this parameter. The default is 4, so that each field is guaranteed to start on a word boundary. However, in this particular C library, the C structs have been packed down to individual bytes, thus a pack value of 1 is used.

The next stage is to marshall each of the fields by adding a MarshallAs attribute to each one:

    [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 11)]
    public string MsgID;

    [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 501)]
    public string MsgText;

    public char IsAlert;

    public char Status;

The marshalling attributes map the field of the C# type onto the corresponding C type. Take the first field. This is a char[11] in the C struct. It has been mapped onto a string and then the string has been marshalled as a ByValTStr with a size of 11. The ByValTStr is a C character array which is embedded in the struct, i.e. a char[11] rather than a char* which points to a separate character buffer.

The char fields have also been marshalled onto a one-byte unsigned type.

The next stage is to marshall the C function that takes this struct as a parameter.

Remember the C function definition:

int GetUsrMsg(int Index, struct MessageStruct* UserMsg);

So, as with previous tutorials, the first step is to declared a delegate type to represent the function’s type with each parameter marshalled onto the C types.

[return: MarshalAs(UnmanagedType.I4)]
private delegate int GetUsrMsg_t(
    [MarshalAs(UnmanagedType.I4)] int index,
    out MessageStruct message);

Note that the message parameter is an out parameter, meaning that there is no need to initialise it and it will be passed by reference.

Then, using the dynamic loader class, the function of this name can be loaded from the DLL and called:

dynaloader loader = new dynaloader("myclib.dll");
GetUsrMsg_t GetUsrMsg = loader.load_function<getusrmsg_t>("GetUsrMsg");
MessageStruct message;
int message_status = GetUsrMsg(0, out message);

So, the first two lines use the dynamic DLL loader to load the C DLL. Then the relevant function is loaded from the DLL. The third line declares a bare MessageStruct to use in the function call. The last line calls the function, passing the struct as an out parameter.

Leave a Reply