Blog
Linux to Windows Cross-compilation
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 architecturei686-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:
Shortcuts are well added to the menu:
And the game launches well!
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 :)