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.
winit? Basically, as described
here, I feel
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.
This is mainly intended to be a straight port of
there's a few other things in this category to consider, if only for
sokol_app.h actually works pretty well!
This is a promising line of inquiry but has some problems:
sokol_appis 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
c2rustis not trivial, it takes a fair bit of pipeline setup to do and yet more to do well
c2rustoccasionally still needs some massaging by hand to compile. Not usually very much, at least.
c2rustsays 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!
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
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
x11-dl and dynamically load the GLX functions and definitions I
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
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,
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
winit could be.
Glutin is even worse though! The
glutin_*_sys crates actually look
really useful as low-level platform bindings, without those
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,
glutin. But eventually it became desirable to separate windowing
and graphics context setup, and windowing was refactored into what is now
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
which just uses
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!