about::code - my personal blog march 2018

Creating a RAII-powered graphics stack (and what GLFW is currently missing)

This is the first part of my writedown about creating a new graphics stack architecture (using OpenGL/Vulkan and modern C++). The goal was to come up with something that is easy to use, exception-safe, requires the least amount of client-code and catches common bugs at compile-time. And of course: it should be equivally fast as a well-written plain old C equivalent ("zero cost abstraction"). This first part features a small summary of the API I came up with and why GLFW would require some small API changes [pull request].

 

RAII quickly became one of my favourite patterns of all time. Not only does it make your code exception-safe (which is indeed a very important benefit) but it allows you to write less code that is equally fast as the verbose equivalents while being more expressive and much more maintainable.

Many frameworks like GLFW are written in C for two main reasons: The first one is clearly having control and the ability to easily create bindings for different languages (since all current programming languages know how to call C functions). The goal of using such frameworks is to abstract the low-level (and platform dependent) details away such that the user can focus on solving the actual problem. While C is a powerful and universal language, it lacks many abstractions of modern languages that are often desired. But writing good bindings for a higher-level language is hard since the framework should feel native - which means that the bindings are required to blend perfectly into the target language by following all of its standards and idioms. IMHO those important idioms of modern C++ are RAII and a clear ownership model while providing control and flexibility with just the right degree of abstraction to enable maximal performance. Before discussing the framework itself, let's start by looking how initialization and teardown work with GLFW:

 

GLFW uses the calls glfwInit() for initialization and glfwTerminate() for termination which cleans up all ressources allocated by GLFW. We can provide an error-callback via glfwSetErrorCallback(...) before glfwInit() is called. Multiple calls to glfwInit() are perfectly fine, it will return immediately when it is already initialized. Calls to glfwTerminate() must only happen when all ressources (e.g. all GLFWwindows) were properly deallocated through the provided interface (glfwDestroyWindow(...)). This should be enough information to write a RAII-styled wrapper, right? Let's try it out!

 

I'll call this class glfw_context and use the ctor for initialization and the dtor for cleanup. I'll create another class called glfw_window that requires a reference to a const glfw_context  (along with other parameters like the resolution) in the ctor. The first 'trick' is to provide another overload that would take a a glfw_context rvalue reference (a temporary) and disable it by writing = delete;

This way we are sure that the user of our framework creates a glfw_context l-value before a glfw_window is created which means that the dtor of glfw_context (that means the call to glfwTerminate()) is only ever executed after the dtor of glfw_window (which uses RAII to deallocate the window by calling glfwDestroyWindow(...)). We're exception-safe and the code is clean:

 

        gfx::glfw_context glfw{ };
        gfx::glfw_window  window( glfw, config );

 

The compiler is now preventig the user from misusing the framework:

 

        gfx::glfw_window  window( gfx::glfw_context{ }, config );

 

will not compile (since it would use the deleted ctor discussed above). The reason why this is important is that the temporary glfw_context would not outlive the window and thus glfwTerminate() would be called before all windows were deallocated. Still this interface might not seem ideal because a user can still introduce bugs. Even if we were to disable all sorts of copy- or move-construction and assignment, a user could write the following code:

 

        gfx::glfw_context glfw{ };
        gfx::glfw_window  window( glfw, config );

        gfx::glfw_context{ } /* rvalue */ ;

 

 

As soon as the rvalue goes out of scope the destructor would be called and we would attempt to tear GLFW down before all windows were properly destroyed. The underlying problem is that neither the glfw_context called glfw nor the rvalue know if they really have initialized GLFW since if they called glfwInit() after GLFW was already initialized both would get the same successfull result. Of course we can misuse any framework and GLFW is no exception (we could do this in the C version as well). It's the user's responsibility to use the framework as intended. Still, let's see if we can do better!

 

Let's take a step back and look at the proposed solution which is a singleton since there can only be one instance of GLFW at a time. It might look like a valid solution at first but I claim a singleton never is. They can not be tested very well and introduce tight and hidden coupling. Yes, we can implement them in a idiomatic way in modern C++ with a predictable lifetime e.g. by using a static std::weak_ptr and and lock() it to obtain a std::shared_ptr to the actual ressource or allocate it if it is not available. However, this singleton pattern is still against one of C++'s strongest idioms: Do not pay for what you don't need - and in this case we would be allocating the context to make sure glfwTerminate() is only called once although that the bit of information (if GLFW is already initialized) is basically a duplicate of something that is already known inside GLFW (since it can immediately return if it has already been initialized). And there might be a second hidden allocation in this scenario less people know about: the weak_ptr is a static member of a function and because the C++ standard gurantees that the destructors of objects are run in opposite order than the constructors when the program exits, we need some sort of bookkeeping such that this guarantee can be fulfilled. If GLFW would provide us with this bit of information we would easily solve a whole class of problems as I'll describe below.

The required API change would be that GLFW either provides a way to query if it is already initialized or that the return value of glfwInit() indicates more information that tells us if this was the first successful initialization or if it returned immedeatly. IMHO querying that bit of information is a more stable solution since it would not break old client-code that assumes there are only two values that glfwInit() can return (which is not the case since the return-value is an int although only two values (GLFW_TRUE and GLFW_FALSE) are specified). Also, by querying we could potentially use that bit of information again during the teardown-stage.

If GLFW provided us with that bit a whole class of problems would vanish. The ctor of glfw_context could query if GLFW is already initialized and store that bit of information. When the dtor is run we know that we must not call glwfTerminate() if we did not initialize GLFW for either one of those reasons: Another glfw_context is already present which will do the cleanup since it has a longer lifetime or another framework/the user itself  is managing GLFW. With the singleton approach we would have no way to know about that and we would always attempt to call glfwTerminate() at the end and it would be more expensive. When we use the bit of information directly from GLFW even that scenario would still work!

In this case none of our instances initialized GLFW but none would attempt to tear it down. Our client-code would enforce this at compile-time and we know that GLFW has been initialized when we try to create the glfw_window since we provide a glfw_context in the ctor - and we know it will be terminated afterwards again. I'll go over how to handle errors safely in another blog-post, just assume this problem is solved nicely and we can only reach that state if no errors occured. So the problematic code above

 

        gfx::glfw_context glfw{ };
        gfx::glfw_window  window( glfw, config );

        gfx::glfw_context{ } /* rvalue */ ;

 

 

would now work without errors since it would not tear down GLFW anymore, is exception-safe and even handles cases implicitly where the user wants to call glfwInit() and glfwTerminate(). Also we do not need to make any allocations. Neat!

But this approach is still no silver bullet since there could still be some scenarios where one could misuse the framework (e.g. we create a std::vector< glfw_window > followed by the glfw_context and emplace_back() any window afterwards) without getting a compiletime error - and I'll cover my solution to this in a later post. For now I hope I could give some arguments why GLFW should really provide that bit of information in order to enable idiomatic C++ bindings and - even more importantly - I hope I was able to explain some basic concepts of how the framework is using RAII to achieve exception-safe code that is compact and for which resource lifetime issues are already detected at compile time. For more information, be sure to check this post.


or