Linux to Windows Cross-compilation | Blog | Superflous Returnz

Linux to Windows Cross-compilation | Blog | Superflous Returnz Linux to Windows Cross-compilation | Blog | Superflous Returnz
  • Steam
  • Nintendo Switch
  • Itch.io
  • Google Play
Superfluous and his assistant Sophie

Blog

Linux to Windows Cross-compilation

2022-02-23

You probably won't be surprised if I tell you that I do development —and also perform all of my digital activities– on GNU/Linux. Of course, as many people use Microsoft Windows to play games, it is obvious that the game must also run on this platform.

As I'm not into the idea of ​​approaching a Windows system without the presence of an exorcist, I looked early into the possibility of "cross-compiling" my game: producing a Windows-compatible executable (the famous .exe) from a Linux environment.

MinGW

Minimalist GNU for Windows, MinGW to make it short, is a set of GNU development and compilation tools for Windows which has the advantage of being available on Linux and thus allow cross-compilation. It is available in the repositories of the usual distributions and can installed, for example on Debian and derived, using:


# apt install mingw-w64

The package provides a set of executables, all of which are of the form [architecture]-w64-mingw32-[executable], for example:

  • x86_64-w64-mingw32-g++ to compile C++ for a 64-bit architecture
  • i686-w64-mingw32-gcc to compile C for a 32-bit architecture (also 64-bit compatible)
  • x86_64-w64-mingw32-windres to handle Windows resource files (which allow in particular to give an icon to an executable)

And many more, like tools for analyzing dynamic libraries required by an executable, but also things like gprof or gcov. Enough to keep you busy.

We also find software libraries… not in SO format but in DLL format of course, because it's Windows. For 64-bit architecture, they are in /usr/lib/gcc/x86_64-w64-mingw32/9.3-win32/. They include things like libstdc++-6.dll, the standard C++ library.

Third-party libraries

A reminder that Superfluous Returnz uses 3 third-party libraries:

Of course, they are all compatible with Windows (yes, it's a good idea to check that before jumping into the idea of ​​cross-compiling).

The SDL library is kind enough to provide its development libraries for MinGW directly on its download page (and this is also the case for its related components).

For the other two, MinGW development versions can be found in the MSYS2 Packages database:

If you need other libraries, this is a good place to look for them.

Setup & build

Well, we have our dev tools and our libraries, all we have to do now is plug this all together. I'm using CMake as a configuration tool, and in the case of cross-compiling, I use what's called a toolchain file, which sets a number of variables:


set(CMAKE_SYSTEM_NAME Windows)

set(CMAKE_C_COMPILER i686-w64-mingw32-gcc)
set(CMAKE_CXX_COMPILER i686-w64-mingw32-g++)
set(CMAKE_RC_COMPILER i686-w64-mingw32-windres)

set(CMAKE_FIND_ROOT_PATH  /usr/i686-w64-mingw32 /home/gee/local/i686-w64-mingw32)

file(GLOB_RECURSE SOSAGE_SYSTEM_DLLS "/usr/lib/gcc/i686-w64-mingw32/9.3-win32/*.dll")
file(GLOB_RECURSE SOSAGE_LOCAL_DLLS "/home/gee/local/i686-w64-mingw32/bin/*.dll")

set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)

I indicate with CMAKE_FIND_ROOT_PATH the places where the development environments are stored: I preferred to place the third-party libraries in a local user folder, hence the presence of /home/gee/local/.

The file(GLOB_RECURSE ...) commands list the DLL files used, which we will use in the next step.

Once the project is configured with this toolchain file, we can simply launch the compilation, and shazam! A nice superfluous-returnz.exe file is generated!

CPack-generated installer

Alright, that's all very well, but of course, if you distribute this .exe alone, it won't work: an .exe executable is not enough to distribute a video game on Windows, we also need to share the data, the required third-party libraries, and if possible do it in a user-friendly way. Luckily, CMake, through CPack, provides an automated installer creation framework.

For it to work, we must first explain how/where the software is installed with the install() commands in the CMakeLists.txt. We can of course separate the Linux part from the Windows part:


if (CMAKE_SYSTEM_NAME STREQUAL Linux)
  target_compile_options(${SOSAGE_EXE_NAME} PUBLIC -DSOSAGE_INSTALL_DATA_FOLDER="../share/${SOSAGE_EXE_NAME}/")
  install(TARGETS ${SOSAGE_EXE_NAME} DESTINATION bin)
  install(DIRECTORY ${SOSAGE_DATA_FOLDER}/data/ DESTINATION share/${SOSAGE_EXE_NAME})
  install(FILES ${SOSAGE_DATA_FOLDER}/resources/icon.svg DESTINATION share/icons/hicolor/scalable/apps/ RENAME ${SOSAGE_EXE_NAME}.svg)
  install(FILES ${SOSAGE_DATA_FOLDER}/resources/${SOSAGE_EXE_NAME}.desktop DESTINATION share/applications/)
  set(CPACK_GENERATOR "DEB;RPM")
elseif (CMAKE_SYSTEM_NAME STREQUAL Windows)
  target_compile_options(${SOSAGE_EXE_NAME} PUBLIC -DSOSAGE_INSTALL_DATA_FOLDER="data/")
  install(TARGETS ${SOSAGE_EXE_NAME} DESTINATION ".")
  install(DIRECTORY ${SOSAGE_DATA_FOLDER}/data/ DESTINATION "./data")
  install(FILES ${SOSAGE_SYSTEM_DLLS} DESTINATION ".")
  install(FILES ${SOSAGE_LOCAL_DLLS} DESTINATION ".")
  set(CPACK_GENERATOR "NSIS")
endif()

As you can see, if on Linux, we use the usual system paths (the bin/share folders of the global path /usr/bin or local /usr/local/bin or something else), on Windows, we put everything into the game directory: "everything", this includes the requied DLLs, hence why we listed them in the previous step. You may notice we specify which CPack generator to use: Deb/RPM for Gnunux, NSIS for Windows.

You may also notice that on Linux, there is the installation of a .desktop file which allows to create an entry in the application menu, and an icon. On Windows, we let the installer take care of the shortcuts/launchers part, but for the icon, we use a "resource" file sosage.rc, a simple text file which contains a single line:


id ICON "icon.ico"

We have taken care, of course, to convert our PNG icon into a Windows ICO file. Then, to make this icon associated with the produced .exe file, we use the following commands in CMake:


set(SOSAGE_SRC ${SOSAGE_SRC} "${SOSAGE_DATA_FOLDER}/resources/sosage.rc")
set(CMAKE_RC_COMPILER_INIT windres)
enable_language(RC)
set(CMAKE_RC_COMPILE_OBJECT
"<CMAKE_RC_COMPILER> <FLAGS> -O coff <DEFINES> -i <SOURCE> -o <OBJECT>")

After compiling the game, you can run the cpack command to generate the installer. Note that CPack uses a whole bunch of optional and non-optional variables to customize the installer. They are listed in the CMake doc, here are the ones I personally use:


set(CPACK_NSIS_DISPLAY_NAME ${SOSAGE_NAME})
set(CPACK_NSIS_PACKAGE_NAME ${SOSAGE_NAME})
set(CPACK_PACKAGE_INSTALL_DIRECTORY ${SOSAGE_EXE_NAME})
set(CPACK_NSIS_URL_INFO_ABOUT ${SOSAGE_URL})
set(CPACK_NSIS_MUI_ICON "${SOSAGE_DATA_FOLDER}/resources/icon.ico")
set(CPACK_NSIS_CONTACT ${SOSAGE_AUTHOR})
set(CPACK_NSIS_WELCOME_TITLE "Welcome to the installer of ${SOSAGE_NAME}")
set(CPACK_NSIS_FINISH_TITLE "The installation ${SOSAGE_NAME} is now finised")
set(CPACK_NSIS_UNINSTALL_NAME "uninstall-${SOSAGE_EXE_NAME}")
set(CPACK_NSIS_CREATE_ICONS_EXTRA
    "CreateShortCut '$SMPROGRAMS\\\\$STARTMENU_FOLDER\\\\${SOSAGE_NAME}.lnk' '$INSTDIR\\\\${SOSAGE_EXE_NAME}.exe'"
)

I differentiate between ${SOSAGE_EXE_NAME}, which is the "standardized" version used to name the executable, i.e. superfluous-returnz, and ${SOSAGE_NAME} which is the name written in a more classical way, Superfluous Returnz, used for shortcuts.

CPack creates a standard installer for Windows in the form of an .exe file which is self-sufficient and can be distributed as is for installation on the computers of players!

Testing on a virtual machine

Being able to cross-compile Linux Gnunux is great, but at some point we have to test the result, and there's no way to avoid this: we have to use a Windows system. As I haven't had a computer running on Windows for more than 10 years, I naturally leaned on virtual machines.

Good news: Microsoft provides free Windows virtual machines specifically designed for development! In my case, I prefer to keep developing on Linux, but it makes it easy to test the game.

All we have to do is download this (20 GiB, enough time to drink a coffee), import the VM into VirtualBox, and go! Let's then transfer the installer there via any preferred method (a folder shared between host and VM, for example), and let's launch everything.

The installer has a classic look, maybe a bit dated, but frankly, that's no big deal:

Installer

Shortcuts are well added to the menu:

Menu

And the game launches well!

Game

Debugging

Well, okay, I'm bragging for the article, but it probably won't surprise you if I admit that the game didn't start correctly on the first try... yes, like everything else, a port has to be tested and debugged before working.

Although it is less famous than the ones on Linux, but Windows does have its own terminal emulator to retrieve error messages. The problem is that the standard C++ outputs std::cout and std::cerr are not displayed there… they wouldn't want to make it easy, would they?

From there, you can either look at how console IOs work on Windows… or trust the SDL to do it for you and use the provided cross-platform functions, like SDL_Log(). That's what I did.

A reminder that the same problem had happened on Android, so we can just add a variant to our message display function:


#if defined(SOSAGE_ANDROID)
inline void debug (const std::string& str)
{
  __android_log_print (ANDROID_LOG_INFO, "Sosage Info", "%s", str.c_str());
}
#elif defined(SOSAGE_WINDOWS)
inline void debug (const std::string& str)
{
  SDL_Log("%s", str().c_str());
}
#else
inline void debug (const std::string& str)
{
  std::cerr << str << std::endl;
}
#endif

Actually, we could use SDL_Log() in any case, but it's also nice to show how to handle this in different ways.

Windows-specific adaptations

Again, even if the vast majority of the code will compile and work as is on Windows, some portions may require adaptations. Just as I had to do for Android, I again had to adapt the section of code that manages the language. Indeed, for some reason, Windows has decided not to conform to the std::locale of the standard library (exorcist, where are you?), so we have to call functions from the windows library.

In practice, it looks like this:


#if defined(SOSAGE_ANDROID)
#  include <jni.h>
#elif defined(SOSAGE_WINDOWS)
#  include <wchar.h>
#  include <winnls.h>
#else
#  include <locale>
#endif

std::string get_locale()
{
#if defined(SOSAGE_ANDROID)
  JNIEnv* env = (JNIEnv*)SDL_AndroidGetJNIEnv();
  jobject activity = (jobject)SDL_AndroidGetActivity();
  jclass jni_class(env->GetObjectClass(activity));
  jmethodID method_id = env->GetStaticMethodID(jni_class, "getLocale", "()Ljava/lang/String;");
  jstring jstr = (jstring) env->CallStaticObjectMethod(jni_class, method_id);
  const char *str = env->GetStringUTFChars(jstr, 0);
  std::string out (str);
  env->ReleaseStringUTFChars(jstr, str);
  env->DeleteLocalRef(jstr);
  env->DeleteLocalRef(jni_class);
  return out;
#elif defined(SOSAGE_WINDOWS)
  wchar_t name[LOCALE_NAME_MAX_LENGTH];
  int l = GetUserDefaultLocaleName(name, LOCALE_NAME_MAX_LENGTH);
  if (l == 0)
    return "";
  std::wstring ws (name, name + l);
  return std::string(ws.begin(), ws.end());
#else
  return std::locale("").name();
#endif
}

Another small subtlety that gave me a bit of trouble: the GetUserDefaultLocaleName() function is only available from a certain version of Windows (namely Windows Vista) which is not enabled by MinGW by default. We therefore have to define the WINVER variable with the correct version number when we are on Windows (we can find the list in the Microsoft doc):


#if defined(_WIN32)
#define SOSAGE_WINDOWS
#define WINVER 0x0600
#endif

Once that little definition is added, everything runs like clockwork!

Conclusion

Even though we ended up having to use a Windows system for the "testing" part, cross-compiling allows to manage all the development/build part on Linux, and it's a huge advantage: when I want to test a new version of the code, I can thus, from the same script, launch the compilation for Linux, for Android and for Windows!

And even… for Mac. Yes, let's end this article with a little announcement: I managed to build a working port for MacOS. How? Well, the exact same way: by cross-compiling from Linux. It was a bit more complicated and less well documented, but it's doable. I will tell you about it in a future article :)

i