Having trouble marshalling an array of structs from c to c#

369 views Asked by At

I'm trying to marshal an array of c structs into C# (Using Unity) but, no matter the method I use, I always get an exception or a crash.

I'm loading dlls (libretro cores) that conform (or should...) to the Libretro API, the c/c++ side is not available to me (more precisely, not allowed to be modified by me), which means I have to handle the data I get back from that dll no matter how it is laid out.

The C API structs are defined as follow (RETRO_NUM_CORE_OPTION_VALUES_MAX is a constant with a value of 128):

struct retro_core_option_value
{
   const char *value;
   const char *label;
};

struct retro_core_option_definition
{
   const char *key;
   const char *desc;
   const char *info;
   struct retro_core_option_value values[RETRO_NUM_CORE_OPTION_VALUES_MAX];
   const char *default_value;
};

struct retro_core_options_intl
{
   struct retro_core_option_definition *us;
   struct retro_core_option_definition *local;
};

My C# mappings look like this at the moment:

[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)]
public struct retro_core_option_value
{
    public char* value;
    public char* label;
}

[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)]
public struct retro_core_option_definition
{
    public char* key;
    public char* desc;
    public char* info;
    [MarshalAs(UnmanagedType.ByValArray, SizeConst = RETRO_NUM_CORE_OPTION_VALUES_MAX)]
    public retro_core_option_value[] values;
    public char* default_value;
}

[StructLayout(LayoutKind.Sequential)]
public struct retro_core_options_intl
{
    public IntPtr us;
    public IntPtr local;
}

The callback function has the following signature on the C# side:

public unsafe bool Callback(retro_environment cmd, void* data)

retro_environment is an unsigned int converted to an enum, a switch is performed on it and then dictates how to handle the void* data pointer appropriately. Here data is a retro_core_options_intl*.

I'm able to do the void* conversion in 2 ways:

retro_core_options_intl intl = Marshal.PtrToStructure<retro_core_options_intl>((IntPtr)data);

or

retro_core_options_intl* intl = (retro_core_options_intl*)data;

I get a readable address with both approaches (intl.us for the first and intl->us for the second), the "local" part is empty in my particular case but the "us" part is defined as mandatory by the API. intl->us points to an array of retro_core_option_definition of variable length.

The issue I'm having is trying to read the values inside of this mandatory construct.

The array I'm trying to load right now can be seen here: https://github.com/visualboyadvance-m/visualboyadvance-m/blob/master/src/libretro/libretro_core_options.h at line 51.

The API defines a fixed size for the "struct retro_core_option_value values[RETRO_NUM_CORE_OPTION_VALUES_MAX]" struct member, but code that comes in is almost always defined as an array where the last element is "{ NULL, NULL }" to indicate the end, so they don't always (almost never) contain 128 values.

I tried:

retro_core_options_intl intl = Marshal.PtrToStructure<retro_core_options_intl>((IntPtr)data);
retro_core_option_definition us = Marshal.PtrToStructure<retro_core_option_definition>(intl.us);

This gives a NullReferenceException.

retro_core_options_intl intl = Marshal.PtrToStructure<retro_core_options_intl>((IntPtr)data);
retro_core_option_definition[] us = Marshal.PtrToStructure<retro_core_option_definition[]>(intl.us);

This gives a retro_core_option_definition array of 0 length.

retro_core_options_intl intl = Marshal.PtrToStructure<retro_core_options_intl>((IntPtr)data);
retro_core_option_definition us = new retro_core_option_definition();
Marshal.PtrToStructure(intl.us, us);

This gives a "destination is a boxed value".

That's basically where I'm at... Any help would be much appreciated :)

The entire codebase can be found here: https://github.com/Skurdt/LibretroUnityFE

1

There are 1 answers

2
jjxtra On

First thing I see is that you either need to use wchar_t types instead of char types in you C code, or you can use byte instead of char in C#. System.Char in C# is two bytes. char in C code is 1 byte.

You can also use System.String in the C# code and annotate it with a MarshalAs attribute to tell it what type of char data is coming in, such as Ansi or Unicode C strings.