Implementing a Console Callback System in C++


Dropdown console in SpiderGame

Back when I was writing a terrain generator in C++, I used this library called glConsole to put a quake-style dropdown console into the project. By a “dropdown console”, I mean the thing in the screenshot above - a place to execute scripts from inside the game. Basically, if I typed this in the console:

wireframe 1

I wanted to execute this in C++:

void Renderer::setWireframeRendering(bool value) {
    this->wireframe = value;
}

However, this wasn’t possible in glConsole. While it did most things pretty well, the one thing it could not do was bind member functions - functions which are part of a class. You could bind free functions and static functions, but not methods. Lately I’ve been working on a very simple first-person shooter using C++ and no game engine. I wanted something like glConsole, but without the glaring downside. Plus, I was writing the game from scratch for a reason - why not write my own console?

In this post, I’ll explain my approach to implementing a callback system which stores functions and executes them at a later date. My goals for this system were:

  1. Member functions should be supported.
  2. You should be able pass in any method/callable object to be called at a later date.
  3. It should be (at least somewhat) typesafe.

Overview

Our goal is to implement a class, CallbackMap, which has two responsibilities:

  1. Given a function name and a callable object, it will store a mapping between the two.
  2. Given a function name and some arguments, it will call the object associated with the function name, passing the given arguments.

The interface could look a bit like this:

Class CallbackMap {
public:
    void storeCallback(const std::string& functionName, Callback function);
    void call(const std::string& functionName, const std::string& args);
private:
    std::unordered_map<std::string, Callback> callbackMap;
}

Remember we’re mainly interested in implementing a console. That means all of our “function calls” from the console will just be strings, which is why args above is a string. We’ll assume args is a space-delimited set of arguments; the input string (e.g. “foo 1 2 3”) can simply be split at the first space and then passed into call.

std::function

Our first job is to define Callback, the actual “function” which we want to store and execute later. The first place your mind might turn when thinking about storing functions is a function pointer. However, this is exactly the mistake glConsole made - using function pointers will not allow member functions to be specified as callbacks. Intuitively, this is because a member function is called upon a specific object, and normal C function pointers aren’t built to hold state like that.

We could write our own wrapper object to hold callables (e.g. by storing the function pointer along with a void*), but luckily for us there’s already one in the standard library: std::function. std::function allows us to store anything that looks like a callable object: lambdas, functors, or free functions. For example:

int add(int a, int b) {
    return a + b;
}

// later
std::function addFunc(&add);
std::cout << addFunc(1, 2); // prints 3

Member functions require a small trick to be stored. There’s two pieces to this trick. First, we’ll need to explain std::bind, a function also introduced in C++11.

It takes, as arguments, a function and some arguments to that function. Yes, you read that right; some if its arguments are arguments to another function. It wraps the underlying function and the arguments for later use in a std::function. When we call the returned std::function, the arguments we passed in std::bind are automatically forwarded to the underlying function. If we didn’t pass arguments, we need to specify them in the call. An example probably helps here:

void main () {
    std::function addTwo = std::bind(&add, 2, std::placeholders::_1);
    std::cout << addTwo(1); // prints 3
    std::cout << addTwo(9000); // prints 9002
}

We pass the function add to std::bind, along with two parameters. These two parameters are the parameters the new std::function will forward to add. The first parameter, a, we specify simply as 2, and the second parameter, b, we leave unspecified by passing a std::placeholders object. Then, later, when we call the returned std::function, we only need to pass one argument, b - the 2 we passed in std::bind is automatically passed to add.

It’s kinda unclear how this helps us at all with member functions. What you might not realize is that, internally, C++ treats the object on which a member function is being called as an argument. That means if you take a call like this:

renderer.setWireframeRendering(true)

Internally, it really looks more like this:

setWireframeRendering(&renderer, true)

That means std::bind can be used to bind specific object references to member functions like this:

Renderer renderer;
std::function setWireframeMode(&Renderer::setWireframeRendering, &renderer, std::placeholders::_1);
setWireframeMode(true);

Now, whenever we call setWireframeMode, we are internally calling renderer.setWireframeRendering. When I first saw this, it blew my mind. It’s probably second nature to anyone familiar with functional languages, but for someone rigidly set in a C++ mindset, it was a breath of fresh air.

However, you might be thinking about how slow std::function might be. Remember we are just implementing a console; the only time we’re going to execute one of these std::functions is when the user brings up the console and executes a command, so the overhead of std::function is not noticeable.

Hopefully I’ve convinced you that std::function is awesome, but we still have a bit of a problem…

Parsing the command line

Let’s take another look at our interface definition:

Class CallbackMap {
public:
    void storeCallback(const std::string& functionName, Callback function);
    void call(const std::string& functionName, const std::string& args);
private:
    std::unordered_map<std::string, Callback> callbackMap;
}

Callback is just one type. We can’t store std::function objects taking arbitrary arguments in callbackMap. For example, what if we wanted to store both add, which requires a std::function<void(int, int)> and setWireframeRendering, which requires std::function<void(bool)>? Those are two distinct types.

Well, we’d really like to just abstract callback arguments down to space-delimited lists. That is, add and setWireframeRendering could both just take a std::string, and parse out the arguments they expect. For example, our setWireframeRendering method would look like this:

Renderer::setWireframeRendering(const std::string& args) {
    this->wireframe = (args == "true");
}

Now, all callbacks are just one type! But… gosh, that’s gonna be painful. We basically need to write a wrapper function for every callback we now want to implement.

To be honest, I never did come up with something ideal, but here’s my solution. Instead of storing the actual callback inside callbackMap, I store a lambda which does the argument parsing for you. If you are not familiar with lambdas, they are essentially functions defined at runtime.

Callback is defined as std::function<void(std::string)>, but it’s not the “real” type of our callbacks. There’s one intermediate step to translate our actual callback functions into something we can store in callbackMap. For that intermediate step, I wrote several helper functions which all look like this:

template <class T1>
Callback defineCallback(std::function<void(T1)> func)
{
    auto f = [func](const std::string& args) {
        T1 v1;
        std::stringstream sstream;
        sstream << args;
        sstream >> v1;
        // There should be some additional error handling here
        func(v1);
    };
    return f;
}

This method defines a lambda which stores the real callback, func, and takes a single argument, args. When the lambda is executed, it parses the argument using a std::stringstream and passes the now-typed argument to func. Here’s defineCallback in action:

CallbackMap callbackMap;
callbackMap.storeCallback(defineCallback<int, int>(&add));
callbackMap.storeCallback(defineCallback<bool>(std::bind(&Renderer::setWireframeRendering, &renderer, std::placeholders::_1)));

There’s a couple of downsides with this method.

  1. We’re relying on std::stringstream to do our parsing, and it doesn’t always do what we want. For example, it won’t parse wireframe true correctly, because “true” is not parsed as a boolean. wireframe 1 does work, though so does wireframe 1.5, or wireframe -1.
  2. If a function takes more than one argument (or no arguments), defineCallback must be overridden appropriately to take more template arguments. For example, the two-argument version is very similar to the first:
template <class T1, class T2>
Callback CallbackMap::defineCallback(std::function<void(T1,T2)> func)
{
    auto f = [func](const std::string& args) {
        T1 v1;
        T2 v2;
        std::stringstream sstream;
        sstream << args;
        sstream >> v1 >> v2;
        func(v1,v2);
    };
    return f;
}

In practice, I only defined the 0 to 3 argument versions of defineCallback, and that was enough.

Final Touches

Now that we have a method to pass the callbacks in, we can finally get around to storing and calling them. Most of the complexity has been hidden away in defineCallback and std::bind, and now our CallbackMap methods are very simple.

void CallbackMap::storeCallback(const std::string& callbackName, std::function<void(std::string)> callback)
{
    callbackMap[callbackName] = callback;
}

bool CallbackMap::call(const std::string& callbackName, const std::string& args)
{
    auto iter = map.find(callbackName);
    if (iter == map.end()) {
        // Callback not found
        return false;
    }

    iter->second(args);
    return true;
}

So, our final approach is summarized like this:

  1. The real callback, f, is passed to defineCallback, which generates a lambda.
  2. The lambda is stored in callbackMap with an associated string functionName.
  3. Later, callbackMap.call(functionName, args) is executed. This pulls the lambda out and executes it.
  4. The lambda parses the space-delimited arguments in args using a std::stringstream.
  5. The callback, f, is called with the parsed arguments.

Confused, or TL;DR?

I have the source code for the CallbackMap in a gist, which you can find here. It includes additional error handling which I didn’t go over in this post. You can also take a look at the classes and some usage in the actual repo for my project SpiderGame.