on
C++
Introduction
C++ is one of the few languages that can incite as much debate as an editor holy war. However, it resolutely holds it’s position as a state-of-the-art lingua franca, losing in ubiquity only to it’s predecessor C. While it’s easy to bemoan the warts of the language, it’s important to understand that the modernization of C++ since the advent of C++11 (which continues through refinements introduced in C++14 and upcoming additions to C++17), has truly changed the game (ref. The Beast is Back).
Historically, C++ has not been kind to interested developers. The awkward mix of various Make build utilities, opaque compilation toolchains, obscure flags, and more provide both a immense configurability for nearly any platform and a absolute swamp of complexity.
My hope with this article is to provide you, the reader, with a decent foothold from which to begin. You might be writing shared libraries for mobile apps, high performance simulations, graphics engines for consoles, or even embedded code for a remote control car. Regardless, while I may not have time or space to delve into every nook and cranny now, you should feel comfortable using what you learn here as a starting point. It’s worth noting that this is not meant to be a language tutorial or language introduction even. Think of it more as a whirlwind tour of a workflow to making several different parts that interact with each other in a non-trivial fashion. For learning C++, I have a few references listed at the end. However, here’s a rough outline of what we will cover:
- The objective: What are we going to get by the end of this article?
- The executable: How are C++ programs compiled and how do they run?
- The build process: How do we set up our project for multiple platforms and configurations?
- The implementation: How do we implement the project?
- Some refinements: Touching up our implementation and discussing other improvements.
- Testing and publishing: How do we make our library testable and package it?
- Notes on writing code: How do I typically author C++ code?
- The wrap-up: Where do I go from here?
The objective
Our end goal is a self-contained project containing two parts. A shared library, and a runtime we can use to test the library (hosted as the project RePlex). The function of the library is to act as a live code-reloader, which is extremely useful for live development (and potentially a surprise for people that didn’t realize C++ could do this). This way, we’ll cover how to make a reusable library across other executables, link that library to a sample executable, and cover a cool technique which needs to account for the host platform. Neat, let’s get started!
The executable
First, we need to understand the basic building blocks of a C++ executable (also known as a binary executable or just binary for short). While this might seem overly pedantic, it actually isn’t as complicated as you might think (if you skip the less interesting parts). Knowledge about these building blocks will give us a common pool of terminology and concepts we’ll use throughout the rest of the article. If this is something you’re already familiar with, feel free to skip ahead to the next section.
C++ code is usually organized into two types of files, header files and source
files, usually with a .h
or .cpp
file suffix respectively. The smallest
useful granularity of compiled code is an object file, which is the output when
compiling a single source file. Thus, a set of N
source files will emit N
associated compiled object files. These object files can then be combined into a
library or standalone-executable in a process known as linking. In addition to
linking object files, the linker can also link other libraries (which were
themselves created by linking object files together). Here’s a
flow chart showing some of the possibilities:
Source Data Compilation Linking
----------- ----------- -------
+-------------+ +----------+
|Source file 1| ----------> |Obj file 1| -
+-------------+ +----------+ \
+-------------+ +----------+ \ +---------------------+
|Source file 2| ----------> |Obj file 2| ----> |Library or executable|
+-------------+ +----------+ / +---------------------+
+----------+ /
|Static Lib| -
+----------+
Notice that I referred to the external library in the above flow chart as a static library. There are actually two types of libraries, static and dynamic (also known as shared). Static libraries are bundled into the executable at compile time as you’ve just seen. Dynamics libraries are loaded on demand by the executable at runtime.
As you may have guessed, our reloader is only going to work on dynamic libraries. Reloading a static library would require a relink of the entire executable and doing this on the fly would be quite a challenge indeed (left as a sadistic exercise to the reader).
The build process
If you haven’t guessed already, builds for C++ programs can get complicated fast. Decisions like link order, what to link, what the linker should produce have to be made by the programmer. The compilation of the object files is quite configurable as well. For example, we can specify how much the compiler should optimize or if we need debug symbols. Couple this with the fact that there are many compilers, each with their own feature sets and options (which may or may not be platform specific) and we have a real doozy on our hands. Fortunately, there are a lot more solutions for managing this today than there were years ago. The solution I’m going to cover here leverages premake5. Another piece of good news is that once you have some of the boilerplate in place, it’s easy to fork a new project off of it. There exist other build solutions which I’ll list non-exhaustively at the end of the section, but without further ado, let’s start scaffolding our project.
RePlex
|-- premake5.lua
|-- lib
| |-- RePlex.cpp
| +-- pub
| +-- RePlex.h
+-- runtime
+-- Main.cpp
There are countless ways one can organize files within a C++ project.
Personally, I like having public headers separated other files within the same
module and in an easily identifiable folder name (like “pub”). I also ensure
that modules themselves (standalone libraries and executables) are separated.
Occasionally, I introduce more folder nesting within a module to provide even
more structure where it makes sense (e.g. grouping platform specific code, test
harness code, etc). The contents of lib will be used to make our library, and
the contents of runtime will make our executable. The file
RePlex/lib/pub/RePlex.h
will be the public interface of the library. Note that
we’ll add files as we need them throughout this article; this is just a starting
point.
What you’re actually curious about though, is that lua
file. The Premake build
configuration system actually consumes Lua files in order to generate project
files or makefiles appropriate to your target platform. For example, on Windows,
you could invoke premake5 vs2015
in the RePlex
directory to generate Visual
Studio 2015 solution files to then edit and compile the code. Alternatively, on
OSX you could generate Xcode project files, or GNU makefiles on Linux. Many more
backends to premake
exist and you can
even
make your own.
The Lua files Premake consumes are declarative in nature, but you can use the
entire Lua runtime at your disposal to do more complex build tasks if you wish
(like preprocessing, or what have you). Here are the contents of premake5.lua
file.
workspace "RePlex"
configurations {"Debug", "Release"}
-- Use C++ as the target language for all builds
language "C++"
targetdir "bin/%{cfg.buildcfg}"
-- Get that C++14 goodness
flags { "C++14" }
filter "configurations:Debug"
-- Add the preprocessor definition DEBUG to debug builds
defines { "DEBUG" }
-- Ensure symbols are bundled with debug builds
flags { "Symbols" }
filter "configurations:Release"
-- Add the preprocessor definition RELEASE to debug builds
defines { "RELEASE" }
-- Turn on compiler optimizations for release builds
optimize "On"
-- RePlex Library
project "RePlex"
kind "SharedLib"
-- recursively glob .h and .cpp files in the lib directory
files { "lib/**.h", "lib/**.cpp" }
-- RePlex Runtime
project "RePlexRuntime"
kind "ConsoleApp"
-- recursively glob .h and .cpp files in the runtime directory
files { "runtime/**.h", "runtime/**.cpp" }
-- link the RePlexLib library at runtime
links { "RePlex" }
includedirs { "lib/pub" }
I don’t want to belabor the mechanics of this in too much detail, but hopefully
the structure makes sense. At the top level, we have a workspace called “RePlex”
which is purely semantic; it has no meaning as far as C++ is concerned but acts
as a container for all our build targets (i.e. libraries and executables). This
is useful for signalling what should be grouped to IDEs like Visual Studio,
Xcode. Premake files are structured with lexical scoping, where keywords like
workspace
and project
increase the depth of the scope tree. Thus all the
properties defined underneath line 1 apply to all projects in that workspace.
Here, we define two possible build configurations, the workspace language,
target directory, and configuration specific compiler flags and preprocessor
definitions.
Then, we define two projects for our shared library and console application
respectively. Note that RePlexLib is a dependency of RePlexRuntime;
RePlexRuntime has access to all headers in lib/pub
and also links RePlexLib.
Note that we could have overridden or specified any of global workspace
properties on a per project basis.
Let’s make some stub files too:
// lib/pub/RePlex.h
#pragma once
class Foo
{
public:
int GetTheAnswer() const;
private:
int m_answer = 42;
};
// lib/RePlex.cpp
#include "pub/RePlex.h"
int Foo::GetTheAnswer() const
{
return m_answer;
}
// runtime/Main.cpp
#include <RePlex.h>
#include <iostream>
int main()
{
Foo foo;
std::cout << "The answer is " << foo.GetTheAnswer() << std::endl;
return 0;
}
As a side note, my preference for editing code varies based on the platform I am using. When developing on Windows, I prefer Visual Studio (which has improved dramatically since 2010, the version I first learned on). When developing on OSX, I use Xcode due to its integration with the various simulators for iOS devices. On Linux, I use Emacs with Vim emulation, or Eclipse when I need a visual debugger (I’ve had issues using Eclipse as an editor due to stability, although this may have been addressed in more recent builds since my last experience with it in 2014). When writing this article, I opted to use a simple text editor and the make system, as this is the most ubiquitous build system and the reader should be able to reproduce all the work with the IDE of their choosing relatively easily.
At this point, you should install Premake if you haven’t already for the
operating system of your choice. With this, you should be able to invoke
premake5 gmake
(substitute gmake
with whatever action you like) in the root
directory of the app should create the corresponding Makefiles or project files
depending on what action you choose. Subsequently, building the workspace should
emit bin/Debug/RePlexRuntime
and bin/Debug/libRePlex.dylib
. Running
bin/Debug/RePlexRuntime
should give us the output we expect.
The answer is 42
Woohoo, progress!
The implementation
As mentioned before, we want to author a library that will handle “hot-reloading” a different library on the fly. Let’s first imagine what the interface to this library might look like. Obviously, the calling site needs to supply the name of the library they want to link. In addition, they need to specify the symbols in the library they wish to use. A symbol is, roughly speaking, the name given by the compiler to a variable name or function in your program. “Why can’t they just use the name I gave it?” you might ask. The reason is because of features like function overloading, namespacing, templating, and a number of other features that make the given visible name insufficient for unique identification purposes. The symbol is generally used by the linker at compile time to determine where in memory the data or function exists. For our purposes, we need to make this association at runtime, but fortunately, an API for doing this exists in all major operating systems. We’re going to focus on UNIX-based operating systems here. The functions we need are:
dlopen
: Given a file name, reads the library from disk into memorydlsym
: Given a symbol, returns the address of that symboldlerror
: Returns an error message describing the last thing that went wrongdlclose
: Releases a reference to the specified library. If the reference count drops to zero, the library is removed from the address space.
Let’s do a quick and dirty demo to test the mechanics of these functions. The structure of the program will look like the following:
+------+
|RePlex| Library for performing loading and unloading
+------+
| \ Is a dependency of
| \
| *---*
| \ +-------------+
| *--> |RePlexRuntime|
| +-------------+
| Loads /
| / Uses symbols in RePlexTest
\ /
\ +----------+
*--> |RePlexTest| Library that gets reloaded
+----------+
RePlexRuntime is the executable that will be running in our test. RePlexTest will be the library that we eventually want to hotload. Replex will be our library for encapsulating the various functions to interact with the dynamic library. To accommodate this structure, let’s add the test library to our Premake file.
project "RePlexTest"
kind "SharedLib"
files { "test/**.h", "test/**.cpp", "test/pub/*.h" }
Easy enough. We’ll also add a header and source file to our test library.
// test/pub/Test.h
#pragma once
// This line prevents C++ name mangling which would prevent dlsym from retrieving
// the correct symbols.
extern "C"
{
void foo();
// The extern keyword here exports a global variable that will be defined in Test.cpp
extern int bar;
}
// test/Test.cpp
#include "pub/Test.h"
#include <cstdio>
void foo()
{
printf("Hi\n");
}
int bar = 4;
Our RePlex library for now will just have a very thin wrapper to the various
dl*
functions.
// lib/pub/RePlex.h
#pragma once
#include <dlfcn.h>
void* Load(const char* filepath);
void* LoadSymbol(void* library, const char* symbol);
void Reload(void* &library, const char* filepath);
void PrintError();
// lib/RePlex.cpp
#include "pub/RePlex.h"
#include <cstdio>
void* Load(const char* filepath)
{
return dlopen(filepath, RTLD_NOW);
}
void* LoadSymbol(void* library, const char* symbol)
{
return dlsym(library, symbol);
}
void Reload(void* &library, const char* filepath)
{
dlclose(library);
library = dlopen(filepath, RTLD_NOW);
}
void PrintError()
{
printf("Error: %s\n", dlerror());
}
Finally, we’ll modify our runtime to use these new functions and test run a dll hot-load.
#include <RePlex.h>
#include <iostream>
#ifdef DEBUG
const char* g_libPath = "bin/Debug/libRePlexTest.dylib";
#else
const char* g_libPath = "bin/Release/libRePlexTest.dylib";
#endif
void (*foo)();
int main()
{
void* handle = Load(g_libPath);
if (handle)
{
// Set the memory address of the function foo from the library to our foo.
foo = reinterpret_cast<void (*)()>(LoadSymbol(handle, "foo"));
// Call foo
foo();
// Read the data from the global variable bar in the test library
int bar = *reinterpret_cast<int*>(LoadSymbol(handle, "bar"));
std::cout << "bar == " << bar << std::endl;
// Wait for input to give us a chance to recompile the library
std::cout << "Make some changes, recompile, and press enter." << std::flush;
while(std::cin.get() != '\n') {}
// Reload the library!
Reload(handle, g_libPath);
// We need to refetch the symbol because it's location in memory may have changed
foo = reinterpret_cast<void (*)()>(LoadSymbol(handle, "foo"));
foo();
// Do the same for bar
bar = *reinterpret_cast<int*>(LoadSymbol(handle, "bar"));
std::cout << "bar == " << bar << std::endl;
}
else
{
PrintError();
}
return 0;
}
Running this on my machine generates output like the following:
Hi
bar == 4
Make some changes, recompile, and press enter.
Can i haz hot-reloading
bar == 314159
Note that after the second line of that output, I changed the contents of
Test.cpp and reinvoked make
to recompile the library. So far so good! Now that
we have some grasp of the strange incantations of this program, we can start to
think about a better way to structure it. One thing worth noting is our use of
extern "C"
. This has a special meaning in C++ and informs the compiler to not
use name-mangling on the symbols defined in its scope (in our case, the foo
function and bar
global). This makes those symbols callable from C code, and
also allows functions like dlsym
to locate them using a simple C-string
lookup. Symbol lookup for C++ functions and variables is a bit more complex due
to the various decorators that can be attached to functions and variables. More
importantly, the way in which the compiler assigns names to these decorated
functions and variables is not standardized and can vary from compiler to
compiler.
The existing program has a number of problems. First, we need to manually load
the symbol ourselves. This will certainly become tedious if a module exports a
lot of functions and variables. In addition, the test library where the actual
code resides doesn’t actually specify it’s own exports which is a little odd.
What we really want is a way to package all exports in a pretty package. This
means that RePlex will need to expose two public interfaces, one for publishing
a hot-loadable library, and one for loading and reloading those hot-loadable
libraries. To do this, we’ll make a class called RePlexModule
in Replex.h. The
intended usage of this module is to be inherited by the test library and
specialized so it can load all the correct symbols and expose a cleaner
interface to the end user of the library. Let’s start with just the public
interface:
// lib/pub/RePlex.h
#pragma once
#include <array>
#include <unordered_map>
#include <string>
#include <dlfcn.h>
template <typename E, size_t NumSymbols>
class RePlexModule
{
public:
static void LoadLibrary() { GetInstance().Load(); }
static void ReloadLibrary() { GetInstance().Reload(); }
protected:
static E& GetInstance()
{
static E instance;
return instance;
}
//...
//... continued later
};
The first thing you’ll notice are the template parameters attached to our class. That’s right, this is a template class! If you haven’t seen them before (or saw them and didn’t understand them), the following example should give you the basic gist.
template <typename T>
class Foo
{
public:
T GetT() { return t; }
T t;
}
Foo
is a class template (you might have heard the term “template class”
before, but honestly, I don’t think that makes any sense; just know that they’re
interchangeable and “template class” is a bit more common) and has one template
parameter. We can’t make an object of type Foo
since we don’t know what T
is. However, later we might instantiate the class template like so:
Foo<double> foo;
This makes an instance of the class template Foo
with T = double
. The
compiler essentially writes out the code like so:
class Foo<double>
{
public:
double GetT() { return t; }
double t;
}
The compiler simply did a substitution of the unqualified type T
for the type
double
. If you conceptually think of templates in this way and do mental
substitutions, you’ll have a good mental model for what’s going on. In addition
to types, template arguments can be countable numbers (like NumSymbols
).
Going back to RePlexModule
, the only two public functions
RePlexModule will expose to our runtime is LoadLibrary
and ReloadLibrary
which both depend on GetInstance
. Notice that these are all static functions
that operate on a singleton. Singletons are often considered an antipattern,
however, in this case we actually want to enforce that only one copy of this
class exists in memory. It really doesn’t make sense to have multiple instances
(if we wanted, say, two separate versions of the library to coexist, we would
associate them with entirely different types, not two instances of the same
type). Why doesn’t GetInstance
return a reference to RePlexModule
? Because
remember, we want our test library to inherit from this class to specialize
behavior. Thus, we expect it to supply the value of the template parameter E
as itself. If this is confusing now, don’t worry. It will get clarified better
later on. We also need to remember to remove the RePlex
library as a Premake
project since it now is a header only file that doesn’t require standalone
compilation (this includes removing it as a link dependency of the runtime).
Now, let’s look at the functions exposed to the test library that will inherit
RePlexModule
:
// Start of RePlexModule declaration above
// ...
protected:
// Should return the path to the library on disk
virtual const char* GetPath() const = 0;
// Should return a reference to an array of C-strings of size NumSymbols
// Used when loading or reloading the library to lookup the address of
// all exported symbols
virtual std::array<const char*, NumSymbols>& GetSymbolNames() const = 0;
template <typename Ret, typename... Args>
Ret Execute(const char* name, Args... args)
{
// Lookup the function address
auto symbol = m_symbols.find(name);
if (symbol != m_symbols.end())
{
// Cast the address to the appropriate function type and call it,
// forwarding all arguments
return reinterpret_cast<Ret(*)(Args...)>(symbol->second)(args...);
}
else
{
throw std::runtime_error(std::string("Function not found: ") + name);
}
}
template <typename T>
T* GetVar(const char* name)
{
auto symbol = m_symbols.find(name);
if (symbol != m_symbols.end())
{
return static_cast<T*>(symbol->second);
}
else
{
// We didn't find the variable. Return an empty pointer
return nullptr;
}
}
private:
void Load()
{
m_libHandle = dlopen(GetPath(), RTLD_NOW);
LoadSymbols();
}
void Reload()
{
dlclose(m_libHandle);
m_symbols.clear();
Load();
}
void LoadSymbols()
{
for (const char* symbol : GetSymbolNames())
{
m_symbols[symbol] = dlsym(m_libHandle, symbol);
}
}
void* m_libHandle;
std::unordered_map<std::string, void*> m_symbols;
};
The data members of the class at the very bottom are a pointer to the library
handle after it gets loaded and an associative container mapping symbol names to
their pointers in memory. Working upwards, we have functions that operate very
similarly to our initial toy implementation. LoadSymbols
iterates over all
elements returned from GetSymbols
and populates m_symbols
. Load
works as
before but also calls LoadSymbols
. Reload
also works as before but clears
the contents of m_symbols
first to ensure there aren’t any invalid symbols
lingering around. Load
and Reload
are called by the static functions
LoadLibrary
and ReloadLibrary
defined above respectively.
Towards the top, we have pure virtual functions we expect implementers of this
class to override: GetPath
and GetSymbols
. We’ll override these soon, but
first, let’s look at the (possibly terrifying) functions Execute
and GetVar
.
template <typename Ret, typename... Args>
Ret Execute(const char* name, Args... args)
{
auto symbol = m_symbols.find(name);
if (symbol != m_symbols.end())
{
return reinterpret_cast<Ret(*)(Args...)>(symbol->second)(args...);
}
else
{
throw std::runtime_error(std::string("Function not found: ") + name);
}
}
Execute
takes a function name and Args... args
as arguments. Its return type
is Ret
. The first argument is unlikely to be contentious but the second is
likely unfamiliar to those who haven’t touched C++ since the advent of the C++11
standard. The ...
syntax denotes a parameter pack and is useful for specifying
a variadic number of arguments with varying types. For example, if I called this
function like:
Execute<char, int, float>("stuff", 7, 2.4f);
// Compiler turns this into something like:
// char Execute(const char* name, int arg1, float arg2)
// {
// auto symbol = m_symbols.find(name);
// if (symbol != m_symbols.end())
// {
// return reinterpret_cast<char(*)(int, float)>(symbol->second)(arg1, arg2);
// }
// else
// {
// throw std::runtime_error(std::string("Function not found: ") + name);
// }
// }
The compiler would interpret Ret
as a char
, args...
would be expanded
to 7 and 2.4f, and Args...
would be expanded to an int and float type. This
allows us to invoke Execute
to first lookup the symbol, call it as a function
with the appropriate arguments, and subsequently return the correct return
value. Neat! We throw an exception if the function isn’t found because it’s hard
to know what to return in this case.
The function GetVar
is a little simpler. We simply look up the symbol and cast
it as a pointer to specified template type before returning it.
Now, we’re ready to specialize this class for our test library.
// Test.h
#pragma once
#include <RePlex.h>
extern "C"
{
void foo();
extern int bar;
}
std::array<const char*, 2> g_exports = { "foo", "bar" };
class TestModule : public RePlexModule<TestModule, g_exports.size()>
{
public:
static void Foo()
{
// Execute might throw, but we don't bother catching the exception here for brevity
GetInstance().Execute<void>("foo");
}
static int GetBar()
{
// decltype is a relatively new operator. decltype(bar) resolves to int
// Note that this function does not protect against retrieving nullptr
return *GetInstance().GetVar<decltype(bar)>("bar");
}
protected:
virtual const char* GetPath() const override
{
#ifdef DEBUG
return "bin/Debug/libRePlexTest.dylib";
#else
return "bin/Release/libRePlexTest.dylib";
#endif
}
virtual std::array<const char*, g_exports.size()>& GetSymbols() const override
{
return g_exports;
}
};
In addition to the things we actually want to export from before (foo
and
bar
), we make an array of exports of size 2 containing the correct string
names. When we inherit from RePlexModule
, we are careful to fully qualify all
of its template arguments so the compiler can properly substitute all the
template arguments where they are necessary. Thus, GetInstance
will return a
reference to a TestModule
singleton, and GetSymbolNames
will return an array of
2 strings. We override the methods that were declared pure virtual, GetPath
,
and GetSymbolNames
in a straightforward manner. Finally, we provide convenient
static functions Foo
and GetBar
for calling foo
and retrieving bar
.
Notice that the body of Foo
contains
GetInstance().Execute<void>("foo");
because foo
returns void and takes no arguments. We’re finally ready to modify
our main program to take advantage of the new TestModule
class.
// runtime/Main.cpp
#include <RePlex.h>
#include <Test.h>
#include <iostream>
int main()
{
TestModule::LoadLibrary();
TestModule::Foo();
std::cout << "bar == " << TestModule::GetBar() << std::endl;
std::cout << "Make some changes, recompile, and press enter." << std::flush;
while(std::cin.get() != '\n') {}
TestModule::ReloadLibrary();
TestModule::Foo();
std::cout << "bar == " << TestModule::GetBar() << std::endl;
return 0;
}
Much better. Now the user of the test module doesn’t need to think about the
specifics regarding symbol names, reloading each symbol, or their types. We can
just look at the public interface of TestModule
to get it all down pat. If
you’re following along in code, remember to add the correct include paths to the
test library and runtime projects in premake5.lua
so the preprocessor includes
work before compiling.
Some refinements
What we have now is probably usable as an internal library for the purpose of iterating on a C++ module that exposes a public interface of variables and functions. By coupling this with enums, interfaces, structs, and classes in a public header, we can pretty quickly imagine how we might integrate this small library into our workflow. There are a number of refinements possibly worth making to the library which I’ll mention briefly in this section.
First, the library as is will only work on Linux and OSX platforms. The way
symbols get mapped and unmapped in memory is operating system dependent, and
Windows exposes its own set of functions for doing so: LoadLibrary
,
GetProcAddress
, and FreeLibrary
. They are analogous to dlopen
, dlsym
,
and dlclose
respectively, and I will leave it as an exercise to the reader to
implement this. There are at least two ways to accomplish this. One way is to
add a Premake filter on
the platform name and create preprocessor definitions that can be used to ensure
the correct function is called. Alternatively, you can split the interface to
the operating system in files based on operating system and exclude files that
were meant for different platforms in Premake.
A second problem is one of performance. If we are calling Execute
or GetVar
many times in an inner loop, we have to repeatedly hash the symbol name to do a
lookup for the symbol address. To avoid this, we could cache the result of the
lookup. Even easier though, is to avoid using the map in the first place and
store the symbols in the same order as the symbol names. This might make it
harder to change what symbols get loaded between loads, but it’s unlikely that
this would be a useful feature anyways.
// Before we stored a map
// std::unordered_map<std::string, void*> m_symbols;
//
// Now we'll use a reference to the array that was passed in
using SymbolArray = std::array<std::pair<const char*, void*>>;
SymbolArray& m_symbols;
Also, we’ll change our LoadSymbols
function to populate this array:
void LoadSymbols()
{
for (auto&& symbol : m_symbols)
{
symbol.second = dlsym(m_libHandle, symbol.first);
}
}
Note the auto&&
here which shorthand for saying the variable symbol
can bind
to any type regardless of const-ness or
value category. In
this case, there is only one possibility of auto&&
and the compiler will treat
it as a std::pair<const char*, void*>&
.
Next, we modify our Execute
and GetVar
functions to take an index instead of
a string name, in addition to adding a constructor which accepts a reference to
the symbol array.
RePlexModule(SymbolArray& symbols) : m_symbols(symbols) {}
template <typename Ret, typename... Args>
Ret Execute(const int index, Args... args)
{
auto symbol = m_symbols.at(index);
return reinterpret_cast<Ret(*)(Args...)>(symbol.second)(args...);
}
template <typename T>
T* GetVar(const int index)
{
auto symbol = m_symbols.at(index);
return static_cast<T*>(symbol.second);
}
This is now quite a bit simpler than before because we leave it to the method
std::array::at
to do bounds checking for us. However, is it likely that this
index will be dynamically determined at runtime? Not really. Instead, if we made
the index a function template parameter, we can enforce that the correct address
is retrieved without a bounds check. Doing this for the GetVar
function for
example:
template <typename T, size_t index>
T* GetVar()
{
static_assert(Index >= 0 && Index < NumSymbols, "Out of bounds symbol index");
auto symbol = m_symbols[index];
return static_cast<T*>(symbol.second);
}
Doing m_symbols[index]
doesn’t do a bounds check like m_symbols.at(index)
does, but we still get the bounds checking via the static_assert
which means
it’s enforced at compile time instead of runtime. Great! There need to be a few
changes to the TestModule
to accommodate this new interface:
std::array<std::pair<const char*, void*>, 2> g_exports = {
std::make_pair("foo", nullptr),
std::make_pair("bar", nullptr)
};
class TestModule : public RePlexModule<TestModule, g_exports.size()>
{
public:
static void Foo(int input)
{
GetInstance().Execute<0, void>();
}
static int GetBar()
{
return *GetInstance().GetVar<1, decltype(bar)>();
}
TestModule() : RePlexModule(g_exports) {}
// Rest of class identical except GetSymbolNames was removed
};
Now, we can expect that Foo
and GetBar
will be very fast since they no
longer require the string based map lookup. At most, they will need an offset
from the array address, but it’s likely that the optimizer will even elide that
instruction.
This library is far from complete. It’s not thread-safe. It doesn’t protect you
from reloading if you’re in the middle of executing a function that might do a
symbol lookup shortly after the previous library is unloaded. It requires the
programmer to repeat himself or herself with regard to function return types and
arguments when binding Execute
. It doesn’t handle errors well (library not
found, symbol missing, etc). It’s also missing a number of nice features,
like automatic reload if the file changes. The goal of this article wasn’t to
provide a perfect implementation, but hopefully convey a since of how software
like this might be written and structured.
One thing that’s important to understand, is that the template programming we are doing here should not define one’s programming style. In this case, we are using generics simply because we are defining a generic interface, which is where the template really shines. In particular, templates coupled with static assertions can go a long way in enforcing the type safety and correctness of an application. It’s worth noting that all the templating is not exposed to the main runtime executable, who has the luxury of an easy-to-use interface. Indeed, abstracting away common generic behavior can be a good tool to reduce complexity and code duplication if done correctly.
Testing and publishing
Making manual changes to the library and recompiling it, followed by a keystroke to see if things worked visually is a pretty terrible workflow as far as detecting regressions goes. In this section, we’ll make things a little neater and repeatable. We’ll also discuss the mechanics of publishing our code so others can use it.
For the purposes of this article, we will use
googletest which is a batteries-included
test suite which contains the necessities (assertions, test framework) and other
amenities like test report generation and an optional
frontend. To make our repository
self-contained, let’s add googletest
as a git submodule, and also add a
Premake project for it.
git submodule add [email protected]:google/googletest.git
// premake5.lua
# ...
project "GoogleTest"
kind "StaticLib"
files { "googletest/googletest/src/gtest-all.cc" }
includedirs { "googletest/googletest/include", "googletest/googletest" }
project "RePlexRuntime"
kind "ConsoleApp"
files { "runtime/**.h", "runtime/**.cpp" }
includedirs { "lib/pub", "test/pub", "googletest/googletest/include" }
links { "GoogleTest" }
Now, the Google test framework is bundled in the repository and invoking
premake5 gmake
will compile it and link it to RePlexRuntime
. Now to actually
author the tests themselves. Let’s first do a simple test to see how this all
works.
// runtime/Main.cpp
#include <gtest/gtest.h>
TEST(SillyTest, IsFourPositive)
{
EXPECT_GT(4, 0);
}
TEST(SillyTest, IsFourTimesFourSixteen)
{
int x = 4;
EXPECT_EQ(x * x, 16);
}
int main(int argc, char** argv)
{
// This allows us to call this executable with various command line arguments
// which get parsed in InitGoogleTest
::testing::InitGoogleTest(&argc, argv);
return RUN_ALL_TESTS();
}
Compiling and invoking RePlexRuntime
generates the following output:
./bin/Debug/RePlexRuntime
[==========] Running 2 tests from 1 test case.
[----------] Global test environment set-up.
[----------] 2 tests from SillyTest
[ RUN ] SillyTest.IsFourPositive
[ OK ] SillyTest.IsFourPositive (0 ms)
[ RUN ] SillyTest.IsFourTimesFourSixteen
[ OK ] SillyTest.IsFourTimesFourSixteen (0 ms)
[----------] 2 tests from SillyTest (0 ms total)
[----------] Global test environment tear-down
[==========] 2 tests from 1 test case ran. (0 ms total)
[ PASSED ] 2 tests.
Great! To learn the feature set of the Google test framework more completely, I recommend reading it’s documentation, starting with the primer. As an exercise, I recommend recompiling the above with a failing test to see what happens before continuing. What we need is to provide a way for the tests to emit code as text, recompile, and continue at runtime. To do this, we’ll author a fixture class.
// runtime/Main.cpp
#include <RePlex.h>
#include <Test.h>
#include <cstdlib>
#include <fstream>
#include <gtest/gtest.h>
const char* g_Test_v1 =
"#include \"pub/Test.h\"\n"
"int bar = 3;\n"
"int foo(int x)\n"
"{\n"
" return x + 5;\n"
"}";
const char* g_Test_v2 =
"#include \"pub/Test.h\"\n"
"int bar = -2;\n"
"int foo(int x)\n"
"{\n"
" return x - 5;\n"
"}";
class RePlexTest : public ::testing::Test
{
public:
// Called automatically at the start of each test case.
virtual void SetUp()
{
WriteFile("test/Test.cpp", g_Test_v1);
Compile(1);
TestModule::LoadLibrary();
}
// We'll invoke this function manually in the middle of each test case
void ChangeAndReload()
{
WriteFile("test/Test.cpp", g_Test_v2);
Compile(2);
TestModule::ReloadLibrary();
}
// Called automatically at the end of each test case.
virtual void TearDown()
{
TestModule::UnloadLibrary();
WriteFile("test/Test.cpp", g_Test_v1);
Compile(1);
}
private:
void WriteFile(const char* path, const char* text)
{
// Open an output filetream, deleting existing contents
std::ofstream out(path, std::ios_base::trunc | std::ios_base::out);
out << text;
}
void Compile(int version)
{
if (version == m_version)
{
return;
}
m_version = version;
EXPECT_EQ(std::system("make"), 0);
// Super unfortunate sleep due to the result of make not being fully flushed
// by the time the command returns (there are more elegant ways to solve this)
sleep(1);
}
int m_version = 1;
};
The fixture class should be fairly self explanatory. There are three primary
methods for setup, teardown, and reloading the library. We keep track of the
currently loaded library in a member variable m_version
so we avoid
recompiling the library if the one we want is already loaded (note that
m_version
defaults to 1 at the beginning). We also have two versions of
Test.cpp
that we will write out and compile at runtime. You’ll have to change
the function signatures of Foo
in Test.h
so things compile properly. To use
the fixture, we use the TEST_F
macro instead of the TEST
macro like so:
TEST_F(RePlexTest, VariableReload)
{
EXPECT_EQ(TestModule::GetBar(), 3);
ChangeAndReload();
EXPECT_EQ(TestModule::GetBar(), -2);
}
TEST_F(RePlexTest, FunctionReload)
{
EXPECT_EQ(TestModule::Foo(4), 9);
ChangeAndReload();
EXPECT_EQ(TestModule::Foo(4), -1);
}
int main(int argc, char** argv)
{
::testing::InitGoogleTest(&argc, argv);
return RUN_ALL_TESTS();
}
Running this will generate a fair bit of output due to the runtime compilation
but we should have all our tests passing. A bad thing we did in order to make
this work was the sleep in our Compile
function. Even though the system
call
is synchronous, there is a race condition when reading the file from the disk
which is being flushed by the make
command. The sleep here is unfortunate
because it makes the tests slower, and also makes the test non-deterministic.
The proper way to implement these tests is to install a handler for a file
change notification. The implementation of this will vary based on the platform,
and is left as an exercise to the reader.
At this point, we might decide that the library is good enough for others to
use. The only file a 3rd-party user would need to leverage our library is
RePlex.h
(in other words, this is a “header-only” library). Thus, distribution
is just a matter of copying RePlex.h
into the include path of the target
project. If we had needed to export compiled code in the form of a static or
shared library, we have two options. First, we may opt to compile the library
for all the various combinations of OS and architecture (x86/x64/etc) we might
support. This library would then be distributable as a binary file.
Alternatively, we can simply publish the code with the Premake script we
authored and let the end user compile the code themselves and link the result to
their own library or executable. These are the two primary options at the
moment, and sadly, no unified “package manager” has been authored in the C++
community (although the author of this article is very interested in efforts to
do so).
Notes on writing code
Without getting into editor/IDE battles, I am posting below a survey of the various tools I use. If your favorite tool isn’t listed, it is either because I haven’t tried it, or have reason to use an alternative. This isn’t meant to be exhaustive or definitive. Rather it should be used as a starting point for someone just starting to experiment with the language.
- Microsoft Visual Studio 2015: The IDE has come quite a long way, and is by and large among the most fully featured IDEs in existence. The debugger, watch windows, conditional breakpoints, immediate window, peek windows, graphics debugger, and performance analyzers are tools I leverage frequently.
- Xcode: For developing on OSX. It has a similar feature-set as VS and compiles using the Clang compiler instead (possibly better error messages, stricter warnings).
- Eclipse: Open source IDE for use on Linux that I’ve used (disclaimer: on Linux I tend to fall back to text editors and command line debuggers)
- cppcheck: A static analysis tool which can help you catch bugs
- Spacemacs: An Emacs distribution with batteries included that uses Vim modal editing as its default mode (this is going to make me popular /s).
- unittest-cpp: A nice lightweight framework for authoring unit tests if you don’t need something as heavy duty as gtest.
- googletest: The Google Test framework used in this article.
- Premake: Build configuration tool leveraged throughout this article (alternatives to consider include bazel, CMake, FastBuild, or Scons). Meta-make systems like Premake and CMake have supplanted simple Makefiles for more complex projects, and I personally favor Premake due to its simplicity and modularized architecture (it also helps that it isn’t built on top of a strange DSL).
- lldb/gdb: Command line debuggers for quick-and-dirty debugging if the code was compiled with Clang or GCC respectively
- Clang tools: A suite of tools that should be part of any robust build pipeline including formatters, linters, static analysis, and more.
- cppreference: Invaluable online reference for the C and C++ language and standard library
There is also an entire gamut of profilers, heap analyzers, leak detectors, and more which vary based on operating system and task, which I will leave for perhaps another time. My recommendation regarding tools is, pick an IDE and learn it very well (seriously!). Your skills will likely translate to other IDEs. It is a little difficult to recommend using only an editor (unless you have access to a plethora of editor scripts and features) when programming in C++, primarily because on-the-fly static analysis can save a lot of time by detecting compile errors early (e.g. missing symbols, type mismatches, const-correctness violations, etc). In addition, debugging runtime problems like thread deadlocking, memory stomping, and the like are a bit tricky without a robust debugger. Other languages might do away with these types of problems, but they won’t offer as much control either, so it’s a two-way street.
The wrap up
At this point, we have a project that, while not necessarily in its final form, sufficiently accomplishes what we set out to do. We also made three inter-dependent modules, with one providing a shared interface to the other two, and configured it so that the code could be compiled on any platform (with some tweaking). We optimized and streamlined the code a little bit using some new language facilities to make the code faster and less error-prone. Finally, we also used some functionality provided directly by the operating system, and potentially learned something about code gets compiled, loaded, and executed. If you would like to compile the code and run it yourself or make modifications, please visit the project github page. This was intended to be an educational project, but improvements are welcome!
If you are a newcomer to the language, you will find that C++, being a fully multiparadigm language, can be unfriendly at first but is ultimately unopinionated with regards to how you wish to interact with the hardware. With the advent of C++11 and C++14 (both of which are supported by all major compilers), there are now better and easier-to-use abstractions than ever before. Old-timers of the language, stand to improve the style and understandability of their code without sacrificing power. New users will find the language much more accessible compared to previous attempts to learn the language. I recommend the C++ Primer (Lippman, Lajoie, Moo), Effective Modern C++ (Meyers), and Programming: Principles and Practice Using C++ (Stroustroup), all in their latest incarnations for studying the language.