A proof-of-concept framework built around plugins and seamless hot-reloading in C++. Cross-platform thanks to SDL, including building to web using Emscripten.
This was originally a pair of class projects. The framework for Game Engines & Implementation is the foundation that forms most of Sanable. It uses custom memory pools to track objects, allowing for cache-friendly update and render calls, as well as enabling the hot-reload feature. I used the Game Architecture project significantly less, but did reuse the asset management system.
Memory pooling makes almost every advanced feature of this project possible. The framework is oriented around game objects owning components, which can mark themselves as receivers for render and update calls. Each receiver class is then called in one big batch to better optimize for the CPU cache.
The hot-reload system wouldn’t be possible at all without a way to track all existing objects, which memory pooling handily provides. Plugins are written in pure C++ and compile to DLLs (or .so on linux/web), which makes the problem more difficult. When unloading and reloading a DLL, addresses may change, invalidating any pointers–which most importantly includes virtual function tables. However, almost every compiler stores vtable pointers at the start of the object. Even if we don’t know the type of a vtable, we know all pointers have the same width (either 4 or 8 bytes), so we can overwrite vtable pointers obtained from fresh instances.
Hot reloading v1
Hot reloading has a suite of problems to solve, but in perhaps its simplest version it must correct invalidated pointers. When a module is unloaded and loaded again, we cannot guarantee that it will appear in the same memory location. All compiler-generated RTTI in that module is now invalid, and by extension any polymorphic objects defined in that module are now ticking time bombs–we’d crash with an access violation exception the next time our program uses virtual functions,
typeid, because that object’s vptr now points to bad memory. Most compilers use the vtable for all type info, so Sanable v1 was a naive implementation that copied a vptr from a dummy object to all living objects:
typedef void* vptr_t; //Find correct vptr MyObject dummy; //Never used directly, just stealing its vptr vptr_t goodVptr = *reinterpret_cast<vptr_t*>(&dummy); //Overwrite bad vptr on existing object (badObj is a pointer) *reinterpret_cast<vptr_t*>(badObj) = goodVptr; badObj->update(); //No longer crashes!
This technique is called vptr jamming or pointer hydration. While it’s easy to implement and test, it isn’t consistent. With multiple inheritance or virtual inheritance, we’d have multiple vptrs to overwrite. Additionally, some compilers store metadata differently: MSVC adds an additional type info field right after the vptr. Finally, it completely neglects changes to class members.
Hot reloading v2
We can fix almost all those problems with reflection information detailing the members of every class. This is tedious to write by hand, as well as being fragile and near-uncheckable, so Sanable automatically generates those data structures from Clang’s AST with a custom tool that hooks into CMake as a pre-build step.
By knowing the location and size of each field (both inherited and explicit), we can deduce that the gaps are either implicitly-generated constants, or padding generated to respect alignment requirements. This allows us to not only capture vptrs, but any additional generated members, restoring functionality for compilers like MSVC.
Sanable takes this one step further, and deducing what is padding and what is an implicit field. We can create multiple dummy objects, first filling the memory they will occupy to some known value unique to that dummy. Compilers shouldn’t touch padding bytes (although some do), so the bytes that always match the known value must be padding.
Framework for Game Architecture
The class for this framework was just aimed at building the systems needed to support a game, and was built on Allegro (and later transitioned to SDL). I built it with support for multiple programming styles and a focus on quality of life. For example, the logging system includes timestamps and color coded log levels for easier debugging.
The framework accomplishes two forms of resource streaming through RAII. Reference counting wrappers load assets such as texture sheets and sounds when needed, and can unload them as soon as all objects using that resource release it. Almost every referenceable asset is a descriptor file, which also specifies any other assets it may depend on, such as a sprite needing the texture sheet it is contained in. Allegro’s addons are also handled through RAII, initializing and cleaning up modules like sound and fonts.
The input system receives events from Allegro’s buffer and supports polling for key presses as well as pushing events using both functional hooks and inheritance-based listeners.
To field-test, I implemented a game in the style of Snake with colored console graphics, and a target practice game with full image graphics.