Since this problem can not be explained in a handful of lines, please bear with me and the size of this question.
The situation
We are developing an embedded system, which needs to manage its heap on its own by replacing the operators new
, new[]
, delete
, and delete[]
. These user-defined replacing functions are implemented in their own module.
We decided to use a class with static methods, we could as well have used a namespace to keep the global namespace uncluttered.
// allocator.h
#include <cstddef> // for size_t
class Allocator
{
public:
static void setup();
static void* allocate(size_t size);
};
Since during run-time the allocated instances will not be deleted, we prohibit the rest of the application from calling delete
by shutting down the system otherwise. The testing framework can intercept the shutdown, so this is already testable.
// allocator.cpp
#include "allocator.h"
static char* baseAddress = nullptr;
static size_t spaceLeft = 0;
void Allocator::setup()
{
static char heap[1000]; // super-simple for StackOverflow example
baseAddress = heap;
spaceLeft = sizeof heap;
}
void* Allocator::allocate(size_t size)
{
void* p = 0;
if (size <= spaceLeft)
{
p = static_cast<void*>(baseAddress);
baseAddress += size;
spaceLeft -= size;
}
return p;
}
void* operator new(size_t size)
{
void* p = Allocator::allocate(size);
return p;
}
void* operator new[](size_t size)
{
void* p = Allocator::allocate(size);
return p;
}
void operator delete(void*)
{
// shutdown(); // commented out for StackOverflow example
}
void operator delete[](void*)
{
// shutdown(); // commented out for StackOverflow example
}
Actually the RAM for the heap is allocated differently, but this does not matter here.
To unit-test this module, we are using GoogleTest, but the specific testing framework does not really matter. We could use any other framework.
The following source is a simulation of a testing framework I made up to investigate the problem. It uses the global operators new
and delete
, and these shall not be replaced by the user-defined operators from above, of course. Otherwise the framework would try to allocate new objects on a heap that does not yet exist.
// framework.cpp
#include <cstdlib> // for malloc() and free()
#include <iostream>
#if 0 // For the example to compile and link, currently commented out
void* operator new(size_t size)
{
void* p = malloc(size);
std::cout << __func__ << "(" << size << ") : " << p << std::endl;
return p;
}
void* operator new[](size_t size)
{
void* p = malloc(size);
std::cout << __func__ << "(" << size << ") : " << p << std::endl;
return p;
}
void operator delete(void* block)
{
std::cout << __func__ << "(" << block << ")" << std::endl;
free(block);
}
void operator delete[](void* block)
{
std::cout << __func__ << "(" << block << ")" << std::endl;
free(block);
}
#endif
void framework()
{
int* p1 = new int;
std::cout << __func__ << " p1 = " << static_cast<void*>(p1) << std::endl;
int* p2 = new int[4];
std::cout << __func__ << " p2 = " << static_cast<void*>(p2) << std::endl;
delete p1;
delete[] p2;
}
And of course, its header file, for your convenience.
// framework.h
void framework();
Here is the testdriver, simplified for this example. It calls framework stuff (in the real situation behind the scene), tests failing and succeeding allocations with the module-under-test, and calls framework stuff again.
// testdriver.cpp
#include <iostream>
#include "framework.h"
#include "allocator.h"
int main()
{
framework();
#if 1 // possibility to comment out for experiments
int* p1 = new int; // expected to be 0
std::cout << __func__ << " p1 = " << static_cast<void*>(p1) << std::endl;
int* p2 = new int[4]; // expected to be 0
std::cout << __func__ << " p2 = " << static_cast<void*>(p2) << std::endl;
#endif
Allocator::setup();
#if 1 // possibility to comment out for experiments
p1 = new int;
std::cout << __func__ << " p1 = " << static_cast<void*>(p1) << std::endl;
p2 = new int[4];
std::cout << __func__ << " p2 = " << static_cast<void*>(p2) << std::endl;
delete p1;
delete[] p2;
#endif
framework();
return 0;
}
The standard to use for the software to develop is C++98, as we are bound to such an ancient compiler.
The standard to use for the tests is C++11, as GoogleTest needs this as minimum.
These are the commands to compile and link:
g++ -Wall -Wextra -pedantic -std=c++11 -c allocator.cpp -o allocator.o
g++ -Wall -Wextra -pedantic -std=c++11 -c framework.cpp -o framework.o
g++ -Wall -Wextra -pedantic -std=c++11 -c testdriver.cpp -o testdriver.o
g++ -Wall -Wextra -pedantic -std=c++11 testdriver.o framework.o allocator.o -o testdriver
First solution, polluting the module's source with testing artefacts
My first idea was to insert these conditionally compiled lines into the module's source.
// allocator.cpp
//...
#if !defined(TESTING)
#define operator_new_single operator new
#define operator_new_array operator new[]
#define operator_delete_single operator delete
#define operator_delete_array operator delete[]
#endif
//...
void* operator_new_single(size_t size) // void* operator new(size_t size)
{
// ...
}
void* operator_new_array(size_t size)
{
// ...
}
void operator_delete_single(void*)
{
// ...
}
void operator_delete_array(void*)
{
// ...
}
To compile for testing, I used:
g++ -Wall -Wextra -pedantic -std=c++11 -c -DTESTING allocator.cpp -o allocator.o
Now the testdriver can simply call these functions, because they are no operators any more.
But our safety staff said "No way!" And I have to agree. Testing instrumentation in safety-related software is dangerous because it could sneak into the final product. You simply don't do this.
Second solution, fragile due to dependencies on the compiler and its version
We are using the GCC in its incarnation of MinGW64, so I came up with the linker option -wrap
. In a single sentence: This option makes the linker prepend __wrap_
to symbols at the calling site and to prepend __real_
at the called site.
So I looked up the mangeled names of the operators, since the linker does not know anything about C++; it just does not need to know. ;-) Well, G++ in the version we use has this "translation":
_Znwy := operator new(unsigned long long)
_Znay := operator new[](unsigned long long)
_ZdlPv := operator delete(void*)
_ZdaPv := operator delete[](void*)
Now I could extend the testdriver with replacements for the operators, working with the C memory allocators. (Thank you C++ guys, for leaving that stuff in the libraries!)
// testdriver.cpp
#include <cstring> // for malloc() and free()
// ...
extern "C" void* __wrap__Znwy(size_t size)
{
return malloc(size);
}
extern "C" void* __wrap__Znay(size_t size)
{
return malloc(size);
}
extern "C" void __wrap__ZdlPv(void* block)
{
free(block);
}
extern "C" void __wrap__ZdaPv(void* block)
{
free(block);
}
// ...
And these are the declarations of the real operators in the module-under-test, for the testdriver to call.
// testdriver.cpp
// ...
extern "C" void* __real__Znwy(size_t size);
extern "C" void* __real__Znay(size_t size);
extern "C" void __real__ZdlPv(void* block);
extern "C" void __real__ZdaPv(void* block);
// ...
The command to link is now:
g++ -Wall -Wextra -pedantic -std=c++11 -Wl,-wrap,_Znwy,-wrap,_Znay,-wrap,_ZdlPv,-wrap,_ZdaPv testdriver.o framework.o allocator.o -o testdriver
This worked, too. But it is kind of complicated and ugly. And it works only with the GCC, additionally I'm not sure that different versions will keep these mangeled names. Most probably they do, to be compatible, but who knows.
My single question
Thanks for reading all of this, here comes my question:
What else can I try?
I'm looking for a solution that does not change the module's source, and that works with (mostly) any compiler.