Super Early Global Variables in C++
I've had a problem for a while now where the code I'm writing in C++ can potentially run before the global constructor list is invoked by libc.
Let's imagine, for example, that you are building a memory allocator to replace malloc
:
class Heap {
Heap() { printf("Heap Constructed!\n"); }
// ...
};
static Heap the_heap;
extern "C" void *malloc(size_t size) {
return the_heap.malloc(size);
}
Libc will call malloc
before the_heap
is constructed, and as a result will likely be operating on uninitialized (likely zero) data.
One possible approach to solving this is to do the following:
static Heap *the_heap = nullptr;
extern "C" void *malloc(size_t size) {
if (the_heap == nullptr) the_heap = new Heap();
return the_heap->malloc(size);
}
This should work! But we get this output:
$ ./a.out
Program received signal SIGSEGV, Segmentation fault
This is because we are calling new
from within malloc
and under the hood, new
is just a wrapper around malloc
, so we end up infinitely recursing and eventually overflow the stack.
So we need to get the memory from somewhere else:
#include <new> // for placement new, below
static Heap *the_heap = nullptr;
static uint8_t heap_storage[sizeof(Heap)];
// We'll put this behind a function now.
static Heap &getHeap(void) {
if (the_heap == nullptr) {
the_heap = new (heap_storage)(); // Placement new!
}
return *the_heap;
}
We'll use placement new, which allows us to tell new
, "Don't allocate your own memory, use mine instead".
Instead, we place the object in the statically allocated heap_storage
block, which is in .bss.
Obviously doing this each time for all your globals can be a pain, so I wrap this up into a struct:
template<typename T>
class LazyGlobal {
T *m_instance; // no initializer!
uint8_t m_storage[sizeof(T)];
public:
T &get() {
if (m_instance == nullptr) {
m_instance = new (m_storage) T();
}
return *m_instance;
}
T &operator*() {
return get();
}
T *operator->() {
return &get();
}
};
Note that m_instance
has no initializer, which is "okay" because this type is a PoD struct, and when a global variable takes this type, all of its fields are initialized to zero by virtue of being loaded from .bss
.
That said, with that type we can now do this:
LazyGlobal<Heap> the_heap;
extern "C" void *malloc(size_t size) {
return the_heap->malloc(size);
}
Its important that the LazyGlobal
type is a plain-old-data, and the class it is managing has a trivial (or at least zero-argument) constructor.
If the LazyGlobal
struct isn't PoD
, its possible the object could be constructed multiple times: once when you call get()
before the global constructors are invoked, and once after.
Anyways, this class solved a longstanding problem I've had, so I thought I'd share :)