coding windows c++ on linux

by on
9 minute read

Using Clang targeting Microsoft's Visual C++ compiler platform can be a useful tool for authoring Windows centric code from Linux or macOS. It's one additional mechanism by which I can support Windows while reducing its usage. I'm quite happy with it.

I'll describe a few small tricks I employ for coding for Windows while on ArchLinux and also, how to completely evade one of the most bloatware pieces of software there's: Visual Studio.

Step 1: unzip -LL EnterpriseWDK.zip

All you need to author C/C++ Windows code you can find in one zip file provided by Microsoft, the Enterprise WDK. It contains Windows SDK headers, C and C++ runtimes, the Driver Development Kit, a build system based on a version of Visual Studio (just the build tools minus the IDE 😄!).

Unzipping it with -LL will be helpful as explained in the following step.

Step 2: ciopfs

ciopfs is a nice and simple FUSE based tool with the purpose of mounting a directory tree as case insensitive while using a case sensitive system. It's generally available in Linux software repositories.

It can be used both from command line:

ciopfs <source directory> <destination mountpoint directory>

As well from boot through fstab:

<source directory>  <mountpoint>  ciopfs  allow_other,default_permissions,use_ino,attr_timeout=0  0 0

All the files you want to access in case insensitive form at mountpoint must be in lowercase at source directory. ciopfs will ignore anything in source directory that's not all lowercase.

That's why unzipping the Enterprise WDK with unzip -LL is useful, the flag will make the unzipped files lowercase while unzipping, so that you're ready to go with ciopfs right after unzipping.

You may also go the route of turning all header inclusions to lowercase instead, avoiding ciopfs. For example:

rg -0l '#\s*include\s*"\s*[^"]*[A-Z][^"]*\s*"' /mnt/c/WDK/1703 \
    | xargs -0 sed -i 's/\(#\s*include\s*"\s*[^"]*[A-Z][^"]*\s*"\)/\L\1\E/'
rg -0l '#\s*include\s*<\s*[^>]*[A-Z][^>]*\s*>' /mnt/c/WDK/1703 \
    | xargs -0 sed -i 's/\(#\s*include\s*<\s*[^>]*[A-Z][^>]*\s*>\)/\L\1\E/'

changes all header inclusions to lowercase for all the files at /mnt/c/WDK/1703 , using ripgrep and sed.

Step 3: clang

You may wonder why care about having case insensitive access to a directory tree. Well, this is one of many ways you can employ to solve the non-portable header inclusion that's prone to happen with Windows header files. This is so that source files having #include <windows.h>, #include <Windows.h>, etc will compile regardless. This is not an issue on Windows, and hence why it happens there, but it doesn't work on case sensitive systems like Linux.

With that out of the way, all you need to check syntax of a C or C++ source file through Clang as if using Microsoft Visual C++'s compiler are correct flags:

#!/bin/sh

clang -x c++ \
    --target=i386-pc-windows-msvc \
    -fsyntax-only \
    -ferror-limit=64 \
    -fms-compatibility-version=19 \
    -Wall \
    -Wextra \
    -Wno-unknown-pragmas \
    -U__clang__ \
    -U__clang_version__ \
    -U__clang_major__ \
    -U__clang_minor__ \
    -U__clang_patchlevel__ \
    -DWIN32 \
    -D_WINDOWS \
    -DNDEBUG \
    -D_MT \
    -D_X86_=1 \
    -DNOMINMAX \
    -D_WIN32_WINNT=0x0501 \
    -DWIN32_LEAN_AND_MEAN=1 \
    -D_CRT_SECURE_NO_WARNINGS=1 \
    -nostdinc \
    -isystem '/mnt/wdk/1703/program files/windows kits/10/include/10.0.15063.0/shared' \
    -isystem '/mnt/wdk/1703/program files/windows kits/10/include/10.0.15063.0/ucrt' \
    -isystem '/mnt/wdk/1703/program files/windows kits/10/include/10.0.15063.0/um' \
    -isystem '/mnt/wdk/1703/program files/microsoft visual studio 14.0/vc/include' \
    -D__EDG__=1 \
    -DBOOST_PP_VARIADICS=1 \
    -I '/opt/src/boost' \
    "$@"

I'll left looking up the meaning of most of these flags as an exercise and talk about the most tricky ones:

  • -U__clang__ et al.
    Avoid having __clang__ et al. defined so that Clang doesn't get identified by multiplatform libraries, enforcing them to assume it's MSVC.
  • -D__EDG__=1 and -DBOOST_PP_VARIADICS=1
    These two flags are solely necessary for most part of Boost headers, without them Boost headers will make heavy use of MSVC preprocessor magic that Clang is unable to cope with. This is a hack to make Boost assume we're in MSVC IntelliSense mode instead of the actual compiler, so that it doesn't make use of the preprocessor magic that Clang doesn't support yet (and probably never will).

The previous script can be used to check compilation errors and warnings of a C++ source file using Clang but acting like Microsoft's compilers and using their standard library implementation and system headers.


Bonus 1: libclang

For my fork of YouCompleteMe I use a variation of the following base .ycm_extra_conf.py on my C and C++ user mode Windows projects:

import os
import ycm_core

common_flags = [
    '--target=i386-pc-windows-msvc',
    '-ferror-limit=64',
    '-fms-compatibility-version=19',
    '-Wall',
    '-Wextra',
    '-Wno-unknown-pragmas',
    '-U__clang__',
    '-U__clang_version__',
    '-U__clang_major__',
    '-U__clang_minor__',
    '-U__clang_patchlevel__',
    '-DWIN32',
    '-D_WINDOWS',
    '-DNDEBUG',
    '-D_MT',
    '-D_X86_=1',
    '-DNOMINMAX',
    '-D_WIN32_WINNT=0x0501',
    '-DWIN32_LEAN_AND_MEAN=1',
    '-D_CRT_SECURE_NO_WARNINGS=1',
    '-nostdinc',
    '-isystem', '/mnt/wdk/1703/program files/windows kits/10/include/10.0.15063.0/shared',
    '-isystem', '/mnt/wdk/1703/program files/windows kits/10/include/10.0.15063.0/ucrt',
    '-isystem', '/mnt/wdk/1703/program files/windows kits/10/include/10.0.15063.0/um',
    '-isystem', '/mnt/wdk/1703/program files/microsoft visual studio 14.0/vc/include',
    ]

c_flags = [ '-x', 'c' ] + common_flags

cxx_flags = [
    '-x', 'c++',
    '-D__EDG__=1',
    '-DBOOST_PP_VARIADICS=1',
    '-I', '/opt/src/boost',
    ] + common_flags

# Set this to the absolute path to the folder (NOT the file!) containing the
# compile_commands.json file to use that instead of 'flags'. See here for
# more details: http://clang.llvm.org/docs/JSONCompilationDatabase.html
#
# Most projects will NOT need to set this to anything; you can just change the
# 'flags' list of compilation flags.
compilation_database_folder = ''

if os.path.exists( compilation_database_folder ):
  database = ycm_core.CompilationDatabase( compilation_database_folder )
else:
  database = None

C_SOURCE_EXTENSIONS   = [ '.c', '.m' ]
CXX_SOURCE_EXTENSIONS = [ '.cpp', '.cxx', '.cc', '.mm' ]
SOURCE_EXTENSIONS     = C_SOURCE_EXTENSIONS + CXX_SOURCE_EXTENSIONS

def DirectoryOfThisScript():
  return os.path.dirname( os.path.abspath( __file__ ) )


def MakeRelativePathsInFlagsAbsolute( flags, working_directory ):
  if not working_directory:
    return list( flags )
  new_flags = []
  make_next_absolute = False
  path_flags = [ '-isystem', '-I', '-iquote', '--sysroot=' ]
  for flag in flags:
    new_flag = flag

    if make_next_absolute:
      make_next_absolute = False
      if not flag.startswith( '/' ):
        new_flag = os.path.join( working_directory, flag )

    for path_flag in path_flags:
      if flag == path_flag:
        make_next_absolute = True
        break

      if flag.startswith( path_flag ):
        path = flag[ len( path_flag ): ]
        new_flag = path_flag + os.path.join( working_directory, path )
        break

    if new_flag:
      new_flags.append( new_flag )
  return new_flags


def IsHeaderFile( filename ):
  extension = os.path.splitext( filename )[ 1 ]
  return extension in [ '.h', '.hxx', '.hpp', '.hh' ]


def GetCompilationInfoForFile( filename ):
  # The compilation_commands.json file generated by CMake does not have entries
  # for header files. So we do our best by asking the db for flags for a
  # corresponding source file, if any. If one exists, the flags for that file
  # should be good enough.
  if IsHeaderFile( filename ):
    basename = os.path.splitext( filename )[ 0 ]
    for extension in SOURCE_EXTENSIONS:
      replacement_file = basename + extension
      if os.path.exists( replacement_file ):
        compilation_info = database.GetCompilationInfoForFile(
          replacement_file )
        if compilation_info.compiler_flags_:
          return compilation_info
    return None
  return database.GetCompilationInfoForFile( filename )


# This is the entry point; this function is called by ycmd to produce flags for
# a file.
def FlagsForFile( filename, **kwargs ):
  if database:
    # Bear in mind that compilation_info.compiler_flags_ does NOT return a
    # python list, but a "list-like" StringVec object
    compilation_info = GetCompilationInfoForFile( filename )
    if not compilation_info:
      return None

    return {
      'flags': MakeRelativePathsInFlagsAbsolute(
        compilation_info.compiler_flags_,
        compilation_info.compiler_working_dir_ ) }
  else:
    relative_to = DirectoryOfThisScript()
    extension = os.path.splitext( filename )[ 1 ]
    if extension in C_SOURCE_EXTENSIONS:
      return {
        'flags': MakeRelativePathsInFlagsAbsolute( c_flags, relative_to ) }
  return { 'flags': MakeRelativePathsInFlagsAbsolute( cxx_flags, relative_to ) }

For kernel mode I change flags to:

common_flags = [
    '--target=i386-pc-windows-msvc',
    '-ferror-limit=64',
    '-fms-compatibility-version=19',
    '-Wall',
    '-Wextra',
    '-Wno-unknown-pragmas',
    '-U__clang__',
    '-U__clang_version__',
    '-U__clang_major__',
    '-U__clang_minor__',
    '-U__clang_patchlevel__',
    '-DWIN32',
    '-D_WINDOWS',
    '-DNDEBUG',
    '-D_MT',
    '-D_X86_=1',
    '-DNOMINMAX',
    '-D_WIN32_WINNT=0x0501',
    '-DWIN32_LEAN_AND_MEAN=1',
    '-D_CRT_SECURE_NO_WARNINGS=1',
    '-nostdinc',
    '-isystem', '/mnt/wdk/1703/program files/windows kits/10/include/10.0.15063.0/shared',
    '-isystem', '/mnt/wdk/1703/program files/windows kits/10/include/10.0.15063.0/km/crt',
    '-isystem', '/mnt/wdk/1703/program files/windows kits/10/include/10.0.15063.0/km',
    '-I', '.',
    ]

c_flags = [ '-x', 'c' ] + common_flags

cxx_flags = [ '-x', 'c++' ] + common_flags

Bonus 2: NFS on Windows

I use KVM and virt-manager on Linux and to let my Windows VMs access my host folders I use NFS shares. I guess it's not the most performant solution, but it does work.

Conclusion

I have a Windows partition on my machine and I just extract the Enterprise WDK there for the case I need to compile stuff there. When on Linux (which I use most of the time) I just mount the Windows partition and reuse the same WDK installation with ciopfs.

When using Windows virtual machines on Linux to actually compile stuff, I use some mechanism to let the Windows machine have access to the source files on the Linux developing machine.

Having CMake based projects coupled with WDK's msbuild is sufficient to have C and C++ projects started from scratch and built. I couple this practice with the previous knowledge so I don't leave my usual development environment to tamper with Windows code, only for final tests and builds, which is generally just a workspace away, where I leave a VM instance in full screen with Cmder ready to run build commands.

A note on YouCompleteMe

While I use YouCompleteMe, I actually hate and love it. I love it because it does the job of completion and diagnostics on Vim reasonably well. I hate it because it's bloated and works just reasonably well, where it could do better.

  • I have around 100 plugins in my .vimrc, just one of these plugins, YouCompleteMe, is responsible for 72% of the .vim/plugged directory size, it takes 1.3GB and submodule clones stuff for programming languages I'll never care for.

  • I really don't like YouCompleteMe's moto of looking to emulate all possible IDE-like features in one single plugin. It's one of the reasons it's bloated, and it's completely against Unix philosophy of "do one thing, do it well".

  • It has a bad reputation of installation problems, mostly due to deploying on python on user's machines and lack of release packages. I particularly don't have issues with this but many people have.

  • It's instant most of the time for C and C++ but there are some really annoying pain points. Currently it's unable to tackle very large C headers to do semantic completion, this happens solely because of how YouCompleteMe works internally. When there are too many identifiers at global scope it can get slow to obtain code completion, not due to libclang, it's itself to blame alone. Having many identifiers at global scope is quite common to happen with C libraries, even more with Windows headers like windows.h, which is huge. That's why when you hit ctrl-space to get a function name at global scope you may feel completion is being slow. It's sad to know that libclang is being fast but YouCompleteMe is falling short to cope with it.

So, while it helps me while using Vim, I use it because it's what I have that does the job, not because I find it particularly good.

cpp, clang, libclang, completion, intellisense, vim, ycm, windows, linux
Spotted a mistake in this article? Why not suggest an edit!