Day 22. Instantaneous Live Code Editing

Day 22. Instantaneous Live Code Editing
Video Length (including Q&A): 1h53

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.

Day 21 Day 23

(Top)
Variable PDB Name
  1.1  Add Ability to Edit DLL PDB Name
  1.2  Randomize DLL PDB Name
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
Remove Delay Between Reloads
    3.0.1  Get Last Write Time
  3.1  Store the Last Write Time
  3.2  Remove Latency
Recap
Programming Notions
  5.1  Metaprogramming
Navigation

   

Variable PDB Name

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:

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.

Note that the PDB is locked only in a some debuggers and not in the others. For instance, RemedyBG doesn't have this issue while Visual Studio does.

If your debugger doesn't have this issue, feel free to skip this section entirely.

   

Add Ability to Edit DLL PDB Name

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:

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
 Listing 1: [build.bat] Allowing PDB naming.
   

Randomize DLL PDB Name

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
 Listing 2: [build.bat] Random PDB naming.

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
 Listing 3: [build.bat] Cleaning up before recompilation.

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
 Listing 4: [build.bat] Cleaning up delete messages.

And there you have it, you will hear no more from the del command.

   

Re-enable Debugger Support

   

Retrieve Module Filename

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
);

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.

Keep in mind that 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));
//... }
 Listing 5: [win32_handmade.cpp] Retrieving the executable path.
   

Determine Module Path Without Filename

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.

OnePastLastSlashpointerEXEFilenamepointerw:\handmade\build\win32_handmade.exeModulefilenameDesired"cursor"Position

 Figure 1: Retrieving the desired position in a file string.

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; } }
 Listing 6: [win32_handmade.cpp > WinMain] Retrieving desired cursor position.

*Scan condition can also be written as *Scan != 0: While the value of Scan is not 0, continue scanning.

   

Get Desired Filenames

Let's suppose that we have a magic function, CatStrings, which allows us to conCATenate two strings together in a third string.

StringAw:\handmade\build\win32_handmade.exe\0texttocopyStringBhandmade.dll\0texttocopyResultw:\handmade\build\handmade.dll

 Figure 2: Hypothetical CatStrings functionality.

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);
 Listing 7: [win32_handmade.cpp > WinMain] Calculating full path of the source game DLL.

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);
 Listing 8: [win32_handmade.cpp > WinMain] Removing null terminator from the source strings.

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);
 Listing 9: [win32_handmade.cpp > WinMain] Calculating full path of the game copy DLL.

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; }
 Listing 10: [win32_handmade.cpp] Defining CatStrings function.
   

Use New Strings

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_code
Win32LoadGameCode(char *SourceDLLName, char *TempDLLName)
{ win32_game_code Result = {};
CopyFile(SourceDLLName, TempDLLName, FALSE); Result.GameCodeDLL = LoadLibraryA(TempDLLName);
Result.DLLLastWriteTime = Win32GetLastWriteTime(SourceDLLName); // ... }
 Listing 11: [win32_handmade.cpp] Passing both file names to Win32LoadGameCode.

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; } // ... }
 Listing 12: [win32_handmade.cpp > WinMain] Using full file paths.

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!

   

Remove Delay Between Reloads

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.

   

Get Last Write Time

First we need to find the file to use. We can do it usingFindFirstFileA function:

HANDLE FindFirstFileA(
    LPCSTR             lpFileName,
    LPWIN32_FIND_DATAA lpFindFileData
);

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) //...
 Listing 13: [win32_handmade.cpp] Defining Win32GetLastWriteTime.
   

Store the Last Write Time

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; };
 Listing 14: [win32_handmade.h] Expanding win32_game_code to include last write time.

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); }
 Listing 15: [win32_handmade.cpp] Saving last write time. .
   

Remove Latency

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;
} // ... }
 Listing 16: [win32_handmade.cpp > WinMain] Using last write time to reload the code.

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); }
 Listing 17: [win32_handmade.cpp > WinMain] Comparing file times.

And this is all there's to it. If you compile the platform code now, you will see that live reloading is instantaneous.

   

Recap

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.

   

Programming Notions

   

Metaprogramming

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.

   

Navigation

Previous: Day 21. Loading Game Code Dynamically

Up Next: Day 23. Looped Live Code Editing

Back to Index

Glossary

Compiler flags

-FD

-PDB

Win32 API

CompareFileTime

FindFirstFileA

FindClose

GetModuleFileNameA

WIN32_FIND_DATAA structure

Articles

Semantic Compression

CMD redirection syntax

formatted by Markdeep 1.10