Wrestle Legacy C and C++ into a Test Harness – Linker Errors

Getting started with TDD in C and C++ is challenging. On top of that, you have your whole product’s code base to start adding tests to. You don’t have time to stop all development and add tests to your code, so you need a pragmatic approach. As you drag your legacy code, kicking and screaming into a test harness, take your time and solve one problem at a time. It is the fast way.

This article is about getting past a boat-load of C and C++ linker errors. I’ve got a method and a tool to help get through that challenging step. You can find the tool, gen-xfakes, here on my GitHub account.

What’s the Problem?

Let me paint a picture; imagine that you have a 5000 line embedded C or C++ file. You need to change some function that is about 100 LOC. If you think you need to fully test the 5000 LOC you will probably rationalize doing nothing about test automation and give up. The pragmatic approach to getting legacy C and C++ under test is to write some tests for the area of code you are about to change, not the whole compilation unit. The code has never been compiled for anything but the target. Embedded C and C++ can collect some pretty difficult dependencies. Your may not work in embedded, but you too have target dependencies and this article could help with your dependency problems.

Following the recipe from Get Your Legacy C into the Test Harness, you manage to compile your code’s header file in a test case, and then to compile the file in the off-target test environment. You wanted to give up a while ago but stuck with it. Your reward may be hundreds of linker errors complaining about undefined external references. The linker errors are not necessarily all unique, but you may need dozens of test stubs to get rid of all the errors. Only then can you start to exercise your code in a test harness.

Don’t let the pages of linker errors get you down. Only some of the linker errors are for functions called by the function you are changing. For those dependencies you will need test-doubles that let you check your code’s behavior. But we’re getting ahead of ourselves. You need to resolve all linker errors! Don’t be discouraged. I’ve run into this a lot facilitating legacy code workshops. The multitude of linker errors is discouraging and tedious to resolve.

A few years ago, on a train from Munich (work) to Switzerland (play), I wrote a simple script to convert linker errors into exploding fakes. An exploding fake is a test-double that when executed, announces itself and terminates the test case or entire test run. The script has evolved since then and has a name, gen-xfakes.

In a legacy code situation like this, you want a pragmatic approach to getting difficult code under test. You want to solve one problem at a time. You want to avoid batches of changes. You want to only make changes where you can see the effect. After header file dependencies have been chased down, the next problem is linker errors. Before we worry about the perfect test-double in each situation, let’s get the least effort test-double in place.

C link errors are the easiest to satisfy. The creation of C exploding fakes can be fully automated. C++ errors are a different story, so let’s look at C linker errors first.

C Linker Errors to Exploding Fakes

A simple C exploding fake can be created with these macros.

#define BOOM_MESSAGE printf("BOOM! time to write a better fake for %s\n", __func__) 
#define EXPLODING_FAKE_FOR(f) void f(void); void f(void) { BOOM_MESSAGE; exit(1); }

This implementation calls exit, so the first executed exploding fake terminates the test runner. (You could define your own exploding fake using your test runner’s fail mechanism and just fail the one test.)

Here is a small example of C linker error output.

Linking HomeAutomation_tests
objs/LightScheduler.o: In function `LightScheduler_AddTurnOn':
/sandbox/LightScheduler.c:86: undefined reference to `LightController_On'
objs/LightScheduler.o: In function `LightScheduler_AddTurnOn':
/sandbox/LightScheduler.c:88: undefined reference to `g_lightSchedule'
objs/LightScheduler.o: In function `LightScheduler_AddTurnOff':
/sandbox/LightScheduler.c:91: undefined reference to `LightController_Off'
objs/LightScheduler.o: In function `LightScheduler_AddTurnOff':
/sandbox/LightScheduler.c:93: undefined reference to `g_lightSchedule'
lib/libHomeAutomation.a(Scheduler.o): In function `Scheduler_Create':
/sandbox/Scheduler.c:37: undefined reference to `TimeService_SchedulePeriodicAlarm'
lib/libHomeAutomation.a(Scheduler.o): In function `Scheduler_Destroy':
/sandbox/Scheduler.c:53: undefined reference to `TimeService_CancelPeriodicAlarm'
lib/libHomeAutomation.a(Scheduler.o): In function `processReadyEvents':
/sandbox/Scheduler.c:70: undefined reference to `TimeService_DaysMatch'
lib/libHomeAutomation.a(Scheduler.o): In function `Scheduler_Wakeup':
/sandbox/Scheduler.c:79: undefined reference to `TimeService_GetDay'
/sandbox/Scheduler.c:80: undefined reference to `TimeService_GetMinute’
.. etc ...
collect2: error: ld returned 1 exit status

gen-xfakes script turns that linker error output into a ready to compile file of exploding fakes.

// xfakes-c.c
#include <stdio.h>
#include <stdlib.h>
 
#define BOOM_MESSAGE printf("BOOM! time to write a better fake for %s\n", __func__)
#define EXPLODING_FAKE_FOR(f) void f(void); void f(void) { BOOM_MESSAGE; exit(1); }
#define NULL_VOID_FAKE_FOR(f) void f(void); void f(void) {}
#define NULL_VALUE_FAKE_FOR(value_type, f, result) value_type f(void); value_type f(void) { return result; }
 
EXPLODING_FAKE_FOR(g_lightSchedule)
EXPLODING_FAKE_FOR(LightController_On)
EXPLODING_FAKE_FOR(TimeService_CancelPeriodicAlarm)
EXPLODING_FAKE_FOR(TimeService_DaysMatch)
EXPLODING_FAKE_FOR(TimeService_GetDay)
EXPLODING_FAKE_FOR(TimeService_GetMinute)
EXPLODING_FAKE_FOR(TimeService_SchedulePeriodicAlarm)</stdlib.h></stdio.h>

Do not include the function declarations in this file. C compilers and linkers don’t care about the parameters or return types so a test-double can be created pretty simply as a void function with no parameters. The script creates an exploding fake for C globals as well (write to them and they will explode!). It would be nice to separate functions and globals but there is no way to distinguish undefined globals from undefined functions in the C linker error output.

The macros NULL_VOID_FAKE_FOR and NULL_VALUE_FAKE_FOR are for later when you need do-nothing test-doubles.

C++ Linker Errors to Exploding Fakes

C++ linker errors are not so easy. The C++ linker knows more about the code being linked than C. In C++, function name overloading means the same function name can be repeated as long as the parameter types are different. You can see the linker errors include the parameter types when the symbol is not referring to pure C. C++ scoped global variables are also distinguishable from C global variables.

Here is example gcc linker error output.

Building build/test_WebServer
g++ -Wall -DQNX_BUILD -Wno-narrowing -DEOK=0 -DTEST -Wfatal-errors -D__LITTLEENDIAN__ -D__X86__ -D__LINUX__ -D__i386 -include forced_include.h -I../../../include/catch -I../ -I./ -I./fake_includes -I /usr/include/ -I../../..//Libraries/AcmeDatabase/exports -I../../..//Libraries/AcmeWpa/exports -I../../..//Libraries/AcmeSocket/exports -I../../..//Libraries/AcmeCrypto/exports -I../../..//Libraries/AcmeJSON/exports -I../../..//Libraries/AcmeLog/exports -I../../..//Libraries/AcmeNeutrino/exports -I../../..//include ../HardConnect.cpp -o build/test_WebServer ./build/test_WebServer.o
../HardConnect.cpp: In function ‘bool Cloudserver1LinkRequest(const string&amp;, const string&amp;, std::__cxx11::string*)’:
../HardConnect.cpp:510:64: warning: format ‘%d’ expects argument of type ‘int’, but argument 4 has type ‘std::__cxx11::basic_string<char>::size_type {aka long unsigned int}’ [-Wformat=]
snprintf(tempBuffer, sizeof( tempBuffer ), "%d", body.length());
^
../HardConnect.cpp:510:64: warning: format ‘%d’ expects argument of type ‘int’, but argument 4 has type ‘std::__cxx11::basic_string<char>::size_type {aka long unsigned int}’ [-Wformat=]
/tmp/xxxxxx.o: In function `recordWpaConnected(std::__cxx11::basic_string<char, std::char_traits<char="">, std::allocator<char> &gt; const&amp;)':
HardConnect.cpp:(.text+0x20): undefined reference to `AcmeWpa::clockGetSeconds()'
HardConnect.cpp:(.text+0x5a): undefined reference to `log_it'
HardConnect.cpp:(.text+0x19c): undefined reference to `AcmeWpa::clockGetSeconds()'
/tmp/xxxxxx.o: In function `applyTimeZoneUTCOffset(std::__cxx11::basic_string<char, std::char_traits<char="">, std::allocator<char> &gt;, std::__cxx11::basic_string<char, std::char_traits<char="">, std::allocator<char> &gt;)':
HardConnect.cpp:(.text+0x643): undefined reference to `AcmeUpdateTZEnv(char const*, char const*)'
HardConnect.cpp:(.text+0x6fc): undefined reference to `AcmeWpa::clock'
HardConnect.cpp:(.text+0x72b): undefined reference to `AcmeWpa::restartDhcp(double, std::__cxx11::basic_string<char, std::char_traits<char="">, std::allocator<char> &gt;*)'
HardConnect.cpp:(.text+0xd44): undefined reference to `AcmeDatabase_SetCloudserver1Linked(bool)'
HardConnect.cpp:(.text+0xd80): undefined reference to `log_it'
HardConnect.cpp:(.text+0xd9a): undefined reference to `AcmeDatabase_GetCloudserver1Linked(bool*)'
HardConnect.cpp:(.text+0xdd8): undefined reference to `log_it'
HardConnect.cpp:(.text+0xe06): undefined reference to `AcmeRegistry::Write(char const*, char const*)'
HardConnect.cpp:(.text+0xe06): undefined reference to `AcmeRegistry::instance'
HardConnect.cpp:(.text+0xebb): undefined reference to `AcmeCrypto_RemoveKeyFile(char const*)'
HardConnect.cpp:(.text+0x24e3): undefined reference to `pthread_setname_np'
undefined reference to `myJSON_GetObjectItem'
HardConnect.cpp:(.text._ZN32JsonHardConnectInitializeRequest15getNestedStringERKNSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEES7_PS5_[_ZN32JsonHardConnectInitializeRequest15getNestedStringERKNSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEES7_PS5_]+0x8c): undefined reference to `myJSON_IsString'
/tmp/xxxxxx.o: In function `JsonCloudserver1LinkRequest::JsonCloudserver1LinkRequest()':
HardConnect.cpp:(.text._ZN22JsonCloudserver1LinkRequestC2Ev[_ZN22JsonCloudserver1LinkRequestC5Ev]+0x18): undefined reference to `myJSON_CreateObject'
/tmp/xxxxxx.o: In function `JsonCloudserver1LinkRequest::~JsonCloudserver1LinkRequest()':
HardConnect.cpp:(.text._ZN22JsonCloudserver1LinkRequestD2Ev[_ZN22JsonCloudserver1LinkRequestD5Ev]+0x17): undefined reference to `myJSON_Delete'
basic_stringIcSt11char_traitsIcESaIcEEES7_]+0x44): undefined reference to `myJSON_AddStringToObject'
/tmp/xxxxxx.o: In function `AcmeRegistry::GetInstance()':
HardConnect.cpp:(.text._ZN13AcmeRegistry11GetInstanceEv[_ZN13AcmeRegistry11GetInstanceEv]+0x32): undefined reference to `AcmeRegistry::AcmeRegistry()'
HardConnect.cpp:(.text._ZN13AcmeRegistry11GetInstanceEv[_ZN13AcmeRegistry11GetInstanceEv]+0x4b): undefined reference to `AcmeRegistry::~AcmeRegistry()'
collect2: error: ld returned 1 exit status
make: allerrors.txt a.out build cpputest_build.sh cpputest_main.cpp cpputest_WebServer.cpp errors.txt fake_includes forced_include.h Makefile test_WebServer.cpp [build/test_WebServer] Error 1</char></char,></char></char,></char></char,></char></char,></char></char>

Running that error output file through gen-xfakes produces three files. For example:

File What is it for?
xfakes-c.c C linkage fakes, ready to add to your build
xfakes-cpp.cpp C++ linkage fakes, edits needed
xfakes-cpp-globals.cpp C++ undefined globals, edits needed

As with straight C, C linkage undefined references are converted to exploding fakes. The file ready to compile and link.

// xfakes-c.c
#include <stdio.h>
#include <stdlib.h>
 
#define BOOM_MESSAGE printf("BOOM! time to write a better fake for %s\n", __func__)
#define EXPLODING_FAKE_FOR(f) void f(void); void f(void) { BOOM_MESSAGE; exit(1); }
#define NULL_VOID_FAKE_FOR(f) void f(void); void f(void) {}
#define NULL_VALUE_FAKE_FOR(value_type, f, result) value_type f(void); value_type f(void) { return result; }
 
 
EXPLODING_FAKE_FOR(log_it)
EXPLODING_FAKE_FOR(myJSON_AddStringToObject)
EXPLODING_FAKE_FOR(myJSON_CreateObject)
EXPLODING_FAKE_FOR(myJSON_Delete)
EXPLODING_FAKE_FOR(myJSON_GetObjectItem)
EXPLODING_FAKE_FOR(myJSON_IsString)
EXPLODING_FAKE_FOR(pthread_setname_np)</stdlib.h></stdio.h>

C++ class or namespace scoped functions present a problem. We can’t just define a function as a member of a class or namespace without preceding it with its function declaration. Header files are needed. To save a lot of typing, the linker error is converted to a commented out list of function definitions. They won’t cause any new compiler errors (that’s a good thing) or resolve linker errors until to do some editing.

#include <stdio.h>
#include <stdlib.h>
 
#define BOOM_MESSAGE printf("BOOM! time to write a better fake for %s\n", __func__) 
#define BOOM_VOID_CPP BOOM_MESSAGE; exit(1);
#define BOOM_VALUE_CPP(result) BOOM_MESSAGE; exit(1); return result;
 
/*
*   Production code header files
*/
 
// #include "your.h"
 
// void AcmeCrypto_RemoveKeyFile(char const*) { BOOM_VOID_CPP }
// void AcmeDatabase_GetCloudserver1Linked(bool*) { BOOM_VOID_CPP }
// void AcmeDatabase_SetCloudserver1Linked(bool) { BOOM_VOID_CPP }
// void AcmeRegistry::AcmeRegistry() { BOOM_VOID_CPP }
// void AcmeRegistry::Write(char const*, char const*) { BOOM_VOID_CPP }
// void AcmeRegistry::~AcmeRegistry() { BOOM_VOID_CPP }
// void AcmeUpdateTZEnv(char const*, char const*) { BOOM_VOID_CPP }
// void AcmeWpa::clockGetSeconds() { BOOM_VOID_CPP }
// void AcmeWpa::restartDhcp(double, std::__cxx11::basic_string<char, std::char_traits<char="">, std::allocator<char> &gt;*) { BOOM_VOID_CPP }</char></char,></stdlib.h></stdio.h>

To make this file helpful, you will have to add header files to xfakes-cpp.cpp. You may also have to change the return type and use BOOM_VALUE_CPP(result) in place of BOOM_VOID_CPP.

Because C++ errors are very noisy and sometimes misleading, I suggest you introduce the needed header files one at a time. Avoid batch mode uncommenting until you have a few wins under your belt and it feels repetitive. Then try to get it right with one try. (That said, batch mode might not end well depending on the mistakes you make doing batch editing.)

C++ functions and global variables look different in the linker error output which allows gen-xfakes to produce a file just of C++ linked global variables. Unfortunately the type for the variable is not known, so you’ll have to edit this file into submission as well.

// xfakes-cpp-globals.cpp
 
//cpp-global AcmeRegistry::instance;
//cpp-global AcmeWpa::clock;

You’ll add associated header files and replace cpp-global with the actual type information and add needed initializers. I’d resist batch mode here.

Once you have solved the linker problem, you can move on to running your legacy code. As you drive your code through different paths, you’ll stop the explosions, one at a time, converting the exploding fakes into some other test-double.

Using this incremental approach with gen-xfakes can make a tedious job go more quickly and help you really understand your legacy code. I say getting your legacy code into a test environment helps you become qualified to make changes to that code. Otherwise you are just guessing.


The original article on this topic, Crash to Pass, has a step called makeItLink(). Depending how much of a dependency magnet you are trying to get under test, makeItLink() could take a while. The problems with makeItLink() lead to gen-xfakes and this article.

The high level recipe for getting C and C++ under test is described here in Get Your Legacy C into the Test Harness. You’ll find references to many other articles that can help you get your legacy C and C++ into a test harness.

Leave a Reply

Your email address will not be published. Required fields are marked *

Be gone spammers * Time limit is exhausted. Please reload the CAPTCHA.