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 nint
s 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?
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.CharSet
.[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;
}
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);
[In] in
not a *
pointer.[DllImport(LibraryName, CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
public static extern SDL_JoystickId SDL_AttachVirtualJoystick([In] in SDL_VirtualJoystickDesc desc);