Cross-compiling C programs for MS Windows using MinGW

2019-09-10

As you may know, I tend to tinker on video games in my spare time. It's been a long time since I actually released anything worthwhile... but speaking of releases, there's always the question of multi-platform support. When making a browser game, the browser is your platform and you don't really worry about the OS underneath. When using a popular engine, you may test if the various releases work mostly as they should, but you're still mostly relying on the original developers' ability to make it run on all the platforms.

But things are vastly different when your game runs without an engine, or using an engine of your own making. You have to take care yourself of making sure the program runs fine on whichever platforms you want to support. But even before running, there's the matter of building – you have to actually compile the program for the target platform. One way to do this is, of course, having a machine running said platform (possibly in multi-boot), which comes with its own set of annoyances. The other solution is to cross-compile – and that's what I'm gonna shortly describe here.

Enter MinGW

For cross-compiling C / C++ programs, I use MinGW – Minimalist GNU for Windows – which, as the name suggests, is a set of programs (ports of gcc, gdb, etc.), headers and static libraries that allow to cross-compile stuff for MS Windows on other platforms. Setting up a MinGW environment from scratch can be tiresome, but luckily the most popular distros package MinGW itself, and MinGW ports of some libraries, in their repositories.

Install the MinGW compiler from the distro is as easy as it is with any other piece of software.

# When targeting 64-bit Windows root $ dnf install mingw64-gcc mingw64-gcc-g++ mingw64-... # When targeting 32-bit Windows root $ dnf install mingw32-gcc mingw32-gcc-g++ mingw32-...

Configuring the build process

Once you've got the compiler installed, it's time to take a look at your own project and its build system.

With Make

If you're using make to manage your build process, then chances are your Makefile already works fine for cross-compiling and you won't need to make any changes.

Basically, there are two requirements at this stage:

  1. It should be possible to specify which C / C++ compiler should be used (e.g. via CC)
  2. It should be possible to specify linker flags (e.g. via LDLIBS)

If you're using hard-coded compiler name or flags, then it's time to edit the Makefile and bring it up to shape. Once all that's done, just specify the MinGW cross-compiler and Windows-specific linker flags and you're good to go.

# For 64-bit builds user $ CC=x86_64-w64-mingw32-gcc LDLIBS=-lmingw32 make [args...] # For 32-bit builds; similar as above, only the compiler name differs user $ CC=i686-w64-mingw32-gcc LDLIBS=-lmingw32 make [args...]

If you're compiling C++ and not C, you will want to use the CXX variable and the ...mingw32-gcc-g++ compiler.

With CMake

When using CMake, you will need to make one small change to your CMakeLists.txt: that is, adding the mingw32 library to your target_link_libraries(). You may scoff at this and say _"oh, I can just use -DCMAKE_EXE_LINKER_FLAGS"_, but the problem with using that variable is that when the compiler is invoked, the variable's contents are placed before object names, like this:

x86_64-w64-mingw32-gcc -Wall -Wextra -lmingw32 -lm -o "build/my-game.exe" build/mygame.o build/stuff.o build/fluff.o

Because the order of arguments is important for the linker (libraries must follow objects that refer them), the build will likely fail with a lot of "unresolved reference" errors. When specifying the libraries via target_link_libraries(), the command is constructed with the library names following the object names, and everything ends up fine and dandy.

Once you've made said change, go ahead run CMake using the MinGW toolchain file.

# For 64-bit builds; other distros may put the toolchain file under a different path user $ cmake -DCMAKE_TOOLCHAIN_FILE=/usr/share/mingw/toolchain-mingw64.cmake [args...] # For 32-bit builds; same as above, but using the 32-bit profile user $ cmake -DCMAKE_TOOLCHAIN_FILE=/usr/share/mingw/toolchain-mingw32.cmake [args...] # Once the build files are generated using the correct toolchain file, # you no longer need to refer to it and can just perform a normal build user $ cmake --build ./

If it looks like your distro doesn't provide a ready-made toolchain file, you can try grabbing the one from Fedora and editing it appropriately (see References at the end of article).

Library-specific quirks

Depending on the libraries you're using, there may be some extra steps involved. For example, when using SDL2, you will also need to add the SDL2main library to the linker flags. The reason for this is that on MS Windows, there's a strict split between "console" and "GUI" programs – the first ones always spawn a cmd.exe window (the MS Windows shell) where they perform their stdin/stdout IO, whereas the second ones don't have that. While console programs begin their execution with the main() function, GUI programs typically begin their execution with the WinMain() function.

To spare you the pain of having to have two versions of your main function, the SDL2main static library provides its own implementation of WinMain(), which executes some additional SDL initialization code and then calls your main() function. This approach is commonly seen in other libraries – for example, both the Allegro and SFML game programming libraries provide their respective allegro_main and sfml-main static libraries.

What about them dependencies?

But yes, all this time I've conveniently left out one thing out of the equation – that being everyone's favourite thing in software development, dependencies.

Writing a game using your own engine is already masochistic, so unless you're trying to achieve new extremes and only use native APIs, your game probably depends on some popular multimedia library, like Allegro, SDL or SFML. To successfully cross-compile your game for MS Windows, you'll also need to take care of that.

The easy way: installing dependencies from the distro repo

If you're fortunate enough and your whole set of dependencies has MinGW versions available in the package manager, the whole thing becomes a cakewalk. Just install stuff from the repository and you're golden.

root $ dnf install mingw64-SDL2 mingw64-SDL2_image mingw64-SDL2_mixer mingw64-SDL2_ttf

The medium way: installing pre-compiled dependencies manually

While the scenario above is definitely the simplest, handling dependencies isn't that hard if their authors provide MinGW development files. For example, let's consider the Allegro library. The releases on GitHub contain many downloable variants of each version, MinGW dev kits being among them. Should you download one of them (pick the one appropriate for your cross-compilation target) and extract the archive, you'll see a directory structure like this:

$ ls allegro-5.2.5.0/ bin/ include/ lib/

The directories in this archive contain DLL files (in bin/), headers (in include/) and .a files – simply put, everything you need to be able to compile and link a program against Allegro, without having to compile Allegro itself. Now all that's left to do is to copy those files to MinGW root – that's /usr/x86_64-w64-mingw32/sys-root/mingw/ for 64-bit libraries and /usr/i686-w64-mingw32/sys-root/mingw/ for 32-bit libraries.

The hard way: compiling dependencies from source

At the end of the list is, of course, compiling dependencies from source. Though time-consuming, it's still relatively easy, as you just need to apply the approach described before to every library you need. The hardest part of compiling your whole dependency chain is, in my experience, tweaking every Makefile / CMakeLists.txt as you go to make each library recognize where its dependencies are – though you can avoid this by installing each compiled library to the MinGW root directory (if you don't mind the mess, that is).

Taking care of the DLLs

Once you've got your game compiled, the last thing that's left to do is to package and publish it. But for the executable to run correctly, you're going to need all the DLLs it depends on. To find out which shared objects dynamically loaded libraries are needed, you can use the objdump program:

user $ objdump -x my-awesome-game.exe | grep 'DLL Name:'

Now that you know which DLLs are needed, you need to find and copy them. If you want to it by hand, keep in mind that DLLs can depend on other DLLs, so grabbing only the direct dependencies of your game's EXE may not be enough. If you're looking for an easy, automated way, you can use copydeps, which is a small Python program I wrote recently for this exact purpose.

References

Share with friends

e-mail Google+ Hacker News LinkedIn Reddit Tumblr VKontakte Wykop Xing

Comments

Do you have some interesting thoughts to share? You can comment by sending an e-mail to blog-comments@svgames.pl.