Słomkowski's technical musings

Playing with software, hardware and touching the sky with a paraglider.

Compiling C++ application for Windows target under Linux


Since forever, I have used Arch Linux for day-to-day computing. Nonetheless, from time to time I need to write some simple code in C++ for Windows. Switching back and forth between operating systems is cumbersome, as is compiling inside a virtual machine.

The classic Windows port of GCC was MinGW (Minimalist GNU for Windows). It supported only 32-bit targets. Nowadays, everyone uses its fork, Mingw-w64. Quite surprisingly, it is available in the repositories of major Linux distributions. It comes complete with a wealth of open-source libraries cross-compiled for the Windows platform.

Leveraging this tool makes writing multi-platform software almost frictionless. It works well in the CLion IDE too. For demonstration purposes, I provide a sample repository. It contains a CMake project of a DLL and Windows executables built with Mingw-w64.

Installing Mingw-w64 toolchain

On Arch Linux, I recommend using the ownstuff unofficial repository. The compiler is packaged in a group named mingw-w64. You might try compiling it from the AUR, but that can be a real pain. There are also many packages available for various third-party libraries. All package names start with mingw-w64-. For more details, please refer to the Arch Wiki.

Mingw-w64 is also available in the official repository under Debian. It is found under the meta-package gcc-mingw-w64. There are numerous precompiled libraries, all containing the mingw-w64 string in their package names. There is also an entry on the Debian Wiki about this topic.

General configuration

The most important thing is to define the compiler executables and set CMAKE_SYSTEM_NAME:

cmake_minimum_required(VERSION 3.10)

project(mingw-test)

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_RANLIB i686-w64-mingw32-ranlib)

set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++23")
set(CMAKE_SHARED_LIBRARY_LINK_CXX_FLAGS)

Then, you simply define the executable:

add_executable(example.exe example.cpp)

I prefer force-naming the CMake targets with the .exe suffix. This ensures the CMake target name matches the output filename. If you name the target executable simply example, Mingw-w64 generates the file example.exe anyway. However, CLion won’t easily recognize it as a runnable application, since it only scans the CMake target names.

To cross-compile a dynamically loadable library, use add_library. For convenience, it is best to set the PREFIX and SUFFIX properties to empty strings. This way, you can use the full filename (including the .dll extension) as the target name.

add_library(shared_lib.dll SHARED shared_lib.cpp shared_lib.h)
set_target_properties(shared_lib.dll PROPERTIES
        PREFIX ""
        SUFFIX ""
        LINK_FLAGS "-Wl,--add-stdcall-alias"
        POSITION_INDEPENDENT_CODE 0 # this is to avoid MinGW warning; MinGW generates position-independent-code for DLL by default
)

To link the executable with the aforementioned DLL, add the following:

target_link_libraries(example.exe shared_lib.dll)

Using pkg-config

The common way to define library compilation flags and directories under Unix is by using the pkg-config tool. This ensures your codebase does not depend on the exact locations of libraries and headers on the operating system. Each library provides a .pc file containing build parameters, such as header locations and compiler flags. Many packages compiled for MinGW-w64 also provide .pc files, at least on Debian and Arch Linux.

For example, let’s add zlib as a dependency:

Load the PkgConfig CMake package and find zlib:

include(FindPkgConfig)
find_package(PkgConfig REQUIRED)

pkg_check_modules(ZLIB "zlib")

Add dependencies to the example.exe executable:

include_directories(${ZLIB_INCLUDE_DIRS})
target_link_libraries(example.exe ${ZLIB_LIBRARIES})

Adding runtime libraries

When you attempt to run your freshly cross-compiled program under Wine, it will likely fail with a message like this:

0009:err:module:import_dll Library libstdc++-6.dll (which is needed by L"your-program.exe") not found

This indicates that Wine cannot find the runtime libraries on which your program depends. These libraries are usually located under /usr/i686-w64-mingw32/bin (that’s right, bin, not lib). You must add this path to the PATH environment variable within the Wine subsystem.

To do so, edit the file ~/.wine/system.reg. This file represents the Windows Registry used by Wine. Find the definition of the PATH variable in the [System\\CurrentControlSet\\Control\\Session Manager\\Environment] section. Append the library path, translated into a Windows-like format: Z:\usr\i686-w64-mingw32\bin. The result should look more or less like this:

"PATH"=str(2):"C:\\windows\\system32;C:\\windows;C:\\windows\\system32\\wbem;Z:\\usr\\i686-w64-mingw32\\bin"

Alternatively, you can distribute the runtime libraries alongside your application. To find the exact library files your application needs, run:

objdump -p example.exe | grep 'DLL Name:'

KERNEL32.dll and USER32.dll are always provided by Windows. However, the other DLLs are most likely runtime libraries from Mingw-w64, which you have to bundle.

Running with Wine under CLion

Your CMake project should load in CLion without issues. Windows-specific headers like windows.h should be available, as they are provided by the Mingw-w64 toolchain. The build should configure itself automatically.

If you have the binfmt_misc mechanism enabled on your Linux system, you can run the Windows executable directly, like any other executable or script: ./example.exe. CLion automatically detects the executables to run:

Run/Debug configuration window.

To run the Windows application without binfmt_misc, modify the generated Run/Debug Configurations to call Wine, as shown in the screenshot below:

Run/Debug configuration window.