Table of Contents

SDL3 bindings

BSGameLib.Sdl3.Interop is a set of .NET bindings for SDL3, SDL3_image and SDL3_mixer. Its functions, structs and enums are generated from the SDL headers, and a few helpers sit beside them. The engine, BSGameLib.Game, is built on these bindings. You can also use them on their own, without the engine, to open a window, read input or play sound yourself. The bundled libraries are SDL 3.4.2, SDL_image 3.4.0 and SDL_mixer 3.2.0.

SDL's own documentation, with a page for every function, is on the SDL wiki. The bindings keep SDL's names and signatures, so the wiki applies to them directly.

dotnet add package BSGameLib.Sdl3.Interop --prerelease

Namespaces

Namespace What is in it
BSGameLib.Sdl3.Interop.Generated The static classes SDL, SDL_image and SDL_mixer, with every function, and the structs and enums they use. The functions are raw P/Invoke calls.
BSGameLib.Sdl3.Interop.Manual Helpers written by hand: Sdl for error handling, SdlConstants for flags and sizes, SdlKeycodes and SdlKeymod for keys, and SdlVulkan for Vulkan.

Add a using static for the class you call most. Names are the same as in SDL's C headers, so SDL_Init is SDL.SDL_Init:

using BSGameLib.Sdl3.Interop.Generated;
using BSGameLib.Sdl3.Interop.Manual;
using static BSGameLib.Sdl3.Interop.Generated.SDL;
using static BSGameLib.Sdl3.Interop.Manual.SdlConstants;

Pointers and unsafe code

The bindings pass SDL's pointers through unchanged. A window, a surface or an event is a pointer, and the functions that use them are unsafe. Your project sets AllowUnsafeBlocks:

<PropertyGroup>
  <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>

Nothing is freed for you. Each object SDL gives you has a function to release it, such as SDL_DestroyWindow, and SDL_Quit ends the library. Memory that SDL allocates is freed with SDL_free.

Strings

SDL takes UTF-8 text that ends in a zero byte, and the bindings type it as sbyte*. A C# string literal has no terminator, and neither does a u8 literal. End the literal with \0 yourself, and cast the pointer:

using BSGameLib.Sdl3.Interop.Generated;
using BSGameLib.Sdl3.Interop.Manual;
using static BSGameLib.Sdl3.Interop.Generated.SDL;
using static BSGameLib.Sdl3.Interop.Manual.SdlConstants;

unsafe
{
  fixed (byte* title = "My game\0"u8)
  {
    var window = Sdl.Check(SDL_CreateWindow((sbyte*)title, 800, 600, SDL_WINDOW_RESIZABLE));
  }
}

A string SDL returns is a pointer to the same kind of zero-terminated text. Convert it with Marshal.PtrToStringUTF8, which is what Sdl.GetError does for SDL's error text.

Results and errors

SDL's functions that return bool return a byte here: 1 for true and 0 for false. SDL_Init is one of them. Functions that return a pointer return null on failure.

Sdl.Check turns those results into exceptions. It has an overload for each kind:

  • Sdl.Check(byte) throws when the result is 0.
  • Sdl.Check<T>(T*) throws when the pointer is null, and otherwise returns it.

Both throw an InvalidOperationException whose message names the method that called Check, and then SDL's error text: SDL error in Start: <error>. The text comes from SDL_GetError, through GetError().

Constants and key codes

SdlConstants holds the flags and sizes you pass to SDL, such as SDL_INIT_VIDEO and SDL_WINDOW_RESIZABLE. Most of them are typed the way SDL3 types them, as uint or ulong.

SdlKeycodes holds the key codes that SDL reports, such as SDLK_ESCAPE. A printable key's code is its Unicode code point. A key that does not print is 0x40000000 plus its scan code. SdlKeymod holds the modifier bits, such as KMOD_CTRL.

Vulkan

SdlVulkan holds the Vulkan calls SDL provides: loading the Vulkan library, reading the instance extensions a window needs, and making a surface. Its functions are plain P/Invoke, like the generated ones.

The native libraries

The package brings SDL's native libraries with it, and copies the ones for your platform beside your executable when you build or publish:

Package Libraries Platforms
BSGameLib.Sdl3.Native SDL3, SDL3_image and SDL3_mixer Windows x64 and arm64, Linux x64 and arm64, macOS
BSGameLib.MoltenVK.Native MoltenVK, which SDL uses for Vulkan macOS

The Linux libraries are built against glibc 2.35 (Ubuntu 22.04), so they run on any distribution with glibc 2.34 or newer and need nothing else installed. The licences for the bundled libraries are copied into your game's output with them; see Licences in your game.

A window

This opens a window. The loop ends when the window is asked to quit, such as by its close button:

using BSGameLib.Sdl3.Interop.Generated;
using BSGameLib.Sdl3.Interop.Manual;
using static BSGameLib.Sdl3.Interop.Generated.SDL;
using static BSGameLib.Sdl3.Interop.Manual.SdlConstants;

unsafe
{
  Sdl.Check(SDL_Init(SDL_INIT_VIDEO));

  SDL_Window* window;
  fixed (byte* title = "Hello SDL3\0"u8)
    window = Sdl.Check(SDL_CreateWindow((sbyte*)title, 800, 600, SDL_WINDOW_RESIZABLE));

  var running = true;
  while (running)
  {
    SDL_Event e;
    while (SDL_PollEvent(&e) != 0)
      if (e.type == (uint)SDL_EventType.SDL_EVENT_QUIT)
        running = false;
  }

  SDL_DestroyWindow(window);
  SDL_Quit();
}

To find the version of the libraries you have loaded:

using BSGameLib.Sdl3.Interop.Generated;
using static BSGameLib.Sdl3.Interop.Generated.SDL;

int version = SDL_GetVersion();
Console.WriteLine($"SDL {version / 1000000}.{version / 1000 % 1000}.{version % 1000}");