Thumbnail How to cope with static callbacks in GLUT

Door: Thijs Zumbrink
23-11-2016 15:47

While refactoring TaZCrash I took another look at GLUT window creation. Originally set up in one-window only mode, I had some trouble in converting this to clean OOP code. In particular, the library only accepts static callbacks with no way to identify the object you are using it for. After some trial and error I found a clean solution.

The problem

When I originally encountered this implementation issue, I had a GameView class that opens a FreeGLUT window to render itself in. This was a regular class with regular member methods. The issue arose when I wanted to implement callbacks, for example to take action when a window is closed. The function signature to register a close callback looks like this:

void glutCloseFunc(void(*callback)(void));


In other words, provide it with a pointer to a plain C-style function or static C++ class function. That function takes no arguments and returns nothing.

At first I was puzzled: "how do I access my object from this callback function? I want to shut down my GameView instance when its window is closed." In my experience, libraries usually add a void* argument to callbacks registration and callback parameters, which you can use to pass this or anything else you like. In the callback you could then cast it back to GameView* before accessing its non-static member method. Since this was not supported, I decided to make GameView a singleton so I could access it statically. Callback code would look like this:

glutCreateWindow("Some title");
glutCloseFunc(GameView::closeWrapper); // Register close callback

...

static void GameView::closeWrapper () {
GameView::instance().windowClose(); // Access via Singleton
}

void GameView::windowClose () {
// Actual shutdown code
}


The downside to this solution is obviously that there cannot be multiple instances. This hinders testing and gives a decidedly static feel to the class. It also litters the class with a wrapper per callback type. Furthermore while refactoring code to a Window class, it would seem silly to again make this a singleton and limit the use to just one window.

My preferred way to actually use this library is to pass any std::function in to give the user maximum freedom. Something like:

glutCreateWindow("Some title");
glutCloseFunc(std::bind(GameView::windowClose, this));

// or:

glutCloseFunc([this] {
windowClose();
});

// or even:

glutCloseFunc([this] {
// Actual shutdown code
});


However this would not work, since the generated callbacks do not compile into a function with zero arguments. After all, at least one argument is needed to pass this so the member method can work on the instance.

The solution

During refactoring into a Window class, I realized that glut does support multiple windows. So intuitively there is some sort of instancing going on, which could help lift the need for static in my GameView. As it turns out, creating a Window returns an ID (int) and any callback can query the active Window via glutGetWindow() to determine where the event happened.

So now I have a Window class that is used by GameView. It contains a small static map that registers which Window objects belong to which GLUT ID's. The Window class stores the closure in a member variable and executes it after finding the correct Window instance in the map:

std::map<int, Window*> Window::windows;

void Window::onClose (std::function<void()> callback) {
closeHandler = callback;
}

void Window::show () {
int glutId = glutCreateWindow("Some title");
windows[glutId] = this;

glutCloseFunc([] {
// Take the correct Window instance from the map:
windows[glutGetWindow()]->closeHandler();
windows.erase(glutGetWindow());
});
}


And finally the cleaned up code in GameView:

Window window("Some title");
window.onClose([this] () {
// Actual shutdown code
});
window->show();


Conclusion

So what's the takeaway from this? At first I had blamed GLUT and FreeGLUT for not providing at least a void* to get around the static problem. I had read the docs and knew that glutGetWindow() existed, but creating a singleton out of GameView seemed the logical solution. It was only after a refactoring into a Window class, with clearly defined concerns and being agnostic to the game code, that the real solution became clear.

Refactoring to separate concerns, splitting code into smaller classes (occasionally value objects) in my case greatly reduced code size and complexity, and removed technical limitations. Things start to fall into place with proper class modeling.

Reacties
Log in of registreer om reacties te plaatsen.
Thijs Zumbrink, 23-11-2016 15:56:
Coincidentally the FreeGLUT todo list shows the wish for a void* parameter in callbacks.