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.
(Top)
1 Set the Stage
1.1 Break Down Existing Flags
2 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
3 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
4 Compile in 32-bit
5 Recap
6 Programming Notions
6.1 Name Mangling
7 Navigation
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.
shell.bat
, aside from adding the misc
folder to the system path, calls vcvarsall
, a lot more complex batch script provided by Visual Studio build tools. This script is responsible for adding to the shell a bunch of paths and variables that the compiler will be using at later stages. We only need to call this script once, when the command prompt is opened.
vcvarsall
: target architecture, i.e. x64
.
build.bat
does a few things:
build
directory (if it doesn't already exist)
build
directory (pushd
)
popd
)
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
.
.
(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.
-DHANDMADE_WIN32=1
: compile-time define, identical to #define HANDMADE_WIN32 1
inside the code.
-DHANDMADE_SLOW=1
: compile-time define, identical to #define HANDMADE_SLOW 1
inside the code.
-DHANDMADE_INTERNAL=1
: compile-time define, identical to #define HANDMADE_INTERNAL 1
inside the code.
-FC
: Produce the full path of the source code file when printing errors, notes, and other diagnostic messages.
-Zi
: Produce debug information (allows us to step into the code).
user32.lib
and gdi32.lib
).
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.
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 informationcl -DHANDMADE_WIN32=1 -DHANDMADE_SLOW=1 -DHANDMADE_INTERNAL=1 %debug% ..\code\win32_handmade.cpp user32.lib gdi32.lib
popd
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
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%
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.
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.
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%
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 FLAGSset 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%
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 4set compiler=%compiler% -WX &:: Treat all warnings as errors
Let's see what is the list of warnings we get after 15 days of typing.
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
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/unionset compiler=%compiler% -wd4100 &:: Unused function parameter
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);
}
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)
{
// ...
}
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 parameterset compiler=%compiler% -wd4189 &:: Local variable not referenced
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.
-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 fileset debug=%debug% -Z7 &:: Produce debug information
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 FLAGSset compiler= -MT &:: Include CRT library in the executable (static link)set compiler=%compiler% -W4 &:: Display warnings up to level 4set compiler=%compiler% -WX &:: Treat all warnings as errors
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.
-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%
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.
-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): -O2cl -Od %compiler% -DHANDMADE_WIN32=1 %defines% %debug% -Fmwin32_handmade.map %code_path%win32_handmade.cpp %win32_libs%
These are the switches which prevent the compiler from doing some other things (mainly C++ related).
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
Let's see a few other Switches that we always want:
RDTSC
). Even if all optimizations are disabled.:: GENERAL COMPILER FLAGSset compiler= -nologo &:: Suppress Startup Banner
set compiler=%compiler% -Oi &:: Use assembly intrinsics where possibleset 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
If you remember, the full compilation is set in stages:
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:
windows
or console
, for a window-based or console-based options. In reality however, if you want maximum compatibility you need to pass subsystem windows,5.2
for an x64 system (and windows,5.1
for x86). This will ensure you'll be able to run all the way until Windows Server 2003 (5.2) or Windows XP (5.1). The default is 6.0, i.e. Windows Vista (you can find the full list of subsystems here).
-opt:ref
to eliminate functions and data that are never referenced. This reduces size of the executable (but there's still a lot of stuff used by the CRT).:: 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%
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
:
VirtualAlloc
.
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);
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
.... 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.
Hopefully you've enjoyed this brief interstitial and are ready to move on! Next, we'll be tightening screws on our input system.
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.
Previous: Day 15. Platform-Independent Debug File I/O
Up Next: Day 17.
How to avoid C++ Runtime on Windows
-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
C4018: Signed/Unsigned Mismatch
C4100: Unreferenced formal parameter
C4100: Local variable not referenced
C4201: conversion from 'type1' to 'type2', possible loss of data