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!
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 asx86_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!
Note: from now on, I'm just going to talk about
x86_64
, for brevity. Everything I say still applies toi686
, if that's what you want. Just replacex86_64
withi686
whenever you see it. Remember that they are completely separate architectures to your computer, and you must do everything twice (once forx86_64
, once fori686
) 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.
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 workingmingw-w64-tools
package. In this case, we'll have to make it ourselves. Save the following script to a file namedx86_64-w64-mingw32-pkg-config
(and do similarly fori686
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 yourPATH
, 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 assdl2.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 theprefix
property set inconsistently. If your compiler fails to find include files or fails to link when usingx86_64-w64-mingw32-pkg-config
, make sure that theprefix
directory actually exists, and if it does, make sure it contains the directories you expect. You might have to change this to something likeprefix=/usr/x86_64-w64-mingw32
.
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
#pragma
s 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.
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 #define
d 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 #define
d 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 #include
s, all having their own #include
s, 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
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
.
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