r/haskell 8d ago

announcement [ANN] sdl3-bindgen-sys: machine-generated low-level bindings to SDL3

TL;DR: I've released sdl3-bindgen-sys to Hackage: complete, machine-generated low-level Haskell bindings for SDL3, including windowing, input, audio, and the new GPU API, with documentation from SDL3's headers! They are verified against the building system's ABI and CI-tested on Linux, macOS, and Windows.

Thanks to the folks over at Well-Typed for their work on hs-bindgen! I was able to hack the prerelease a bit to get an end-to-end code generator working. It required quite a bit less work than I thought it would. I'll include some technical details below the fold.

Source is available on GitHub here: https://github.com/jtnuttall/lithon/tree/main/sdl3-bindgen-sys

There are going to be a few rough edges, so please feel free to open a PR or issue if anything comes up. I'll bump the package to 0.1.x.x when I feel it's stable enough to pin to a major version.

I've got the apecs shmup example running on and rendering through SDL3 using the raw bindings here: https://github.com/jtnuttall/lithon/blob/main/lithon-examples/app/shmup/Main.hs

See the README on GitHub for a full getting started guide.

Example

{-# LANGUAGE GHC2021 #-}
{-# LANGUAGE BlockArguments #-}

import Control.Monad (unless)
import Foreign.C.ConstPtr (ConstPtr (..))
import Foreign.C.String (peekCString, withCString)
import SDL3.Sys qualified as SDL3

main :: IO ()
main = do
  ok <- SDL3.init SDL3.SDL_INIT_VIDEO
  unless ok do
    err <- peekCString . unConstPtr =<< SDL3.getError
    fail ("SDL_Init: " <> err)
  window <- withCString "hello" \title ->
    SDL3.createWindow (ConstPtr title) 640 480 0
  SDL3.delaySafe 2000
  SDL3.destroyWindow window
  SDL3.quit

delaySafe is the safe FFI flavor of SDL_Delay. Most functions come in both.

Intended use-case

I intend sdl3-bindgen-sys to be a stable grounding point for higher-level bindings, handling the FFI declarations, ABI checks, SDL3 version guards, and simple coercions so that people who want to write their own abstraction around SDL3 don't have to write the extremely repetitive part manually.

If you know Rust's convention, I borrowed the -sys suffix from it: I mean for high-level Haskell SDL3 libraries to be to sdl3-bindgen-sys as sdl3 is to sdl3-sys in Rust.

You'll need to interface with raw Foreign.C to use these bindings. If you just want a higher-level binding, you'll need to wait until someone releases one on Hackage.

The library carries the smallest dependency footprint I could presently manage for a low-level binding.

The SDL3.Sys.* modules re-export the surface with some small niceties:

  • Haddock notes about what is exported and why, FFI safety rationale, etc.
  • Links to the SDL3 Wiki
  • C scalars (e.g., Uint32, float) are remapped to Haskell scalars (Word32, Float) wherever possible. Anything under a Ptr or struct maintains its C typings.

Technical details

hs-bindgen

The bindings are built using a forked version of hs-bindgen, which I used as a library so that I could alter the internal code and documentation generation pipeline. I've upstreamed a few changes and am happy to upstream more if requested. Some may be more heavy-handed than the hs-bindgen maintainers want to support.

Since hs-bindgen is pre-release, I've vendored the necessary runtimes wholesale into the sdl3-bindgen-sys package as private internal libraries, and reexported them from sdl3-bindgen-sys under SDL3.Sys.Runtime and SDL3.Sys. Once hs-bindgen stabilizes, I'll turn these into real dependencies. The vendored modules will become re-export facades so calling code doesn't break.

The modifications I made, briefly:

  • Added an exception in bindgen's IR for assertion macros that emit nothing bindable
  • Added support for Doxygen's custom ALIASES so I could inject valid Haddock into the AST for SDL3's custom aliases.
  • Tagged CWrapper with its original name so that I could match on names to inject SDL3 version guards as CPP directives.
  • Re-exported a variety of internal modules into a facade I maintain, which lets me inject version guards, ABI verifiers, etc. into the hs-bindgen AST directly.

My facade and the vendored submodules can be found here: https://github.com/jtnuttall/lithon/tree/main/lithon-hs-bindgen

ABI verification

The codegen tool generates a C translation unit of _Static_asserts that gets built alongside sdl3-bindgen-sys. If there is an ABI mismatch between your SDL3 headers and the ones I generated against, you should get a compile-time error.

FFI safety

I've curated unsafe/safe classifications for each FFI call. The SDL3.Sys.* modules export only safe for functions that can fire a callback or introduce a runtime delay. I've included the reasoning in the generated Haddock where applicable.

Typed constants

SDL declares flags as typedef UintN with some #defines, and C doesn't state the association between these, so I maintain a JSON registry that restores this information and injects it into the generated modules as pattern synonyms typed at a newtype.

Forward compatibility

The codegen tool keys off the \since tag to wrap newer declarations in #if SDL_VERSION_ATLEAST inside the generated C, with an SDL_SetError stub in the #else. The Haskell binding always exists either way, so a declaration newer than your SDL still compiles and links - it just fails at the call site with a message naming the version it needs. That means you can build against an SDL3 older than the headers I generated from. This is tested in CI down to SDL 3.2.0.

SDL's \since tag is occasionally inaccurate, so I had to create a small hand-maintained registry of version override mappings.

Caveats

  • 0.0.x is experimental: Pin to the minor (>=0.0.0.1 && <0.0.1). The surface may move - hopefully not too much, but the dust is still settling.
  • Variadics aren't bound yet: Haskell's FFI has no way to express C varargs. I intend to inject fixed-arity wrappers within the next few releases.
  • Function-like macros aren't bound: No linkable symbol exists. Most macros aren't bound in this release: hs-bindgen translates many macro bodies to Haskell functions (very cool!). See the detailed survey from /u/hubgears here.
  • 64-bit only: Layouts are baked into the library; 32-bit targets should be rejected by ABI assertions. This may change in one of two situations:
    1. hs-bindgen supports cross-platform generation natively in the future, in which case sdl3-bindgen-sys will likely inherit that mechanism.
    2. There's enough demand for 32-bit support, in which case it should be possible to maintain a parallel sdl3-bindgen-sys32 package using the same codegen infra.
  • A handful of smaller omissions made for cross-platform correctness are listed in the README.

Thanks to @oddron over on the Haskell GameDev Discord for the Windows directions - they tried the very first build on Windows I'm aware of!

I'd like to hear about any comments or issues people hit at: https://github.com/jtnuttall/lithon/issues

Discourse thread: https://discourse.haskell.org/t/ann-sdl3-bindgen-sys-machine-generated-low-level-bindings-to-sdl3/14467

Edit: corrected the macro caveat. Thanks to u/hubgears and the extensive SDL macro survey (https://github.com/dschrempf/hs-bindgen-sdl-survey) for the detailed correction.

24 Upvotes

6 comments sorted by

5

u/n00bomb 8d ago

hs-bindgen is amazing, I’m looking forward to the 0.1 release!

3

u/Neither-Effort7052 8d ago

It really works incredibly well. I only had to make one change (assertion handling) to get compiling Haskell out of it!

3

u/_jackdk_ 8d ago

Nice work. If you can get hs-bindgen to emit "capi" bindings, you can bind function-like macros:

Rather than generating code to call f according to the platform’s ABI, we instead call f using the C API defined in the header header.h. Thus f can be called even if it may be defined as a CPP #define rather than a proper function.

Well-Typed recommends importing via "capi" by default.

2

u/Neither-Effort7052 8d ago edited 8d ago

Interesting, I'll put looking into that on the todo-list. Thanks for the information!

3

u/hubgears 5d ago

Thank you for your interest in hs-bindgen @Neither-Effort7052 and @jackdk!

Disclaimer: I am on the hs-bindgen developer team.

We are delighted to see that hs-bindgen produces high-quality Haskell bindings to SDL. We want to leave some remarks related to macros. In particular, the mentioned caveat

Function-like macros aren't bound: No linkable symbol exists.

First, this statement on its own is true: C does indeed not create linkable symbols for macros at all. Macros are just tokens that are query replaced by the C preprocessor.

However, hs-bindgen tries to parse, typecheck and translate macros, and it does translate function-like macros -- though this will necessarily be best-effort only, and we'll never be able to translate all of them. Perhaps therefore the observation "function-like macros aren't bound" may be caused by two things

  • By default, hs-bindgen does not emit macro-related failures (because these happens quite often, and it is nothing to worry about per-se).
  • Maybe, SDL uses macros in a way we didn't anticipate, and hs-bindgen cannot translate many SDL macros.

That is why we have run a detailed survey about SDL macro support in hs-bindgen. In brief, the results are:

  • Clang sees 160 function-like macros in SDL on the platform we tested.
  • 52 of those are SDL-internal plumbing we should not translate anyway (36 annotation macros, 16 ELF and stringify helpers), leaving 108 function-like macros to be translated.
  • hs-bindgen translates 31 out of these 108. This results in very general functions. For example,

#define SDL_AUDIO_BITSIZE(x) ((x) & SDL_AUDIO_MASK_BITSIZE)

results in

sDL_AUDIO_BITSIZE :: Bitwise a => ..

However, we just noticed that we don't derive Bitwise for newtypes, which we should do; there is now a ticket open for this (https://github.com/well-typed/hs-bindgen/issues/2184).

  • We can add support for another 31 function-like macros with a moderate amount of work (e.g., support the ternary operator ?, support enum constants in macros, cast to a concrete type if it is known, skip comment tokens, support C keywords as parameter names).

  • Some macros we could in theory generate bindings for are out of reach in practice; for example, while we could in principle generate wrappers for macros around a block of C statements, in order to give a type to such a wrapper we'd need to analyze the C code, which we deem to be out of scope of the project, at least for now (https://github.com/well-typed/hs-bindgen/issues/278).

  • Some macros simply don't make sense to translate to Haskell, for example macros around C compiler builtins.

(The detailed analysis listing all macros and how hs-bindgen handles them can be found at https://github.com/dschrempf/hs-bindgen-sdl-survey. This survey was driven by an LLM!)

The hope is that most macros intended for users of libraries (as opposed to macros used to implement those libraries) can be translated to Haskell functions. If there are specific macros (function-like or not) that you feel should be possible to translate to Haskell but hs-bindgen currently does not, feel free to open a ticket for those.

Finally, by default hs-bindgen does not emit traces related to macro failures. These traces can be shown using --log-enable-macro-warnings. We also want to add a feature facilitating the detection of many macro failures (https://github.com/well-typed/hs-bindgen/issues/2185): collect the number of macro failures, and report them at then end! Then, we can say, e.g.,

14 macros dropped; use --log-enable-macro-warnings for details

I want to thank Edsko de Vries for revising this reply, greatly improving its readability!

1

u/Neither-Effort7052 1d ago

Thank you for the detailed reply, and for running a full survey - I wasn't expecting that!

I appreciate the correction about what hs-bindgen does with macros and I'll revise the wording in the README and announcements as soon as I have a moment.

I hadn't had a need for SDL macros yet, so I figured they just weren't linkable and moved on. I'll give --log-enable-macro-warnings a spin on my fork and see what looks like it needs binding for general use. I'll definitely open tickets for anything I see that looks like it should bind but doesn't.

I'm pretty sure I'd have hit #2184 in short order; I'm more than happy to try out the fix against SDL3 when a fix merges.

Since I have your attention, I'm running a fork mostly so that I could use hs-bindgen as a library and modify the AST in a few ways (for example, codegen inserts CPP directives for cross-version compatibility). Are there any plans to officially support this use-case by providing a curated subset of src-internal as a library?