Search code examples
c#sdlpinvoke

How do I marshal a C# struct with delegate fields?


I'm writing a C# wrapper for SDL3. I'm currently trying to implement the following struct:

typedef struct SDL_VirtualJoystickDesc
{
    Uint16 type;        /**< `SDL_JoystickType` */
    Uint16 padding;     /**< unused */
    Uint16 vendor_id;   /**< the USB vendor ID of this joystick */
    Uint16 product_id;  /**< the USB product ID of this joystick */
    Uint16 naxes;       /**< the number of axes on this joystick */
    Uint16 nbuttons;    /**< the number of buttons on this joystick */
    Uint16 nballs;      /**< the number of balls on this joystick */
    Uint16 nhats;       /**< the number of hats on this joystick */
    Uint16 ntouchpads;  /**< the number of touchpads on this joystick, requires `touchpads` to point at valid descriptions */
    Uint16 nsensors;    /**< the number of sensors on this joystick, requires `sensors` to point at valid descriptions */
    Uint16 padding2[2]; /**< unused */
    Uint32 button_mask; /**< A mask of which buttons are valid for this controller
                             e.g. (1 << SDL_GAMEPAD_BUTTON_SOUTH) */
    Uint32 axis_mask;   /**< A mask of which axes are valid for this controller
                             e.g. (1 << SDL_GAMEPAD_AXIS_LEFTX) */
    const char *name;   /**< the name of the joystick */
    const SDL_VirtualJoystickTouchpadDesc *touchpads;   /**< A pointer to an array of touchpad descriptions, required if `ntouchpads` is > 0 */
    const SDL_VirtualJoystickSensorDesc *sensors;       /**< A pointer to an array of sensor descriptions, required if `nsensors` is > 0 */

    void *userdata;     /**< User data pointer passed to callbacks */
    void (SDLCALL *Update)(void *userdata); /**< Called when the joystick state should be updated */
    void (SDLCALL *SetPlayerIndex)(void *userdata, int player_index); /**< Called when the player index is set */
    int (SDLCALL *Rumble)(void *userdata, Uint16 low_frequency_rumble, Uint16 high_frequency_rumble); /**< Implements SDL_RumbleJoystick() */
    int (SDLCALL *RumbleTriggers)(void *userdata, Uint16 left_rumble, Uint16 right_rumble); /**< Implements SDL_RumbleJoystickTriggers() */
    int (SDLCALL *SetLED)(void *userdata, Uint8 red, Uint8 green, Uint8 blue); /**< Implements SDL_SetJoystickLED() */
    int (SDLCALL *SendEffect)(void *userdata, const void *data, int size); /**< Implements SDL_SendJoystickEffect() */
    int (SDLCALL *SetSensorsEnabled)(void *userdata, SDL_bool enabled); /**< Implements SDL_SetGamepadSensorEnabled() */
} SDL_VirtualJoystickDesc;

This struct is used in the following C function:

extern SDL_DECLSPEC SDL_JoystickID SDLCALL SDL_AttachVirtualJoystick(const SDL_VirtualJoystickDesc *desc);

My (naive) C# implementation for SDL_VirtualJoystickDesc is

[StructLayout(LayoutKind.Sequential)]
public unsafe struct SDL_VirtualJoystickDesc
{
    /// <summary>
    /// A value from <see cref="SDL_JoystickType"/>.
    /// </summary>
    public SDL_JoystickType Type;

    private readonly ushort _padding1;

    /// <summary>
    /// The USB vendor ID of this joystick.
    /// </summary>
    public ushort VendorId;

    /// <summary>
    /// The USB product ID of this joystick.
    /// </summary>
    public ushort ProductId;

    /// <summary>
    /// The number of axes on this joystick.
    /// </summary>
    public ushort NAxes;

    /// <summary>
    /// The number of buttons on this joystick.
    /// </summary>
    public ushort NButtons;

    /// <summary>
    /// The number of balls on this joystick.
    /// </summary>
    public ushort NBalls;

    /// <summary>
    /// The number of hats on this joystick.
    /// </summary>
    public ushort NHats;

    /// <summary>
    /// The number of touchpads on this joystick, requires <see cref="Touchpads"/> to point at valid descriptions
    /// </summary>
    public ushort NTouchpads;

    /// <summary>
    /// The number of sensors on this joystick, requires <see cref="Sensors"/> to point at valid descriptions.
    /// </summary>
    public ushort NSensors;

    private readonly ushort _padding2;

    private readonly ushort _padding3;

    /// <summary>
    /// A mask of which buttons are valid for this controller, e.g. (1 << <see cref="SDL_GamepadButton.South"/>).
    /// </summary>
    public uint ButtonMask;

    /// <summary>
    /// A mask of which axes are valid for this controller, e.g. (1 << <see cref="SDL_GamepadAxis.LeftX"/>).
    /// </summary>
    public uint AxisMask;

    /// <summary>
    /// The name of the joystick.
    /// </summary>
    public readonly string Name => Marshal.PtrToStringUTF8((nint)_name)!;

    private readonly byte* _name;

    /// <summary>
    /// A pointer to an array of touchpad descriptions, required if <see cref="NTouchpads"/> is > 0.
    /// </summary>
    public SDL_VirtualJoystickTouchpadDesc* Touchpads;

    /// <summary>
    /// A pointer to an array of sensor descriptions, required if <see cref="NSensors"/> is > 0.
    /// </summary>
    public SDL_VirtualJoystickSensorDesc* Sensors;

    /// <summary>
    /// User data pointer passed to callbacks.
    /// </summary>
    public void* UserData;

    /// <summary>
    /// Called when the joystick state should be updated.
    /// </summary>
    [MarshalAs(UnmanagedType.FunctionPtr)]
    public SDL_VirtualJoystickUpdateCallback Update;

    /// <summary>
    /// Called when the player index is set.
    /// </summary>
    [MarshalAs(UnmanagedType.FunctionPtr)]
    public SDL_VirtualJoysticSetPlayerIndexCallback SetPlayerIndex;

    /// <summary>
    /// Implements <see cref="SDL.RumbleJoystick(SDL_Joystick*, ushort, ushort, uint)"/>.
    /// </summary>
    [MarshalAs(UnmanagedType.FunctionPtr)]
    public SDL_VirtualJoysticRumbleCallback Rumble;

    /// <summary>
    /// Implements <see cref="SDL.RumbleJoystickTriggers(SDL_Joystick*, ushort, ushort, uint)"/>.
    /// </summary>
    [MarshalAs(UnmanagedType.FunctionPtr)]
    public SDL_VirtualJoysticRumbleTriggersCallback RumbleTriggers;

    /// <summary>
    /// Implements <see cref="SDL.SetJoystickLED(SDL_Joystick*, byte, byte, byte)"/>.
    /// </summary>
    [MarshalAs(UnmanagedType.FunctionPtr)]
    public SDL_VirtualJoysticSetLEDCallback SetLED;

    /// <summary>
    /// Implements <see cref="SDL.SendJoystickEffect(SDL_Joystick*, void*, int)"/>.
    /// </summary>
    [MarshalAs(UnmanagedType.FunctionPtr)]
    public SDL_VirtualJoysticSendEffectCallback SendEffect;

    /// <summary>
    /// Implements <see cref="SDL.SetGamepadSensorEnabled(SDL_Gamepad*, SDL_SensorType, bool)"/>.
    /// </summary>
    [MarshalAs(UnmanagedType.FunctionPtr)]
    public SDL_VirtualJoysticSetSensorsEnabledCallback SetSensorsEnabled;
}

I implemented the function pointers using delegates, something like this:

[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
public unsafe delegate void SDL_VirtualJoystickUpdateCallback(void* userData);

The other delegates implemented the same.

But when trying to implement the SDL_AttachVirtualJoystick function, I get warned by the compiler CS8500 "This takes the address of, gets the size of, or declares a pointer to a managed type [...]"

The C# implementation of the function is as follows:

[DllImport(LibraryName, CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
public static extern SDL_JoystickId SDL_AttachVirtualJoystick(SDL_VirtualJoystickDesc* desc);

SDL_JoystickId is just an enum.

I understand that delegates are managed types, so I can't take its memory addresses that easily. I also know I can implement those fields as nints and use Marshal.GetFunctionPointerForDelegate(), but I'm not sure if that's the most appropiate way.

My question is: what would be the correct way of implementing this struct?


Solution

  • You are overcomplicating it by using bare pointers. Let the marshaller work things out, it's quite good at it.

    • UnmanagedType.FunctionPtr is the default for delegates, you don't have to specify it.
    • You can pass strings directly in the struct if you are managing the memory anyway. Just set the CharSet.
    • The same goes for the arrays: pass them as normal arrays, not pointers.
    [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)]
    public struct SDL_VirtualJoystickDesc
    {
        public SDL_JoystickType Type;
        private readonly ushort _padding1;
        public ushort VendorId;
        public ushort ProductId;
        public ushort NAxes;
        public ushort NButtons;
        public ushort NBalls;
        public ushort NHats;
        public ushort NTouchpads;
        public ushort NSensors;
        private readonly ushort _padding2;
        private readonly ushort _padding3;
        public uint ButtonMask;
        public uint AxisMask;
        public string? Name;
        public SDL_VirtualJoystickTouchpadDesc[]? Touchpads;
        public SDL_VirtualJoystickSensorDesc[]? Sensors;
        public IntPtr UserData;
        public SDL_VirtualJoystickUpdateCallback? Update;
        public SDL_VirtualJoysticSetPlayerIndexCallback? SetPlayerIndex;
        public SDL_VirtualJoysticRumbleCallback? Rumble;
        public SDL_VirtualJoysticRumbleTriggersCallback? RumbleTriggers;
        public SDL_VirtualJoysticSetLEDCallback? SetLED;
        public SDL_VirtualJoysticSendEffectCallback? SendEffect;
        public SDL_VirtualJoysticSetSensorsEnabledCallback? SetSensorsEnabled;
    }
    
    • Do not use void* or any other pointer type in the function pointers, it's not necessary. Just use IntPtr for UserData, and pass a GCHandle if you actually want some data in there, or IntPtr.Zero if not.
    [UnmanagedFunctionPointer(CallingConvention.Cdecl)]
    public delegate void SDL_VirtualJoystickUpdateCallback(IntPtr userData);
    
    [UnmanagedFunctionPointer(CallingConvention.Cdecl)]
    public delegate void SDL_VirtualJoysticSetPlayerIndexCallback(IntPtr userData, int player_index);
    
    [UnmanagedFunctionPointer(CallingConvention.Cdecl)]
    public delegate int SDL_VirtualJoysticRumbleCallback(IntPtr userData, ushort low_frequency_rumble, ushort high_frequency_rumble);
    
    [UnmanagedFunctionPointer(CallingConvention.Cdecl)]
    public delegate int SDL_VirtualJoysticRumbleTriggersCallback(IntPtr userData, , ushort left_rumble, ushort right_rumble);
    
    [UnmanagedFunctionPointer(CallingConvention.Cdecl)]
    public delegate int SDL_VirtualJoysticSetLEDCallback(IntPtr userData, Uint8 red, Uint8 green, Uint8 blue);
    
    [UnmanagedFunctionPointer(CallingConvention.Cdecl)]
    public delegate int SDL_VirtualJoysticSendEffectCallback(IntPtr userData, IntPtr data, int size);
    
    [UnmanagedFunctionPointer(CallingConvention.Cdecl)]
    public delegate int SDL_VirtualJoysticSetSensorsEnabledCallback(IntPtr userData, SDL_bool enabled);
    
    • Pass the struct as [In] in not a * pointer.
    [DllImport(LibraryName, CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
    public static extern SDL_JoystickId SDL_AttachVirtualJoystick([In] in SDL_VirtualJoystickDesc desc);