Day 16. Visual Studio Compiler Switches

Day 16. Visual Studio Compiler Switches
Video Length (including Q&A): 1h41

Welcome to “Handmade Hero Notes”, the book where we follow the footsteps of Handmade Hero in making the complete game from scratch, with no external libraries. If you'd like to follow along, preorder the game on handmadehero.org, and you will receive access to the GitHub repository, containing complete source code (tagged day-by-day) as well as a variety of other useful resources.

Our Win32 interface is mostly complete and we're close to working at the game part of the game. In the next series of episodes we'll be focusing on what is essentially the cleanup of the loose ends that we've left hanging.

Today, however, we'll talk about something completely different. We'll discuss our build system that we so quickly defined during day 1. We will be improving it by passing additional switches to the compiler.

Day 15 Day 17

(Top)
Set the Stage
  1.1  Break Down Existing Flags
Compiler Warnings
  2.1  Check Existing Warnings
    2.1.1  C4201 Nameless struct/union
    2.1.2  C4100 Unreferenced Formal Parameter
    2.1.3  C4244 Possible Loss of Data
    2.1.4  C4018 Signed/Unsigned Mismatch
    2.1.5  C4189 Local Variable not Referenced
Other Compiler Switches
  3.1  Revisit -Zi
  3.2  Statically Link CRT Library
  3.3  Map File
  3.4  Optimization Level
  3.5  Disable C++ Features
  3.6  Other Switches
  3.7  Linker Options
Compile in 32-bit
Recap
Programming Notions
  6.1  Name Mangling
Navigation

   

Set the Stage

If you recall, our build system is extremely simple: we only ever rely on two batch scripts. We set up our environment variables via shell.bat and then build using build.bat. Both of these scripts are little more than one-liners, but there's a lot of things that happen in those lines.

What exactly happens at step 3? We call the cl command, nice and simple. cl.exe is a program somewhere in the Visual Studio folders which we know about thanks to vcvarsall script we ran at the shell startup. This program doesn't run on its own: at the very least it needs to know which is the file to compile. In our case, it's ..\code\win32_handmade.cpp. Remember that we are running cl from build directory (see step 2), that's why we need to go up one level (..), then down to code.

On Windows and POSIX systems (Linux, MacOS, etc.), each directory contains a “virtual” subdirectories . (which refers to itself) and .. (which refers to the level above).

We also pass a few flags/switches/options (call them as you like): when receiving them, cl.exe will do some actions associated with that option. Right now we're passing the following flags. We've touched all of them in the past, so this should serve as a refresher.

It's important to note that any flags we pass don't necessarily enable something. By default, cl.exe has some features enabled and some disabled. And the list of the options is long! It's up to us to set the values as we want them, and ignore those that we don't care.

   

Break Down Existing Flags

You could re-organize your build file by adding a bit more clarity.

In the .bat files, you can comment out the whole line by prefixing it with REM [comment] or :: [comment]. You can even comment on the same line if you add & symbol before that (&REM or &::, respectively). Together with the batch script variables, we can use this knowledge to make a reference line for each flag. Let's add a debug variable and put the flags we already have:

@echo off

if not exist build (mkdir build)
pushd build
:: DEBUG VARIABLES set debug= -FC &:: Produce the full path of the source code file set debug=%debug% -Zi &:: Produce debug information
cl -DHANDMADE_WIN32=1 -DHANDMADE_SLOW=1 -DHANDMADE_INTERNAL=1 %debug% ..\code\win32_handmade.cpp user32.lib gdi32.lib
popd
 Listing 1: [build.bat] Breaking down each compiler flag.

Note that we need to re-include the previously defined value to add new values to it (except for the first time).

We can also pull out all but win32-specific compile-time defines. We will omit in-line comments as it should be self-explanatory.

:: DEBUG VARIABLES
set debug=        -FC &:: Produce the full path of the source code file
set debug=%debug% -Zi &:: Produce debug information
:: CROSS_PLATFORM DEFINES set defines= -DHANDMADE_INTERNAL=1 set defines=%defines% -DHANDMADE_SLOW=1
cl -DHANDMADE_WIN32=1 %defines% %debug% ..\code\win32_handmade.cpp user32.lib gdi32.lib
 Listing 2: [build.bat] Pulling out cross-platform defines.

Last, we can also pull out `..\code` and the import libraries we use on our win32 platform.

set code_path=..\code\
:: DEBUG VARIABLES :: ...
:: WIN32 PLATFORM LIBRARIES set win32_libs= user32.lib set win32_libs=%win32_libs% gdi32.lib
:: CROSS_PLATFORM DEFINES :: ...
cl -DHANDMADE_WIN32=1 %defines% %debug% %code_path%win32_handmade.cpp %win32_libs%
 Listing 3: [build.bat] We now have a lot of %s in our cl line.

To recap, this is what happens here. We're also arranging all the comments in a table so that you can see each component at a glance.

Full-linecommentsignInlinecommentsignComments::DEBUGVARIABLESsetdebug=-FC&::Producethefullpathofthesourcecodefilesetdebug=%debug%-Zi&::Producedebuginformationactualcompilerflagcontentsofthevariable(onlyuseifitwasset)variablenamecommandtosetavariablecontentsofthevariablecl%debug%../code/win32_handmade.cppcommandtostartcompilation

 Figure 1: Structure of build.bat variables.

Keep in mind that this step is entirely optional! We will use this structure for educational purposes, as it should make it easier for the reader to follow which flags we edit, and what they represent. If you feel this view is more confusing than simply writing all the values in one line, you can write more values in each line or even inline it all as it was before.

   

Compiler Warnings

By now you have seen plenty of compiler Errors. They give you anxiety, sometimes outright drive you in panic, but ultimately they are extremely useful as they make your code work. You've also seen some Notes, they usually accompany compiler errors trying to explain the error a bit further. Today, we're going to introduce compiler Warnings.

A warning is something that the compiler doesn't need you to fix. Usually it will implicitly do the necessary fix itself. However, warnings often are extremely helpful as they indicate that something might be wrong.

Warnings are usually arranged in levels, and you can enable them in waves. The idea is that you enable a warning level you're comfortable with. After that, compiler allows you to disable warnings you don't want so they don't bother you.

Right now we don't have any warnings turned on, only the barest minimum. Let's enable warnings in our compile to a higher level of warnings. Add a -Wall flag to your compiler options and rebuild.

set code_path=..\code\
:: GENERAL COMPILER FLAGS set compiler=-Wall &:: Display All Warnings
:: DEBUG VARIABLES :: ... :: WIN32 PLATFORM LIBRARIES :: ... :: CROSS_PLATFORM DEFINES :: ...
cl %compiler% -DHANDMADE_WIN32=1 %defines% %debug% %code_path%win32_handmade.cpp %win32_libs%
 Listing 4: [build.bat] -Wall totally doesn't stand for “Wall of Text”.

You will see... a lot of warnings. These contain:

Yep, Windows header files are littered with warnings. However, those are among the most pedantic errors you'll ever see. Most of these will go away once you enable a lower warning level. Let's try -W4.

:: GENERAL COMPILER FLAGS
set compiler=-W4 &:: Display warnings up to level 4
:: DEBUG VARIABLES :: ... :: WIN32 PLATFORM LIBRARIES :: ... :: CROSS_PLATFORM DEFINES :: ... cl %compiler% -DHANDMADE_WIN32=1 %defines% %debug% %code_path%win32_handmade.cpp %win32_libs%
 Listing 5: [build.bat] Setting a more reasonable warning level.

That's much better. All the Windows warnings should go away, so you'll be left with the warnings in your own code. Now we can go through each warning that we find. What we consider annoying, not interesting, busywork can be turned off. The rest will be left on and fixed.

Let's be clear: we don't want any warnings in our code. Warnings are a useful tool as it can save us from headaches down the line so they should be addressed immediately just like we fix all the compiler errors. However, a file with warnings compiles just fine, so you'd be compelled to return “to them later”... Luckily, the compiler has a flag -WX which resolves this temptation. It treats all warnings as errors, so if we “catch” a warning, we simply won't compile.

:: GENERAL COMPILER FLAGS
set compiler=               -W4     &:: Display warnings up to level 4
set compiler=%compiler% -WX &:: Treat all warnings as errors
 Listing 6: [build.bat].

Warnings catch errors (bugs in the code). When they are there, you'll be happy of it. It's always a good idea to keep the warning level relatively high.

   

Check Existing Warnings

Let's see what is the list of warnings we get after 15 days of typing.

   

C4201 Nameless struct/union

We used nameless structs and unions a couple of times already, and we'll use more of them. The reason this warning appears is because nameless structs or unions are not standard to C++, using them is possible due to compiler extension in the Microsoft Visual C++ toolkit.

That's totally fine by us: not only most of the modern compilers have this feature de facto, it's highly useful as we've already seen. So we'll disable this warning by adding -wd4201 warning.

:: GENERAL COMPILER FLAGS
set compiler=               -W4     &:: Display warnings up to level 4
set compiler=%compiler%     -WX     &:: Treat all warnings as errors
:: IGNORE WARNINGS set compiler=%compiler% -wd4201 &:: Nameless struct/union
 Listing 7: [build.bat] We use the same %compiler% variable but feel free to introduce your %warning% variable, or include warning-related items to %debug%. It's completely up to you!
   

C4100 Unreferenced Formal Parameter

We passed a parameter to a function and didn't use it. We absolutely don't care about this warning, such behaviour can happen all the time for all sorts of reasons: stub functions, partially using an API, following some convention, you name it. We simply disable this warning. -wd4100.

:: GENERAL COMPILER FLAGS
set compiler=               -W4     &:: Display warnings up to level 4
set compiler=%compiler%     -WX     &:: Treat all warnings as errors
:: IGNORE WARNINGS
set compiler=%compiler%     -wd4201 &:: Nameless struct/union
set compiler=%compiler% -wd4100 &:: Unused function parameter
 Listing 8: [build.bat] You'll notice that disabling this warning will thin out the list of our warnings extensively.
   

C4244 Possible Loss of Data

Now this is a totally valid warning. Usually it pops up when you inadvertently convert a float to an int, from a u32 to u16, and so on. We then should choose whether to treat the conversion gracefully (like we did with SafeTruncateUInt64) or to explicitly cast. Usually we opted for the latter, but we missed a couple spots. Let's fix them!

internal void
Win32ResizeDIBSection(win32_offscreen_buffer *Buffer, int Width, int Height)
{
    // ... 
WORD BytesPerPixel = 4;
// ... } // ... internal LRESULT CALLBACK Win32MainWindowCallback(HWND Window, UINT Message, WPARAM WParam, LPARAM LParam) { LRESULT Result; switch (Message) { // ... case WM_SYSKEYDOWN: case WM_SYSKEYUP: case WM_KEYDOWN: case WM_KEYUP: { // ...
u32 VKCode = (u32)WParam;
// ... } // ... } return (Result); }
 Listing 9: [win32_handmade.cpp] In Win32ResizeDIBSection, we set BytesPerPixel to be WORD. Inside Win32MainWindowCallback we do the cast explicitly.
   

C4018 Signed/Unsigned Mismatch

This is another completely valid error. Incorrect conversion or comparison might lead to data loss! Luckily, it only happened once so far, and it's definitely not a case of potential integer overflow. Still, let's fix the issue.

DWORD MaxControllerCount = XUSER_MAX_COUNT;
if(MaxControllerCount > ArrayCount(NewInput->Controllers)) { MaxControllerCount = ArrayCount(NewInput->Controllers); } for (DWORD ControllerIndex = 0; ControllerIndex < MaxControllerCount; ++ControllerIndex) { // ... }
 Listing 10: [win32_handmade.cpp > WinMain] MaxControllerCount was an int (which is signed) where it should have been a DWORD (which is not).
   

C4189 Local Variable not Referenced

This warning is quite useless, both for development and release. During development, you often sketch out some variables for some future use, while for the release the compiler optimizes out (removes) all unused variables anyway. It's safe to disable, so let's do that.

:: GENERAL COMPILER FLAGS
set compiler=               -W4     &:: Display warnings up to level 4
set compiler=%compiler%     -WX     &:: Treat all warnings as errors
:: IGNORE WARNINGS
set compiler=%compiler%     -wd4201 &:: Nameless struct/union
set compiler=%compiler%     -wd4100 &:: Unused function parameter
set compiler=%compiler% -wd4189 &:: Local variable not referenced
 Listing 11: [build.bat] You'll notice that disabling this warning will thin out the list of our warnings extensively.
   

Other Compiler Switches

The MSVC compiler options list is extensive. We should definitely make use of it while we're at it. It's not the last time we revisit build.bat; we'll return and add more flags, but for now let's add the list below.

   

Revisit -Zi

-Zi produces debug information inside .pdb files. This information can get quite big: our executable produces .pdb file almost 10 times as big!

However, this specific flag can be problematic though. It produces a vcXXX.pdb which is shared and can get you into problems. Let's change -Zi to an “older” -Z7. The difference is quite minimal but it allows you not to think of the potential issues any more. For instance, there won't be vcXXX.pdb any longer.

:: DEBUG VARIABLES
set debug=                  -FC     &:: Produce the full path of the source code file
set debug=%debug% -Z7 &:: Produce debug information
 Listing 12: [build.bat] Replacing -Zi with -Z7.
   

Statically Link CRT Library

If you remember, C Runtime Library (or CRT Library) is the standard set of functions implemented by the compiler (in this case, Microsoft) as a part of compiler package. The way it's built by default may vary: it can be linked either statically or dynamically. Static linking means that the functions are physically copied inside your executable. Dynamic, on the other hand, means that the only thing present in your executable are the function stubs, and the actual location of the functions is resolved at runtime (think about when we were looking for xinput.dll and extracting relevant functions).

Normally dynamic linking would be fine for our purposes. You offload some of the functions to the operating system, and your distribution is overall smaller. However, there's a catch. CRT has versions, many many versions in fact, and you can't really guarantee that the user has a version of the C Runtime Library that you are linking against. This forces you to include the “Microsoft Visual C++ XXXX Redistributable” with your game and run it when the user installs it.

It's definitely a bad idea but somehow many developers still do it... Not us, we will use -MT flag. -MT says “use the static library, pack everything in and don't look for DLLs at runtime”. That's something you absolutely want to be set. It's default on the command line but still good to specify.

:: GENERAL COMPILER FLAGS
set compiler= -MT &:: Include CRT library in the executable (static link)
set compiler=%compiler% -W4 &:: Display warnings up to level 4
set compiler=%compiler% -WX &:: Treat all warnings as errors
 Listing 13: [build.bat] CRT library handling.

In general, this is the reason why you want to test on as many machines as possible: to see the compatibility of your game on different setups. That said, there's software like the Dependency Walker (a free tool which we recommend you to check out) which allows to see what the executable relies on.

You can remove your dependency on the CRT Library completely, but you'll need to perform some specific actions to do so though. This guide should get you started.

   

Map File

-Fm flag allows to specify a location to the map file.

The map file is a text file containing a list of all the functions in your program in the order in which they appear in the corresponding .exe or .dll file. A map, if you will. We can add this flag directly on the cl line:

cl %compiler% -DHANDMADE_WIN32=1 %defines% %debug% -Fmwin32_handmade.map %code_path%win32_handmade.cpp %win32_libs%
 Listing 14: [build.bat] Adding a map file flag.

Why would we need such a thing? If you open the file you'll note that there's a lot of functions in the executable, and we can see what module is responsible for outputting which function. This will become more useful once we're ready to tighten up the screws on our program even further.

If you inspect the file, you'll notice there's a lot of name mangling going on (more about it in subsection 6.1). You can also see the import libraries marked with __imp__ prefix.

   

Optimization Level

-Od Disables all optimizations. As a result, it produces quite a slow code, and it's not something we want to ship. However, this flag is quite useful if you want to dive into disassembly or even simply step through your code.

We will want to change optimization level in the future, to test how faster a specific chunk of code performs and of course for the final release. In that situation, you will want to use -O2 flag. For this reason, let's add it directly into our cl line:

:: No optimizations (slow): -Od; all optimizations (fast): -O2
cl -Od %compiler% -DHANDMADE_WIN32=1 %defines% %debug% -Fmwin32_handmade.map %code_path%win32_handmade.cpp %win32_libs%
 Listing 15: [build.bat] Adding -Od flag.
   

Disable C++ Features

These are the switches which prevent the compiler from doing some other things (mainly C++ related).

Exception Handling

Exception handling works in the following way: “try” something, if someone “throws” an exception (things don't behave as expected), “catch” it.

We will never do C++ exception handling. Eventually, however, we might take a look at Windows' own exception handling system, SEH.

:: GENERAL COMPILER FLAGS
set compiler=               -MT     &:: Include CRT library in the executable (static link)
set compiler=%compiler% -Gm- &:: Disable minimal rebuild set compiler=%compiler% -GR- &:: Disable runtime type info (C++) set compiler=%compiler% -EHa- &:: Disable exception handling (C++)
set compiler=%compiler% -W4 &:: Display warnings up to level 4 set compiler=%compiler% -WX &:: Treat all warnings as errors
 Listing 16: [build.bat] Disabling C++ features.
   

Other Switches

Let's see a few other Switches that we always want:

:: GENERAL COMPILER FLAGS
set compiler= -nologo &:: Suppress Startup Banner set compiler=%compiler% -Oi &:: Use assembly intrinsics where possible
set compiler=%compiler% -MT &:: Include CRT library in the executable (static link)
set compiler=%compiler% -Gm- &:: Disable minimal rebuild set compiler=%compiler% -GR- &:: Disable runtime type info (C++) set compiler=%compiler% -EHa- &:: Disable exception handling (C++) set compiler=%compiler% -W4 &:: Display warnings up to level 4 set compiler=%compiler% -WX &:: Treat all warnings as errors
 Listing 17: [build.bat] Adding even more switches.
   

Linker Options

If you remember, the full compilation is set in stages:

Check out also Figure 1 of day 11.

Each step is performed by a different program. In particular, linking is done by link.exe that is called by cl.exe once the latter has done its job. We could pass -c flag which disables calling linker and call it ourselves but it's a bit of an overkill. Instead, cl.exe allows us to communicate with the linker by defining /link flag at the end of the compiler switches, and passing it the linker-specific switches we define.

Let's do just that. We can right away define a couple of switches:

:: WIN32 LINKER SWITCHES set win32_link= -subsystem:windows,5.2 &:: subsystem, 5.1 for x86 set win32_link=%win32_link% -opt:ref &:: Remove unused functions
:: WIN32 PLATFORM LIBRARIES :: ... :: No optimizations (slow): -Od; all optimizations (fast): -O2
cl -Od %compiler% -DHANDMADE_WIN32=1 %defines% %debug% -Fmwin32_handmade.map %code_path%win32_handmade.cpp %win32_libs% /link %win32_link%
 Listing 18: [build.bat] Passing linker options.
   

Compile in 32-bit

Let's have some fun! We'll try to compile the game in 32-bit mode. Before doing that, we only need to make a few changes to our win32_handmade.cpp:

The latter can be easily achieved by casting TotalStorageSize to u32, but this means that the code we'll need to change the cast again once we return to 64-bit mode. Instead, we can use a type called size_t. It's defined to an unsigned 32-bit or 64-bit integer, depending on whether you compile on a 32-bit or 64-bit platform.

game_memory GameMemory = {};
GameMemory.PermanentStorageSize = Megabytes(64);
GameMemory.TransientStorageSize = Gigabytes(1);
u64 TotalStorageSize = GameMemory.PermanentStorageSize + GameMemory.TransientStorageSize;
GameMemory.PermanentStorage = VirtualAlloc(BaseAddress, (size_t)TotalStorageSize, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE);
 Listing 19: [win32_handmade.cpp > WinMain] Reducing memory footprint to support 32-bit environment.

Now, before you can compile to the 32-bit architecture, you need to reboot your environment in the 32-bit mode. Modify your shell.bat as follows:

call "[path to vcvarsall.bat]\vcvarsall.bat" x86
 Listing 20: [shell.bat] Switching architecture from x64 to x86.

.... and reboot your command prompt. If you're compiling through the editor, you'll need to restart it as well. Try compiling now! If everything goes well, your executable will change to a 32-bit one.

Don't forget to change shell.bat back since we'll continue our development in 64-bit mode.

   

Recap

Hopefully you've enjoyed this brief interstitial and are ready to move on! Next, we'll be tightening screws on our input system.

   

Programming Notions

   

Name Mangling

Some functions look absolutely crazy in map files. This is due to application of a technique known as name mangling or name decoration.

In C, the function names are mostly are exactly as you define them in code. On the other hand, C++ is considered among the most widespread users of name mangling. This is in large part due to function overloading (we talked about it on day 13).

Because C++ was initially built on top of C, and the functions that had the same names couldn't be duplicated in C, the compiler would silently add the function parameters. Thus, a function Foo which takes an int and an int as parameters, would get the name FOO@@INT@@INT to be differentiated from the other similar. This mangling can go completely bonkers, depending on compiler, thing type, etc.

You can read more on Wikipedia.

   

Navigation

Previous: Day 15. Platform-Independent Debug File I/O

Up Next: Day 17.

Back to Index

Glossary

Tools and Articles

Dependency Walker

How to avoid C++ Runtime on Windows

name mangling

Subsystem Versions of Windows

MSVC

Structured Exception Handling

MSVC Compiler Options

-EHa- Disable Exception Handling

-Fm Name Map file

-Gm- Disable Minimal Rebuild

-GR- Disable C++ Runtime Type Information ? Use Run-Time Library

-nologo Suppress startup banner

-Od Disable all optimizations

-Oi Generate Intrinsic Functions

-W3, -W4, -Wall, -wd, -WX Warning-related switches

-Zi, -Z7 Debug Info Format

MSVC Compiler Errors/Warnings

C4018: Signed/Unsigned Mismatch

C4100: Unreferenced formal parameter

C4100: Local variable not referenced

C4201: Nameless struct/union

C4201: conversion from 'type1' to 'type2', possible loss of data

MSVC Linker Options

-subsystem

-opt

formatted by Markdeep 1.10