Skip to content

Instantly share code, notes, and snippets.

@CobaltXII
Last active October 15, 2024 22:14
Show Gist options
  • Save CobaltXII/f6f49dd3217569b20879a5e059953544 to your computer and use it in GitHub Desktop.
Save CobaltXII/f6f49dd3217569b20879a5e059953544 to your computer and use it in GitHub Desktop.
Cross-Compiling SDL2 Programs for Windows from Linux

Cross-Compiling SDL2 Programs for Windows from Linux using MinGW-w64

I'll explain how to create programs that run on Windows (.exe files) from a Linux machine. I'll explain how to compile C and C++, and how to make 32- and 64-bit programs. I'll also explain how to use third-party libraries (we'll use SDL2 here), and how to distribute the programs. We'll use the MinGW-w64 toolchain to accomplish this.

Note: MinGW-w64 is a fork of its predecessor, MinGW. They are different. If you install MinGW-w64 in a different way than shown in this document, make sure you have MinGW-w64, not just MinGW!

Installing and Using a Cross-Compiler

To cross-compile for another architecture, you first need to know what architecture you're compiling for. The important part here is the target triplet. It's typically in this format:

machine-vendor-operatingsystem

For more information, see the OSDev Wiki. You can find your native target triplet by typing gcc -dumpmachine. On my machine, I get x86_64-linux-gnu.

Note: you can prefix your native target triple onto any existing commands. For example, g++ is the same as x86_64-linux-gnu-g++ on my machine. I can check this:

cxii@bocks:~$ ls -l /usr/bin/g++
lrwxrwxrwx 1 root root 6 Aug 11 14:28 /usr/bin/g++ -> g++-13
cxii@bocks:~$ ls -l /usr/bin/x86_64-linux-gnu-g++
lrwxrwxrwx 1 root root 6 Aug 11 14:28 /usr/bin/x86_64-linux-gnu-g++ -> g++-13

First, the machine part. This is the CPU architecture. Let's keep it simple: use x86_64 if you want to make a 64-bit program, and use i686 if you want to make a 32-bit program. The x86_64 architecture is backward compatible, so you can run an i686 program anywhere (on 32- or 64-bit hosts). Note that i686 is not forward compatible, so you can't run a 64-bit program on a 32-bit host. I'd recommend to either make just an x86_64 program (nobody uses 32-bit anymore), or both. Please don't make just an i686 program without also providing an x86_64 program.

Second, the vendor part. We'll just write w64 here, since we want to cross-compile for Windows. I have no idea why it's w64. I tried to find something that used w32, with no luck, so we'll just stick with w64. Use w64 even when using i686.

Third, the operatingsystem part. We'll just write mingw32 here. I really don't know why this is mingw32 and not mingw64, but like the above, it just works. Anyways...

Now we know our target triple. To recap, if we want 32-bit programs, it's i686-w64-mingw32. If we want 64-bit programs, it's x86_64-w64-mingw32. Now, we can simply prefix this onto our commands to get a command which targets our target triple. For example: x86_64-w64-mingw32-g++. Go ahead and install these programs if you don't have them already.

Your shell will probably tell you what packages you need to install, but I'll list them here anyways. It'll be in this format: {gcc|g++}-mingw-w64-{i686|x86-64}-{posix|win32}. That's a lot. Basically, choose gcc if you want to compile C, and g++ if you want to compile C++. Install both if you want both. Choose i686 or x86-64 based on if you want to make 32- or 64-bit programs. Note that it's a dash (-) not an underscore (_) in x86-64 (yet another discrepancy!). Choose posix if you want C11/C++11 threads, and win32 if you don't. Hint: you probably want to choose posix here.

For example, if I want to compile 64-bit C++ programs for Windows, I'd install g++-mingw-w64-x86-64-posix. Phew!

We can try compiling a simple program for Windows:

cxii@bocks:~$ printf '#include <stdio.h>\nint main() {printf("Hello, world!\\n");}\n' > main.c
cxii@bocks:~$ cat main.c
#include <stdio.h>
int main() {printf("Hello, world!\n");}
cxii@bocks:~$ x86_64-w64-mingw32-gcc main.c -o main.exe
cxii@bocks:~$ wine main.exe
Hello, world!

Installing Third-Party Libraries to Use With a Cross-Compiler

Note: from now on, I'm just going to talk about x86_64, for brevity. Everything I say still applies to i686, if that's what you want. Just replace x86_64 with i686 whenever you see it. Remember that they are completely separate architectures to your computer, and you must do everything twice (once for x86_64, once for i686) if you're targeting both.

Anyways, in the /usr directory you have /usr/bin, /usr/include, and /usr/lib. These are the binary, include and library directories for your native platform. Inside /usr/include, you'll find /usr/include/SDL2, /usr/include/X11, etc.

When you use gcc or clang, it looks in /usr/include for headers (which makes sense, it's looking for the headers for your native platform, since you're compiling for your native platform). The same idea applies for library files.

Installing packages with apt or similar will put all the relevant files into these directories. For example, if we consider SDL2, we can find it's headers in /usr/include/SDL2. We can find it's library files in /usr/lib/x86_64-linux-gnu. Why do we have to append the native target triple at the end here? I have no idea.

After installing a cross-compiler, you'll also have another directory, such as /usr/x86_64-w64-mingw32. This directory contains /usr/x86_64-w64-mingw32/bin, /usr/x86_64-w64-mingw32/include, and /usr/x86_64-w64-mingw32/lib. These are the binary, include, and library directories for the target platform, in this case, x86_64-w64-mingw32.

Now, when you use x86_64-w64-mingw32-g++ or similar, it looks in /usr/x86_64-w64-mingw32/include for headers. This makes sense, it's looking in the include directory for the specified target, not in the native include directory. The same idea applies for library files.

How do we actually install a third-party library into these directories though? Let's consider SDL2 as an example. There may be a way to do this with a package manager, but I don't know how to do that. Head over to their website. It'll take us to their GitHub releases page, and we can look for the mingw development files. Download them, and we should see a file tree like this:

cxii@bocks:~/Downloads/SDL2-devel-2.28.5-mingw/SDL2-2.28.5$ ls -l
total 104
...
drwxr-xr-x 6 cxii cxii  4096 Oct 31  2018 i686-w64-mingw32
drwxr-xr-x 6 cxii cxii  4096 Oct 31  2018 x86_64-w64-mingw32
...

There are the two target triples we talked about earlier! If we enter x86_64-w64-mingw32, we'll see the following directories:

cxii@bocks:~/Downloads/SDL2-devel-2.28.5-mingw/SDL2-2.28.5/x86_64-w64-mingw32$ ls -l
total 16
drwxr-xr-x 2 cxii cxii 4096 Nov  2 13:05 bin
drwxr-xr-x 3 cxii cxii 4096 Oct 31  2018 include
drwxr-xr-x 4 cxii cxii 4096 Nov  2 13:05 lib
...

And there are the three main directories we talked about earlier! It is now as simple as merging these three directories into /usr/x86_64-w64-mingw32.

Note: SDL2 actually provides make cross to automatically install itself for cross-compiling for you. Other libraries may do something similar. Personally, I prefer to do it myself, as these scripts have not always worked correctly.

Using Third-Party Libraries With a Cross-Compiler

Typically, pkg-config is used to gather flags needed to build software that uses a third-party library. For example, with SDL2 you can write pkg-config sdl2 --cflags to get the flags you need to pass to gcc. You can also write pkg-config sdl2 --libs to get the flags you need to pass to ld. Here's some example output on the native machine:

cxii@bocks:~$ pkg-config sdl2 --cflags
-I/usr/include/SDL2 -D_REENTRANT 
cxii@bocks:~$ pkg-config sdl2 --libs
-lSDL2

Clearly, this won't do for a cross-compilation, as it's outputting the native include directory (as it should). Not to mention that there are no Windows specific options, which we need. We need something like x86_64-w64-mingw32-pkg-config (or i686-) which we can use for this. And we're in luck, such a thing exists. It's part of the mingw-w64-tools package (and conveniently, this package covers both x86_64 and i686). Let's try it:

cxii@bocks:~$ x86_64-w64-mingw32-pkg-config sdl2 --cflags
-I/usr/x86_64-w64-mingw32/include -I/usr/x86_64-w64-mingw32/include/SDL2 -Dmain=SDL_main 
cxii@bocks:~$ x86_64-w64-mingw32-pkg-config sdl2 --libs
-L/usr/x86_64-w64-mingw32/lib -lmingw32 -lSDL2main -lSDL2 -mwindows

If you don't see output like this, or you can't find mingw-w64-tools, then you're a bit unlucky, because some distros don't provide a working mingw-w64-tools package. In this case, we'll have to make it ourselves. Save the following script to a file named x86_64-w64-mingw32-pkg-config (and do similarly for i686 if you need it):

#!/bin/bash
export PKG_CONFIG_PATH=/usr/x86_64-w64-mingw32/lib/pkgconfig
pkg-config $@

Make this file executable (chmod +x x86_64-w64-mingw32-pkg-config) and move it to somewhere in your PATH, like /usr/bin. Now try again, you should get some output as shown above.

We can try compiling a simple SDL2 program for Windows:

cxii@bocks:~$ cat sdl2.c
#include <SDL2/SDL.h>

int main(int argc, char** argv)
{
	SDL_Init(SDL_INIT_EVERYTHING);
	SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_INFORMATION, "Hello, world!", "Hello, world!", NULL);
}
cxii@bocks:~$ x86_64-w64-mingw32-gcc sdl2.c -o sdl2.exe $(x86_64-w64-mingw32-pkg-config sdl2 --cflags --libs)
cxii@bocks:~$ wine sdl2.exe
...

Make sure to package SDL2.dll with your .exe when distributing it. You can find SDL2.dll in /usr/x86_64-w64-mingw32/bin/ or /usr/i686-w64-mingw32/bin/ (or simply in the development files that you downloaded from the SDL website).

Note: the reason x86_64-w64-mingw32-pkg-config even works is because in /usr/x86_64-w64-mingw32/lib there is another directory: /usr/x86_64-w64-mingw32/lib/pkgconfig. This directory contains .pc files, such as sdl2.pc. SDL2 generously provides it's .pc file in it's source distribution (which we downloaded earlier), under the correct directory. Not all libraries are nice enough to do this though. You may have to find the .pc file yourself and move it to /usr/x86_64-w64-mingw32/lib/pkgconfig.

Note: some .pc files come slightly broken. A common problem is that they have the prefix property set inconsistently. If your compiler fails to find include files or fails to link when using x86_64-w64-mingw32-pkg-config, make sure that the prefix directory actually exists, and if it does, make sure it contains the directories you expect. You might have to change this to something like prefix=/usr/x86_64-w64-mingw32.

Using Windows Libraries With a Cross-Compiler

You can use the entire Windows API when cross-compiling. For example, we can try building a simple program that shows a message box, then plays a sound from a file (explosion.wav).

cxii@bocks:~$ cat playsound.c
#include <windows.h> // Note: lowercase!

int main()
{
    MessageBox(NULL, "I'm going to play an explosion sound", "Message Box", MB_OK | MB_ICONWARNING);
    PlaySound(TEXT("explosion.wav"), NULL, SND_FILENAME | SND_SYNC);
    return 0;
}

cxii@bocks:~$ x86_64-w64-mingw32-gcc playsound.c -o playsound.exe
/usr/bin/x86_64-w64-mingw32-ld: /tmp/*.o:playsound.c:(.text+0x25): undefined reference to `__imp_PlaySoundA'
collect2: error: ld returned 1 exit status

It didn't work. The reason for this is that x86_64-w64-mingw32-gcc couldn't find the library that contains the Windows API function PlaySound. Let's look online to find out which library we need. Searching for "playsound windows api" will take us to a Microsoft page. We can scroll to the bottom to find the Requirements table, which should say something like this:

Key Value
Minimum supported client Windows 2000 Professional [desktop apps only]
Minimum supported server Windows 2000 Server [desktop apps only]
Header Mmsystem.h (include Windows.h)
Library Winmm.lib
DLL Winmm.dll
Unicode and ANSI names PlaySoundW (Unicode) and PlaySoundA (ANSI)

This tells us that PlaySound is defined in mmsystem.h, but that we shouldn't include this directly, and that we should rather just #include <windows.h>. Notice the lowercase!

This also tells us that we should link with winmm.lib. We can direct the compiler to do this by passing it the flag -winmm. Since this is MinGW-w64, not real Windows, this flag will tell the compiler to look for libwinmm.a, which is the MinGW-w64 analogue of winmm.lib. If you're curious, you can actually find this file in /usr/x86_64-w64-mingw32/lib. Note that you do not have to package winmm.dll (or any such DLL mentioned on a page like this)! Wine knows where to find them automatically, and real Windows installations have them built in.

You might be asking, how come we didn't get a linker error for MessageBox? If we check the Microsoft documentation for that function, we'll see that it specifies the library as user32.lib. This happens to be a library that MinGW-w64 links with by default. You can try the following command to see all the automatically linked libraries:

cxii@bocks:~$ x86_64-w64-mingw32-gcc -### -xc /dev/null -o /dev/null 2>&1 | grep -oE '"-plugin-opt=-pass-through=-l[^"]+"' | sed 's/"-plugin-opt=-pass-through=-l\(.*\)"/\1/' | sort -u
advapi32
gcc
gcc_eh
kernel32
mingw32
mingwex
moldname
msvcrt
pthread
shell32
user32

You don't have to link with these (i.e., no need to type -luser32), but if you want to, it won't hurt.

So let's try again, with the correct flags this time:

cxii@bocks:~$ x86_64-w64-mingw32-gcc playsound.c -o playsound.exe -lwinmm
cxii@bocks:~$ wine playsound.exe

It worked!

Note: don't try to use nonstandard #pragmas like #pragma comment(lib, "winmm.lib") with MinGW-w64. This doesn't work, it only works on real Windows.

Note: you should also pay attention to the "Minimum supported" information given on the Microsoft page. If you (or a library that you use) uses an unsupported function, the executable will fail at runtime when running on an old version of Windows. This isn't really applicable to Windows 10 and up, but you have a chance to see problems with Windows 7, and you'll most likely see problems with Windows XP.

Helpful Tricks

It's useful to see which directories gcc looks in for headers, by default. Use this command for C:

cxii@bocks:~$ gcc -E -Wp,-v -xc /dev/null 2>&1 | sed '/End of search list\./q'
#include "..." search starts here:
#include <...> search starts here:
 /usr/lib/gcc/x86_64-linux-gnu/13/include
 /usr/local/include
 /usr/include/x86_64-linux-gnu
 /usr/include
End of search list.

Use this command for C++:

cxii@bocks:~$ g++ -E -Wp,-v -xc++ /dev/null 2>&1 | sed '/End of search list\./q'
#include "..." search starts here:
#include <...> search starts here:
 /usr/include/c++/13
 /usr/include/x86_64-linux-gnu/c++/13
 /usr/include/c++/13/backward
 /usr/lib/gcc/x86_64-linux-gnu/13/include
 /usr/local/include
 /usr/include/x86_64-linux-gnu
 /usr/include
End of search list.

You can do the same thing for your cross-compiler, just add the prefix, like usual (e.g., gcc turns into x86_64-w64-mingw32-gcc):

cxii@bocks:~$ x86_64-w64-mingw32-gcc -E -Wp,-v -xc /dev/null 2>&1 | sed '/End of search list\./q'
#include "..." search starts here:
#include <...> search starts here:
 /usr/lib/gcc/x86_64-w64-mingw32/12-posix/include
 /usr/lib/gcc/x86_64-w64-mingw32/12-posix/include-fixed
 /usr/lib/gcc/x86_64-w64-mingw32/12-posix/../../../../x86_64-w64-mingw32/include
End of search list.

cxii@bocks:~$ x86_64-w64-mingw32-g++ -E -Wp,-v -xc++ /dev/null 2>&1 | sed '/End of search list\./q'
#include "..." search starts here:
#include <...> search starts here:
 /usr/lib/gcc/x86_64-w64-mingw32/12-posix/include/c++
 /usr/lib/gcc/x86_64-w64-mingw32/12-posix/include/c++/x86_64-w64-mingw32
 /usr/lib/gcc/x86_64-w64-mingw32/12-posix/include/c++/backward
 /usr/lib/gcc/x86_64-w64-mingw32/12-posix/include
 /usr/lib/gcc/x86_64-w64-mingw32/12-posix/include-fixed
 /usr/lib/gcc/x86_64-w64-mingw32/12-posix/../../../../x86_64-w64-mingw32/include
End of search list.

Notice that these commands clearly show that /usr/x86_64-w64-mingw32/include is part of the include path (since the /usr/lib/gcc/x86_64-w64-mingw32/12-posix/../../../../x86_64-w64-mingw32/include path is actually equivalent).

You can also see the linker search path (in case you want to know if it's looking for libraries in the correct places), using these commands:

cxii@bocks:~$ ld --verbose | grep SEARCH_DIR | tr -s ' ;' \\012
SEARCH_DIR("=/usr/local/lib/x86_64-linux-gnu")
SEARCH_DIR("=/lib/x86_64-linux-gnu")
SEARCH_DIR("=/usr/lib/x86_64-linux-gnu")
...

cxii@bocks:~$ x86_64-w64-mingw32-ld --verbose | grep SEARCH_DIR | tr -s ' ;' \\012
SEARCH_DIR("/usr/x86_64-w64-mingw32/lib")

To find out where a specific header (or anything) is, use find /. If you don't have an SSD, this might be slow.

cxii@bocks:~$ find / -type f -name "windows.h"
/usr/share/mingw-w64/include/windows.h

cxii@bocks:~$ find / -type f -name "SDL.h"
/usr/include/SDL2/SDL.h
/usr/i686-w64-mingw32/include/SDL2/SDL.h
/usr/x86_64-w64-mingw32/include/SDL2/SDL.h

cxii@bocks:~$ find / -type f -name "epoll.h"
/usr/include/x86_64-linux-gnu/sys/epoll.h
/usr/include/x86_64-linux-gnu/bits/epoll.h

To find out which file a macro is #defined in, use grep -rl. Like above, this might be slow without an SSD.

cxii@bocks:~$ grep -rl "INADDR_ANY" /usr/include
...
/usr/include/netinet/in.h

cxii@bocks:~$ grep -rl "INADDR_ANY" /usr/share/mingw-w64/include
...
/usr/share/mingw-w64/include/winsock.h

To find out which file a macro is #defined in, when you're already including that file, add a macro redefinition to the source code. This is useful when you have a ton of #includes, all having their own #includes, and so on, and you have access to the macro from within your code, but you don't know exactly which header is defining the macro.

cxii@bocks:~$ x86_64-w64-mingw32-g++ ...
src/Arcane2D/IpAddress.cpp:33: warning: "INADDR_ANY" redefined
   33 | #define INADDR_ANY
      | 
In file included from include/Arcane2D/IpAddress.h:8,
                 from src/Arcane2D/IpAddress.cpp:26:
/usr/share/mingw-w64/include/winsock2.h:176: note: this is the location of the previous definition
  176 | #define INADDR_ANY (u_long)0x00000000

Common Pitfalls

Sometimes when you distribute the .exe file and try to run it under Windows (not wine) you'll get an error mentioning something about libstdc++-6.dll and libgcc_s_seh-1.dll (or similar). There are two solutions, you can either link with these libraries statically, or distribute the .dll files yourself.

If you want to link statically, pass -static-libgcc -static-libstdc++ to ld. If you want to distribute the .dll files yourself, look in /usr/lib/gcc/x86_64-w64-mingw32 or /usr/lib/gcc/i686-w64-mingw32 for the .dll files you need, and copy them to the same directory as your .exe file.

You may also get errors about Wine or Windows not being able to find some other DLLs. Some common ones are zlib1.dll and libwinpthread-1.dll. You can find these files in /usr/x86_64-w64-mingw32/lib.

Overview

Here's an overview of the parts of the file tree that you're likely to need while cross-compiling.

/usr                            
    /include                <-- native headers (e.g., SDL2/SDL.h)
    /lib
        /gcc
	        /i686-w64-mingw32       <-- 32-bit Windows GCC .a's and .dll's (e.g., libstdc++-6.dll, libgcc_s_seh-1.dll)
	        /x86_64-linux-gnu       <-- native GCC .a's and .so's (e.g., libstdc++.so, libgcc_s.so)
	        /x86_64-w64-mingw32     <-- 64-bit Windows GCC .a's and .dll's (e.g., libstdc++-6.dll, libgcc_s_seh-1.dll)
        /x86_64-linux-gnu       <-- native .a's and .so's (e.g., libSDL2.so)
            /pkgconfig              <-- native .pc's (e.g., sdl2.pc)
    /share
        /mingw-w64
            /include                <-- 32- and 64-bit Windows headers (e.g., windows.h)
    /x86_64-w64-mingw32
        /bin                    <-- 64-bit Windows .dll's (e.g., SDL2.dll)
        /include                <-- 64-bit Windows headers (e.g., SDL2/SDL.h)
        /lib                    <-- 64-bit Windows .a's and .dll's (e.g., libwinpthread-1.dll, zlib1.dll)
            /pkgconfig              <-- 64-bit Windows .pc's (e.g., sdl2.pc)
    /i686-w64-mingw32       <-- same structure as x86_64-w64-mingw32, except for 32-bit
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment