#cross-platform #windowing #api-bindings

bin+lib winflip

An experiment in making a small light-weight window-setup library

1 unstable release

0.1.0 Jan 10, 2020

#930 in GUI

MIT license

65KB
1.5K SLoC

winflip

A Rust-y investigation into making something like https://github.com/floooh/sokol.

This is an EXPERIMENT. I just want to try porting sokol_app.h to Rust to learn more about how minimalistic one can make a cross-platform window setup lib. This may someday become worth using, or maybe it won't. Currently it only runs on Linux+X11, though it's designed to have more backends added as necessary.

Why not winit? Basically, as described here, I feel like winit tries to do too many things perfectly on too many platforms, and so I want to explore what something with more limited goals would look like.

Goals

  • Works on Windows, Linux, and WebGL+WASM
  • Sets up window and processes events
  • Sets up OpenGL context
  • Smallish
  • Easy to use for games

Anti-goals

  • Multiple windows
  • Multiple OpenGL contexts
  • Any particular style of event loop
  • Easy to use for native GUI apps
  • Multithreading
  • Perfection

Things to not worry about YET

  • Gamepad support
  • Mobile support

Prior art

This is mainly intended to be a straight port of sokol_app.h but there's a few other things in this category to consider, if only for contrast:

  • winit
  • glutin
  • minifb
  • pixels

Also, running c2rust on sokol_app.h actually works pretty well! This is a promising line of inquiry but has some problems:

  • sokol_app is actively developed so we'll have to do this multiple times to incorporate future bugfixes -- can't just run it once and make it work, have to automate it to make it always work
  • Running c2rust is not trivial, it takes a fair bit of pipeline setup to do and yet more to do well
  • The output of c2rust occasionally still needs some massaging by hand to compile. Not usually very much, at least.
  • c2rust says it doesn't yet support Windows? And doesn't support cross-compiling either, since it can't set and unset all the magical platform-specific ifdef's that might exist in the world. Troublesome!

Current state and thoughts

As an experiment or "spike" this is more or less complete. After about four days of work it creates a window on Linux using X11, runs an event loop that calls user-provided callbacks correctly, and exits properly. It doesn't have some features implemented (notably window title, hidpi and clipboard support), and its GL context creation is buggy, but it works if you comment out the GLX setup functions in x11::run(). It's also basically prototype state in terms of error handling and doesn't let the user control the event loop, just the callbacks. Still, it creates a window and runs an event loop, which is like 80% of what it needs to do, and is structured so that other backends than X11 can be added pretty easily. Most of the code is unsafe and unaudited, but making a safe interface should be pretty simple.

In terms of dependencies, the x11-dl crate provides most of what we need for this. glutin_glx_sys provides a different subset of most of what we need, arranged just differently enough that it's not a drop-in replacement; it has more of the GLX stuff and less of the X11 stuff. Sorry, I don't remember the exact details. So for now I just use x11-dl and dynamically load the GLX functions and definitions I need myself.

Porting sokol_app.h was kinda weird but really pretty straightforward all in all. I'm sure there are a number of various windowing lib edge cases it doesn't handle but I couldn't find anything obvious. X11 doesn't rear much of its reputed ugliness in this, it's just fairly mundane clunky old C code, though some things like error handling are pretty bad. If you chopped out 90% of X11 that isn't used anymore and actually documented what was left, it probably wouldn't have the gruesome reputation it does. sokol_app.h itself is quite good C code, excepting the usage of static globals heckin' everywhere, but at least they're named consistently. So, the result is pretty easy to follow and port to Rust. Porting C to Rust is once again an exercise in remembering just how crap C really is in comparison. It's okayish by 1980's standards, but we can do far, far, far better now. RAII, specific integer types that aren't transparently convertable to each other, some basic traits like Clone and Drop, and real enum types make life SO damn much better, especially when you have API's that are designed to actually use these features. Really the biggest awfulness of C IMO is its freakish willingness to say "yeah or that can be an int and that's fine" to just about any type, which makes it REALLY HARD to build any kind of strong abstract types. That, and the criminal lack of standard library: a portable program should not have to write its own strncmp() and assert(). Ugh.

It is interesting seeing how this stacks up against winit, as well. winflip is about 2000 lines of code, and would probably be 2500 were it finished. If each backend adds another 2000-3000 lines, then supporting Wayland, Windows, MacOS, and wasm+web-sys, that would probably be 15k significant LOC in total. As of Jan 2020, winit is about 35k lines, and the maintainers themselves are not necessarily thrilled with how complex it is. I feel that winflip serves as a useful data point in how lightweight something that performs the main functions of winit could be.

Glutin is even worse though! The glutin_*_sys crates actually look really useful as low-level platform bindings, without those glutin seems to be 9000 lines of code that actually does very little. The reason for this is historical: Back In The Day, winit didn't exist, just glutin. But eventually it became desirable to separate windowing and graphics context setup, and windowing was refactored into what is now winit. I think it would be desirable to chop a bunch of the fluff out of glutin and make it a much more slender library that only does graphics context setup, and the raw-window-handle crate now means that's possible to do. The actual OpenGL setup in sokol_app.h, apart from all the DLL loading that is be handled by the glutin_*_sys crates, is only a few hundred lines of code. Any volunteers?

A caveat, of course lines of code is not a good proxy for complexity. But it's also all we've got. Assuming that nobody's trying to be gratuitously arcane or verbose, we can at least broadly assume that it's some sort of indirect indicator of how much stuff a program or library has in it.

One last thing... winit's "event loop 2.0" refactor is complicated enough that it takes a fair amount of work to explain, even to people who've done game or UI dev before and know what's going on under the hood. The reason for this is basically that it tries to present one API that works with callback-based API's such as web browsers and Android, AND with the more traditional poll-events-in-a-loop API's like X11 or Windows. Again, nobody's really thrilled with this but nobody's had a better idea for structuring it that doesn't sacrifice capabilities that are important to someone. I personally find winflip's setup, which just uses frame(), update() and event() callbacks called in a loop, to be far nicer to actually use and use correctly, and integrate into other systems, but it DOES sacrifice things like smooth resizing, some latency concerns with frame drawing, stuff like that. For my purposes, these are sacrifices I make gladly.

It's weird to contemplate WHY I like winflip's setup more, because I'm not really sure. When you take a step back it's obvious that both styles are equivalent, you can turn a polling event loop into callbacks just by providing the event loop that calls callbacks, or do the inverse by making the callbacks collect events into a shared queue. I THINK that the different "feeling" because the way winit does it, very concrete events such as "user pressed key" are interspersed with much more abstract events like "frame started" or "event loop cleared" which are... really not events but rather state changes in the event loop itself. Then when you mix in the way it uses ControlFlow to provide feedback about what to do next... It ends up being a somewhat weird-feeling intertwingled state machine disguised as an event callback. I still don't really know for sure though.

So yeah, this was fun!

Dependencies

~115KB