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.
We hope you've been enjoying the course so far; even if you mainly code in Python, JavaScript, or C#, it's never a bad idea to learn low-level programming to better understand what's going higher up.
Yesterday we laid down the foundation for live code editing. However, there are many limitations to our current implementation: first, the code reloading isn't instantaneous; currently, the game only tries to reload the code every 120 frames. Second, you can't debug and use this feature; the debugger won't let you recompile, plus it can have trouble finding handmade.dll.
(Top)
1 Variable PDB Name
1.1 Add Ability to Edit DLL PDB Name
1.2 Randomize DLL PDB Name
2 Re-enable Debugger Support
2.1 Retrieve Module Filename
2.2 Determine Module Path Without Filename
2.3 Get Desired Filenames
2.4 Use New Strings
3 Remove Delay Between Reloads
3.0.1 Get Last Write Time
3.1 Store the Last Write Time
3.2 Remove Latency
4 Recap
5 Programming Notions
5.1 Metaprogramming
6 Navigation
When we left off, we couldn't debug our game correctly. This was for a couple of reasons that really we should address first. Debugging is an essential part of programming, so restoring that functionality is paramount.
First of all, whenever you recompile a program (be it executable or a DLL), the compiler generates multiple files:
.exe
or .dll
: of course, the final product of the compilation and linking.
.map
: a text file containing all the symbols of your program. This is optional, and we enabled these files using the -Fm
parameter in the build line.
.obj
is the machine code of your program immediately after compilation. It's passed to the linker, which then proceeds to link this code with other libraries and external symbols as necessary.
.pdb
(Program DataBase or symbol) files are generated to allow the debugger to properly interact with the program. It will enable things like breakpoints in code or visualizing on which line of your code you're currently paused. Otherwise, you'd need to read the assembly code!
When compiling, all of the potential files should be available for writing. We already tried to circumvent this issue by copying handmade.dll instead of loading it directly. However, the .pdb
file often remains locked by the debugger until the end of the debugging session.
That said, we don't have to always name the .pdb
file with the same name. What if, instead of handmade.pdb
, would it have a completely random name? This way, the debugger can keep all the symbol files it wants open, and we can rewrite the .pdb
as many times as we like.
If your debugger doesn't have this issue, feel free to skip this section entirely.
As usual, there seem to be multiple solutions to this problem. First of all, a quick search on MSDN shows us that there're two flags related to the .pdb
file names:
.pdb
. Seems what we're looking for, yet on a closer inspection, you realize that it's not renaming the correct .pdb
. We don't even have that, thanks to the -Z7
option we pass!
This allows us to change our cl
line for handmade.cpp
(not to be confused with win32_handmade.cpp
) in the following manner:
cl -Od %compiler% %defines% %debug% -Fmhandmade.map %code_path%handmade.cpp -LD /link /pdb:handmade.pdb %link% %dll_link%cl -Od %compiler% -DHANDMADE_WIN32=1 %defines% %debug% -Fmwin32_handmade.map %code_path%win32_handmade.cpp %win32_libs% /link %link% -subsystem:windows,5.2
Now we can control the naming of the PDB files. But how can we add an element of randomness to it? Shell language to the rescue!
The Command Prompt has a surprising amount of functionality, legacy of its DOS past. You already know things like %variable_name%
which is effectively a variable inside the shell, but it also has things like current time or even more obscure functionality. In fact, this lengthy string:
%date:~-4,4%%date:~-10,2%%date:~-7,2%_%time:~0,2%%time:~3,2%%time:~6,2%
will print you the current date in time in YYYYMMdd_HHmmss
format. You can use this one if you like; we'll limit ourselves to the simple %random%
:
cl -Od %compiler% %defines% %debug% -Fmhandmade.map %code_path%handmade.cpp -LD /link /pdb:handmade%random%.pdb %link% %dll_link%cl -Od %compiler% -DHANDMADE_WIN32=1 %defines% %debug% -Fmwin32_handmade.map %code_path%win32_handmade.cpp %win32_libs% /link %link% -subsystem:windows,5.2
If you now build your program several times, you'll notice that you get several .pdb
files with the names like handmade1297.pdb
. That's what we want: a unique name for each pdb.
There's just one more issue. These files will keep piling up. You can delete them once in a while yourself, but why do something manually when you can automate it? Let's simply clean up all the .pdb
files each time we're about to recompile:
del *.pdb
cl -Od %compiler% %defines% %debug% -Fmhandmade.map %code_path%handmade.cpp -LD /link /pdb:handmade%random%.pdb %link% %dll_link%
cl -Od %compiler% -DHANDMADE_WIN32=1 %defines% %debug% -Fmwin32_handmade.map %code_path%win32_handmade.cpp %win32_libs% /link %link% -subsystem:windows,5.2
This solution comes with a new issue: open .pdb
files won't be deletable, so the del
function will complain about it clogging your console. It's not that big of a deal, but we can remedy this as well! Simply pipe out the output of the del
command.
What does that “pipe” even mean?
Console commands can have multiple output streams: standard output
and standard error
are the most common, and they can be redirected (or “piped”) separately. “Pipe” name literally comes from the symbol |
(read as “pipe”), which is used in format commandA | commandB
, meaning “Send the output from commandA
as input to commandB
". You can find more information in the cmd redirection syntax article.
In any case, we won't get too deep into all of this. Shell scripting is a completely separate topic in and of itself that you can study separately. Suffice to say that we want to suppress all the messages of del
command, so we pipe them both to the magic NUL
file. This looks like this:
del *.pdb > NUL 2> NUL
cl -Od %compiler% %defines% %debug% -Fmhandmade.map %code_path%handmade.cpp -LD /link /pdb:handmade%random%.pdb %link% %dll_link%
cl -Od %compiler% -DHANDMADE_WIN32=1 %defines% %debug% -Fmwin32_handmade.map %code_path%win32_handmade.cpp %win32_libs% /link %link% -subsystem:windows,5.2
And there you have it, you will hear no more from the del
command.
With the pdb issue out of the way, we still cannot use our debugger. Frustrating! Why is that?
Early in the course, very early, we asked the debugger to set the working directory of our game to be handmade\data
. This means that our game effectively starts in handmade\data
instead of handmade\build
where our dll and executable are.
LoadLibraryA
that we were using for loading files up until now doesn't see it as an issue. This function has an implicit search path. Unfortunately, that's not the case for some other file I/O functions, such as FindFirstFileA
and even CopyFile
. This function will go and search from the working directory instead.
We can remedy this by rebuilding the correct path from the executable path. For this, there's a handy function GetModuleFileNameA which does just that.
DWORD GetModuleFileNameA(
HMODULE hModule,
LPSTR lpFilename,
DWORD nSize
);
hModule
: optional handle to the module. From the first days of this course, you might remember that a module handle is non-other than HINSTANCE Instance
, passed to us in WinMain
. It is optional, though, so we simply give 0.
lpFilename
: text buffer to contain the path.
nSize
: the size of the text buffer.
We don't know where the executable might the live in file system, so let's reserve a buffer with a magic MAX_PATH
value. It's a Windows #define
which specifies what's a maximum possible filename length can be.
MAX_PATH
is not the safest thing in the world and could produce buffer overruns. Do not use it in production code!Additionally, this function returns the size of the executable. Let's keep it as it might become useful as well.
int WinMain(...)
{ // NOTE(casey): Never use MAX_PATH in code that is user-facing, because it
// can be dangerous and lead to bad results.
char EXEFilename[MAX_PATH];
DWORD SizeOfFilename = GetModuleFileNameA(0, EXEFilename, sizeof(EXEFilename)); //...
}
Now, what do we want? Ask yourself this question often. The issue we're facing is that GetModuleFileNameA
returns a complete path, including the filename itself. That's not what we want! If our module filename is w:\handmade\build\win32_handmade.exe
, we want to know only the w:\handmade\build\
part! And, to be even more precise, we want to get the position of the last slash: this way, we can add to the path handmade.dll
to become w:\handmade\build\handmade.dll
.
String manipulation is complex. It might be a more straightforward task in a higher-level language, but many things happen under the hood.
So what we need to do is to scan the string that we get and see whether or not we find the last slash. Then we move one step further and say that this would be our “cursor” position: one past the last slash. We repeat this until we run until the end of the string (determined by the null terminator: 0
) and use the last value we found.
// NOTE(casey): Never use MAX_PATH in code that is user-facing, because it
// can be dangerous and lead to bad results.
char EXEFilename[MAX_PATH];
DWORD SizeOfFilename = GetModuleFileNameA(0, EXEFilename, sizeof(EXEFilename));
char *OnePastLastSlash = EXEFilename;
for (char *Scan = EXEFilename;
*Scan;
++Scan)
{
if (*Scan == '\\')
{
OnePastLastSlash = Scan + 1;
}
}
*Scan
condition can also be written as *Scan != 0
: While the value of Scan
is not 0, continue scanning.
Let's suppose that we have a magic function, CatStrings
, which allows us to conCATenate two strings together in a third string.
We need to figure out how much text we want to copy from String A. OnePastLastSlash
gives us a pointer, a numerical address. We know already that we can manipulate that to our advantage. OnePastLastSlash
is precisely the desired length amount of bytes longer than the pointer to EXEFilename
. This allows us simply to subtract one from the other to get the desired size.
All this said, and assuming CatStrings
will take the parameters in order (StringA Character Count
, StringA
, StringB Character Count
, StringB
, Result Character Count
, Result
), we can draft the following code:
char *OnePastLastSlash = EXEFilename;
for (char *Scan = EXEFilename;
*Scan;
++Scan)
{
if (*Scan == '\\')
{
OnePastLastSlash = Scan + 1;
}
}
char SourceGameCodeDLLFilename[] = "handmade.dll";
char SourceGameCodeDLLFullPath[MAX_PATH];
CatStrings(OnePastLastSlash - EXEFilename, EXEFilename,
sizeof(SourceGameCodeDLLFilename), SourceGameCodeDLLFilename,
sizeof(SourceGameCodeDLLFullPath), SourceGameCodeDLLFullPath);
We also need to make sure that we don't add the null terminator (0 put automatically at the end of each C string) of the handmade.dll
, so we need to subtract 1 from its size. CatStrings
will add its own null terminator. StringA
is already incomplete so it won't have a null terminator.
char SourceGameCodeDLLFilename[] = "handmade.dll";
char SourceGameCodeDLLFullPath[MAX_PATH];
CatStrings(OnePastLastSlash - EXEFilename, EXEFilename,
sizeof(SourceGameCodeDLLFilename) - 1, SourceGameCodeDLLFilename,
sizeof(SourceGameCodeDLLFullPath), SourceGameCodeDLLFullPath);
Since we copy the game DLL file to handmade_temp.dll
, we can repeat the same operation for it as well:
char SourceGameCodeDLLFilename[] = "handmade.dll";
char SourceGameCodeDLLFullPath[MAX_PATH];
CatStrings(OnePastLastSlash - EXEFilename, EXEFilename,
sizeof(SourceGameCodeDLLFilename) - 1, SourceGameCodeDLLFilename,
sizeof(SourceGameCodeDLLFullPath), SourceGameCodeDLLFullPath);
char TempGameCodeDLLFilename[] = "handmade_temp.dll";
char TempGameCodeDLLFullPath[MAX_PATH];
CatStrings(OnePastLastSlash - EXEFilename, EXEFilename,
sizeof(TempGameCodeDLLFilename) - 1, TempGameCodeDLLFilename,
sizeof(TempGameCodeDLLFullPath), TempGameCodeDLLFullPath);
Speaking of CatStrings
, what we'll make is probably the worst possible implementation of a string concatenation routine. However, it's fast to write and easy to wrap your head around to do for our purposes. Besides, this is all developer-only code: if you needed to write a string processing routine for the game, we'd definitely spend more effort on this.
We have Source A, Source B, Destination, and the respective character counts. We'll simply iterate over source A until we reach its count copying its character over, then do the same thing with source B. Finally, we'll add the null terminator.
If you notice, we won't be doing anything with the character count of the destination buffer. Ideally, we'd need to validate that we can fit both strings inside it, but we will just trust this thing works for now. Hey, we'll even drop a TODO for it!
internal void
CatStrings(size_t SourceACount, char *SourceA,
size_t SourceBCount, char *SourceB,
size_t DestCount, char *Dest)
{
// TODO(casey): Dest bounds checking!
for (int Index = 0;
Index < SourceACount;
++Index)
{
*Dest++ = *SourceA++;
}
for (int Index = 0;
Index < SourceBCount;
++Index)
{
*Dest++ = *SourceB++;
}
*Dest++ = 0;
}
Hurray, we have the working strings containing the full path of our DLLs! Let's fix our code to make use of them.
First, we need to pass both the SourceDLLName
and the TempDLLName
to Win32LoadGameCode
instead of defining them directly in the code:
internal win32_game_codeWin32LoadGameCode(char *SourceDLLName, char *TempDLLName){
win32_game_code Result = {};
CopyFile(SourceDLLName, TempDLLName, FALSE);
Result.GameCodeDLL = LoadLibraryA(TempDLLName); Result.DLLLastWriteTime = Win32GetLastWriteTime(SourceDLLName);
// ...
}
Then, we need to adapt the usage of it inside WinMain
, both inside the GlobalRunning
loop and outside of it:
win32_game_code Game = Win32LoadGameCode(SourceGameCodeDLLFullPath,
TempGameCodeDLLFullPath);u32 LoadCounter = 0;
u64 LastCycleCount = __rdtsc();
while (GlobalRunning)
{
FILETIME NewDLLWriteTime = Win32GetLastWriteTime(SourceGameCodeDLLFullPath);
if (LoadCounter++ > 120)
{
Win32UnloadGameCode(&Game); Game = Win32LoadGameCode(SourceGameCodeDLLFullPath,
TempGameCodeDLLFullPath); LoadCounter = 0;
}
// ...
}
If you followed along so far, you should be good and compilable. You can finally boot up your debugger, run the game and edit its various features almost real-time. Try it now by changing the value of Pixel
inside handmade.cpp > RenderWeirdGradient
and recompiling the game without exiting!
At this point, we still flat-out reload the game every 4 seconds. We don't really want this. If we make a change, we want it to appear whenever we hit the “compile” button. Instantaneously, immediately, right away.
The way of achieving this that we will adopt is comparing the last write time of the source DLL. If it changed, reload the DLL. This can be done easily, using Windows API.
First we need to find the file to use. We can do it usingFindFirstFileA function:
HANDLE FindFirstFileA(
LPCSTR lpFileName,
LPWIN32_FIND_DATAA lpFindFileData
);
lpFileName
: The full path of the file to find. It can use various wildcards like *.dll
or something but we know exactly where and what file to find.
lpFindFileData
: Pointer to WIN32_FIND_DATAA structure. Along with the handle to the found file, Windows will return to us a filled structure containing a wealth of information about the file.
We're after this find data. Among other things, it contains FILETIME
of ftLastWriteTime
that we can store and use for comparison.
One thing to remember that while we can work with the find data directly, we still get the handle to the file. We must not forget to close the handle using the FindClose function.
Let's write a quick helper function so that we never forget what to do. This function will take a filename and return the FILETIME
. You can find it below; hopefully everything that it does is clear to you by now.
inline FILETIME
Win32GetLastWriteTime(char *Filename)
{
FILETIME LastWriteTime = {};
WIN32_FIND_DATA FindData;
HANDLE FindHandle = FindFirstFileA(Filename, &FindData);
if (FindHandle != INVALID_HANDLE_VALUE)
{
LastWriteTime = FindData.ftLastWriteTime;
FindClose(FindHandle);
}
return (LastWriteTime);
}
internal win32_game_code
Win32LoadGameCode(char *SourceDLLName, char *TempDLLName)
//...
To compare the write times, we need to store the last write time first. We have our win32_game_code
structure; we could expand it to do so.
struct win32_game_code
{
HMODULE GameCodeDLL; FILETIME DLLLastWriteTime;
game_update_and_render *UpdateAndRender;
game_get_sound_samples *GetSoundSamples;
b32 IsValid;
};
Now we can simply save the write time of the source DLL whenever we load the game code.
inline FILETIME
Win32GetLastWriteTime(char *Filename)
{
//...
}
internal win32_game_code
Win32LoadGameCode(char *SourceDLLName, char *TempDLLName)
{
win32_game_code Result = {};
Result.DLLLastWriteTime = Win32GetLastWriteTime(SourceDLLName);
CopyFile(SourceDLLName, TempDLLName, FALSE);
Result.GameCodeDLL = LoadLibraryA(TempDLLName);
}
So far, we haven't made any changes to the core functionality. We're still latent. However, we're now in a position to fix that. With one fell swoop, we can get rid of the LoadCounter
and look at the write time instead. If the current write time and the stored write times are not equal, we reload the code.
win32_game_code Game = Win32LoadGameCode(SourceGameCodeDLLFullPath,
TempGameCodeDLLFullPath);u32 LoadCounter = 0;
u64 LastCycleCount = __rdtsc();
while (GlobalRunning)
{ FILETIME NewDLLWriteTime = Win32GetLastWriteTime(SourceDLLName); if (NewDLLWriteTime != Game.DLLLastWriteTime) { Game.DLLLastWriteTime = NewDLLWriteTime; Win32UnloadGameCode(&Game);
Game = Win32LoadGameCode(SourceGameCodeDLLFullPath,
TempGameCodeDLLFullPath); LoadCounter = 0; }
// ...
}
Unfortunately, this won't really compile. FILETIME
is a compound structure (“struct”), and C++ doesn't allow direct comparison of these. While we can certainly make a relevant comparison function ourselves, we can use one from the Windows API called CompareFileTime.
CompareFileTime
returns 0 if the two file times are equal. This is perfect for us since we only want to reload the code if they're not.
FILETIME NewDLLWriteTime = Win32GetLastWriteTime(SourceDLLName);if (CompareFileTime(&NewDLLWriteTime, &Game.DLLLastWriteTime)){
Game.DLLLastWriteTime = NewDLLWriteTime;
Win32UnloadGameCode(&Game);
Game = Win32LoadGameCode(SourceGameCodeDLLFullPath,
TempGameCodeDLLFullPath);
}
And this is all there's to it. If you compile the platform code now, you will see that live reloading is instantaneous.
All of what we've done today probably won't make it into the final game we ship. However, it will help us, the developer, along the way to make it happen.
You might have noticed that we made all of this almost trivially by just moving some things around. It comes from defining our code architecture well: separating platform and cross-platform code, memory handling, etc. This comes from an approach known as semantic compression, and you can read more about it here.
Next time, you will see how many other decisions we made in the past will prove helpful.
Metaprogramming is writing programs that write programs. In C specifically, it is a viable alternative for some missing features (like listing possible members of a struct).
In its most basic form, a meta-program looks something like this:
#include <stdio.h>
int main (int argc, char **argv)
{
FILE *CFile = fopen("out.cpp", "w");
fprintf(CFile, "int main(int argc, char **argv)\n");
fprintf(CFile, "{\n");
fprintf(CFile, "}\n");
}
That's it. First, you compile this program, run it, it will write you out.cpp
so you can compile that. Of course, such a program wouldn't do anything special in its current shape, but as you expand it more and more with complex functionality, things will start getting interesting.
Previous: Day 21. Loading Game Code Dynamically
Up Next: Day 23. Looped Live Code Editing