Categories
Game Development Geek / Technical Linux Game Development

A Better Way to Write Platform-specific C++ Code

Gaming on Linux reported that Linux porter Ethan Lee’s SteamOS & Linux talk at MAGFest has slides and audio available.

Ethan Lee has ported a number of games to GNU/Linux, and his talk gives some insight into what people can do to make the porting process easier.

Some of it is obvious, such as not accidentally introducing proprietary dependencies. Just because you like using Visual Studio, it doesn’t mean you need to force anyone who wants to build your project to need Visual Studio to do so.

He dug into coding patterns that make it easier or harder for someone to port, and while the slides are annotated, having the audio makes it easier to understand the context.

What I found funny was that the day before I saw these slides, I wrote code that looks almost exactly like what he had on slide 20 under “Bad Idea”.

I was basing my code off of Aquaria‘s, which has a long function filled with #ifdef #else #endif lines to get the path to the user’s save directory, providing a different path depending on if you are using Windows, Mac OS X, or GNU/Linux. While I wrote my version a bit more simplistically, it seemed like a decent approach.

Here’s what my code looked like:

std::string Persistence::getUserDataDirectory()
{
    #if defined (__WIN32__)
    const char *environment = std::getenv("APPDATA");
    std::string homeDirectory = (environment ? environment : ".");

    #elif defined (GB_ANDROID_BUILD)

    std::string homeDirectory = RealInstanceDelegator().SDL_AndroidGetInternalStoragePath();

    #else
    // IF ON LINUX

    std::string homeDirectory(".");
    // See https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html
    const char *environment = std::getenv("XDG_DATA_HOME");
    if (NULL == environment || std::strcmp(environment, ""))
    {
        const char *home = std::getenv("HOME");
        homeDirectory = (home ? home + std::string("/.local/share") : ".");
    }
    else
    {
        homeDirectory = environment;
    }
    #endif

    return homeDirectory + "/" + Version::PROJECT_NAME + Version::DEMO_STATUS;
} 

Ick. I would have to have to dive back into it and debug it if there is a problem later. Hopefully I got it right the first time.

But Lee’s presentation made me wonder how the “Good Idea” slide works and what was so different about it.

Here’s the code example in his slide:

char path[PLATFORM_MAX_PATH];
const char* GetSavePath()
{
   PLATFORM_GetSaveDir(path, "save.sav");
   return path;
}

It definitely looks cleaner without the preprocessing code, but even with the audio of the talk I didn’t understand what he was actually doing here. To me, it looked like his GetSavePath() was just delegating to a platform-specific version of the call, but how does this code know which one to use?

So I emailed him and asked.

His response:

The idea is that the way you write portable code separates the different paths from each other in a clear way while also being able to debug each path in a way where reading the path is trivial to do. The big problem with defs is that they often make things _crazy_ hard to read and are just error-prone in general, so I try to separate them in a different way.

Basically PLATFORM_* is just a blanket C namespace I make for separating everything; you just have a platform.h that all of the otherwise #ifdefy stuff will go to, then you write different platform.c files. In the case of the slide example I would write a platform_win32.c and a platform_linux.c, and of course you can mix and match if you really need to (linux.c and osx.c might both share a unix.c), and that’s a lot easier to reason about and is easier to share in places where platform code might be the same in certain places. It’s also a lot easier to know what you need to implement later when the linker points to exactly what PLATFORM_* calls it couldn’t resolve for a new port.

Ohhhhhh.

Ok, I get it.

So basically in, say, your CMakeLists.txt, you know which platform you care about, so you’ll build the project with the platform-specific .c file and ignore the rest, and when you read through the code, you don’t have the #if define #elif #endif mess to read through because they’re separated into different files that never collide with each other in the same build.

Nice! Oh, and also nice is that each of these implementations can easily be unit tested because you can create a separate test for each implementation.

So I got to work. I create one header file called Persistence.h and three different .cpp files: Persistence_ANDROID.cpp, Persistence_LINUX.cpp, and Persistence_WIN32.cpp. My project’s CMakeLists.txt would create a list of .cpp files to build. Now I make sure that list includes the platform-specific version of the .cpp file in the project’s sources. So if I am building an Android version of my game, it would build Persistence_ANDROID.cpp and ignore the Linux and Windows versions of the file.

FILE (GLOB GBLIB_SOURCES *.cpp)
IF(GB_ANDROID_BUILD)
    FILE (GLOB PLATFORM_SOURCES PlatformSpecificImplementation/*_ANDROID.cpp)
ELSEIF(GB_LINUX_BUILD)
    FILE (GLOB PLATFORM_SOURCES PlatformSpecificImplementation/*_LINUX.cpp)
ELSEIF(GB_WINDOWS_BUILD)
    FILE (GLOB PLATFORM_SOURCES PlatformSpecificImplementation/*_WIN32.cpp)
ENDif(GB_ANDROID_BUILD)
ADD_LIBRARY (GB-lib ${GBLIB_SOURCES} ${PLATFORM_SOURCES})

And look at it!

std::string Persistence::getUserDataDirectory()
{
	std::string homeDirectory = RealInstanceDelegator().SDL_AndroidGetInternalStoragePath();

	std::string userDataDirectory = homeDirectory + "/" + Version::PROJECT_NAME + Version::DEMO_STATUS;

	return userDataDirectory;
}

It’s straightforward to read and tweak, especially compared to the ugly mix of code in the original version. New platforms would be easy to support by changing the build script and adding a source file.

Thanks for the pro tips, Ethan Lee!

3 replies on “A Better Way to Write Platform-specific C++ Code”

I assume from the code snippet that you are now working with Android? Is it possible to code android apps C++ as easily as with Java? I know some C++, but was always under the impression that getting C++ to work on Android devices involved a bit of a hack job as it isn’t natively supported? I’m curious to know as I enjoy coding in C++ (although I’m still an amateur), hate Java, but owuld like to program an android app 🙂

Yes, Tim, I am writing C++ for Android.

Android has the NDK, and while they don’t encourage it, it’s available.

I use libSDL2 primarily, and I have to build it and related libraries to work on Android. I’ve since updated the build scripts I use, but you can check out my Ludum Dare 33 entry at http://ludumdare.com/compo/ludum-dare-33/?action=preview&uid=251 to see how I got it done.

It’s not very hacky at all, but it did take me some time to get the build scripts together.

Comments are closed.