1

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

Skurdt
  • 24
  • 3
  • Don't know if this will be helpful, but I stumbled onto this which talks of difference between c and c# with the char type: https://stackoverflow.com/questions/11511333/convert-a-c-struct-to-c-sharp – Russ Dec 31 '20 at 14:35
  • Oh Brother. Marshalling in Unity plugins is just so difficult, I would give up and go home :O Is this for iOS or what platform ? – Fattie Dec 31 '20 at 15:49
  • The program itself has been able run the basics on windows, linux, macos, android and ios (aka. loading some cores and start some games properly). Some more troublesome features only run on windows and one only works on windows while forcing the opengl backend. While it's not perfect, most of the struggles have been resolved somewhat. But this issue really put a wall into my face, I tried a lot of solutions... I started sending the data to c++ and read it there (which seems to work) but I'd rather have a c# solution if it's at all possible first. – Skurdt Dec 31 '20 at 17:16

1 Answers1

2

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.

jjxtra
  • 20,415
  • 16
  • 100
  • 140
  • 1
    I did not even know about `MarshalAs`, thanks! – Fattie Dec 31 '20 at 15:49
  • Changing all char* to string has the same result. I have a .cs file that mimics the .h C header for marshalling purpose and so far almost all of them worked more or less as they should. Some worked using the string type while others needed the use of char* then be converted using Marshal.PtrToStringAnsi (I use the word "need" loosely here, results were/are always unpredictable since the external dlls vary a great deal between the different providers.... this is a list of all the cores that should be compatible with my project: https://buildbot.libretro.com/nightly/windows/x86_64/latest/) – Skurdt Dec 31 '20 at 17:12