Day 21. Loading Game Code Dynamically

Day 21. Loading Game Code Dynamically
Video Length (including Q&A): 1h43

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.

People often ask: are we going to use a scripting language for Handmade Hero? We definitely won't use an existing one, maybe will eventually develop one. But the correct question is: is it a great idea in first place?

Day 20 Day 22

(Top)
Plan For Today
  1.1  About Scripting Languages
  1.2  Hot Reloading
Change build.bat
  2.1  Clean Up Build Variables
  2.2  Request handmade.dll Compilation
  2.3  Export Functions
Separate Game from Platform
  3.1  Move Shared Types
  3.2  Update API
  3.3  Inspect Linker Errors
  3.4  Load Game Code
  3.5  Services to the Game
  3.6  Guard Against Name Mangling
Dynamic Reloading
  4.1  Unload Game Code
  4.2  Slow Down Reloading
  4.3  Allow DLL Rewriting
Recap
Side Debugging
  6.1  Fix Sound Issue
Navigation

   

Plan For Today

   

About Scripting Languages

To answer this question, we need to determine why we'd be using a scripting language and whether it covers the downsides. And the downsides, if you look closely, are many:

If you look at these, it's not really a matter of 'scripting languages are bad'. However, it takes a lot of time to get all those things right, so such use should just be evaluated on a case-by-case basis. For Handmade Hero, it's definitely a no-go.

But maybe we can get all the benefits without having to do all the hard work? When you think of a scripting language, these benefits come to your mind (compared to the programming language the engine is developed on):

This is a debatable point, really. Instead, it seems to be coming from lousy API practices in the programming languages rather than the scripting ones' strengths. Since we're going to really focus on writing a good API for our game, we should be fine in C.

Now, this is a great tool to have. When it comes to fine-tuning the game, having our program running while you modify its values can save a lot of time. Otherwise, you need to exit debugging, change the necessary values, recompile, restart the game, jump to whatever encounter you were tuning, realize the edit wasn't perfect, jump back... This indeed can become very old very quickly.

StartthegameSkiptothedesiredlevelPlaytestFindavaluetofixStopthegameGotothegamecodeFixdesiredvalueRecompilerepeatuntilhappy

 Figure 1: A nightmare game tuning session.

Of course, some debuggers, like Visual Studio, have “Edit and Continue” functionality, where you could set a breakpoint, edit a variable, and continue from where you left. Unfortunately, this functionality is usually quite limited and doesn't really work in more complex situations.

Maybe we could implement our own fool-proof version of “Edit and Continue”? Something that would allow us to replicate the same functionality of “Change the code live” → “Recompile” without breaking or exiting the game!

StartthegameSkiptothedesiredlevelPlaytestFindavaluetofixGotothegamecodeFixdesiredvalueRecompilerepeatuntilhappy

 Figure 2: Our ideal debugging session. No more start-stop for us!

   

Hot Reloading

We're just out of a very complex sound debugging part, so it's about time for us to do something fun! Let's see if we can use Windows to help us do it.

If you review our game code structure so far, you'll see that it's already quite neatly separating Windows (platform-specific layer) from our core game functionality. So, to start implementing the live code editing, we first should ask ourselves: could we split those two things apart entirely? You'll have the platform code (containing WinMain and other platform-specific code) compiled separately from the game code.

And then, will it be possible to unload the game part without killing the executable?

The answer is “yes” to both of these questions. We already have the ability of loading libraries that we implemented for our XInput code when we were working on the controller input:

internal void
Win32LoadXInput()
{
    HMODULE XInputLibrary = LoadLibraryA("Xinput1_4.dll");
    if(!XInputLibrary)
    {
        XInputLibrary = LoadLibraryA("Xinput1_3.dll");
    }
    if(!XInputLibrary)
    {
        XInputLibrary = LoadLibraryA("Xinput9_1_0.dll");
    }
    
    if(XInputLibrary)
    {
        XInputGetState = (x_input_get_state *)GetProcAddress(XInputLibrary, "XInputGetState");
        XInputSetState = (x_input_set_state *)GetProcAddress(XInputLibrary, "XInputSetState");
    }    
}

Here, we first call LoadLibraryA to load a DLL. After we ensure that the operation succeeded we retrieve the necessary functions using GetProcAddress and use them as usual.

If we want, we can just do it with our own code. Instead of compiling our program monolithically, we will build the program as two separate translation units. In simpler terms, we will run our compilation on two different code chunks separately: one for the platform and one for the game. The platform-independent code will be loaded as a .dll to do whatever we need.

After that, it's simple. When we change our game code, we will simply unload, rebuild, and reload the DLL without killing the executable. It should just work!

What really enables us to do it this way is passing memory to the game in a single chunk. We said in the past that there're many good reasons for doing it, well, that's one of them. Our platform layer reserves the memory and passes the whole chunk to the game. When the game is done doing its work, the memory is preserved for the main loop's next iteration. This allows us to unload the game, swap the game code with the new version, and give it the memory the previous guy was using. And poof! The game will keep running as before.

Cross-platformAPIboundaryInputPlatformFileI/OGameexecutableMemoryLibraryGraphicsSound

 Figure 3: The API boundary will now be complete.

Now, there're a couple of caveats where the magic won't work in all places. For instance, if the structures have changed (members were added or removed), this can potentially affect the layout of the memory using those structures, so the game wouldn't be able to read old memory anymore and require a restart. However, it's a simple way of envisioning how such a code would work, at least for the time being.

Another issue that one can think about are the compile times. Some projects are notoriously slow to build, but this comes from lousy programming practices rather than a codebase's complexity. If your code takes more than 10 seconds to compile, you're doing it wrong!

   

Change build.bat

The first step on our journey would be updating our build.bat.

   

Clean Up Build Variables

Last time, we might have gone a little bit overboard with the variables. Our cl line currently looks like this:

cl -Od %compiler% -DHANDMADE_WIN32=1 %defines% %debug% -Fmwin32_handmade.map %code_path%win32_handmade.cpp %win32_libs% /link %win32_link%

where:

Now, the last one can be reworked. We only have 1 Windows-specific flag, the subsystem, which we can pass directly instead of a variable, while the common linker flags can be grouped and expanded together. We want to add -incremental:no flag to the linker flags, simply to make sure we do the complete recompile each time.

:: WIN32 PLATFORM LIBRARIES
set win32_libs=             user32.lib
set win32_libs=%win32_libs% gdi32.lib
set win32_libs=%win32_libs% winmm.lib
:: COMMON LINKER SWITCHES set link= -opt:ref &:: Remove unused functions set link=%link% -incremental:no &:: Perform full link each time
:: 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
:: 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 %link% -subsystem:windows,5.2
 Listing 1: [build.bat] Updating linker flag variables.

Remember that you still need to add /link so that the compiler knows it needs to pass the flags that follow to the linker.

Feel free to expand or change the variables as you feel appropriate to your workflow.

   

Request handmade.dll Compilation

Because we want to make two separate things, we want to call the compiler, cl, twice. This will, in return, produce two files (aside from a bunch of debug files and build artifacts):

In the new line, we can specify:

:: No optimizations (slow): -Od; all optimizations (fast): -O2
cl -Od %compiler% %defines% %debug% -Fmhandmade.map %code_path%handmade.cpp -LD /link %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] Requesting to build handmade.dll.

We miss one last step here.

   

Export Functions

DLL is not much different from an EXE. It essentially contains executable code, like an .exe. Contrary to the latter, though, a DLL has a specific table of functions. These functions can be called from outside the DLL by whoever asks for them. It's this table that a function GetProcAddress goes to read in an attempt to find a specific location to call. This allows linking at runtime, dynamically (hence the name Dynamically Linked Library, or DLL for short).

To allow such functionality, we need to tell the linker which functions we're exporting. There're a couple of ways to do this:

The first method seems quite simple, except it has several drawbacks. The attribute is not standardized, so different operating systems (and even different compilers!) have their own designations. Using a specific one inside the code file, we're essentially tying ourselves to the Microsoft's compiler, and that's not necessarily something that we'd love to do.

We don't have many functions to export. We only want to export GameUpdateAndRender and GameGetSoundSamples, so let's go with the second option.

The keyword we're after is?. Inside the build.bat file, you can write directly on the compile line, or you can add another variable to use for simpler reading later on, as we did below.

:: COMMON LINKER SWITCHES
set link=                   -opt:ref               &:: Remove unused functions
set link=%link%             -incremental:no        &:: Perform full link each time
:: DLL LINKER SWITCHES set dll_link= /EXPORT:GameUpdateAndRender set dll_link=%dll_link% /EXPORT:GameGetSoundSamples
:: No optimizations (slow): -Od; all optimizations (fast): -O2
cl -Od %compiler% %defines% %debug% -Fmhandmade.map %code_path%handmade.cpp -LD /link %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] Adding functions to export.

Of course, if we try to build now, the compiler will complain loudly of all the things it doesn't like. Let's get to finishing separating the two layers.

   

Separate Game from Platform

Up until now, we were building the game as one big monolithic block. Our platform layer and the game were compiled as a single translation unit, even if we separated the two semantically. We have some shared items that we have between the two, but we also have some components that should never talk with each other.

Let's unify the shared and separate even further specific code.

   

Move Shared Types

The first step would be to move the type definitions and common includes away from win32_handmade.cpp header. We also want to remove any reference from handmade.cpp:

// TODO(casey): Implement sine ourselves #include <math.h> #include <stdint.h> typedef int8_t s8; typedef int16_t s16; typedef int32_t s32; typedef int64_t s64; typedef s32 b32; typedef uint8_t u8; typedef uint16_t u16; typedef uint32_t u32; typedef uint64_t u64; typedef float f32; typedef double f64; #define internal static #define local_persist static #define global_variable static #define Pi32 3.14159265359f #include "handmade.cpp"
#include "handmade.h"
#include <windows.h> #include <stdio.h> #include <xinput.h> #include <dsound.h> #include "win32_handmade.h"
 Listing 4: [win32_handmade.cpp] Removing non-Windows specific typedefs and #includes.

Our handmade.h currently fulfills the role of a common interface quite nicely. Let's move all this cut code on top of the file (except handmade.cpp include, we don't want circular includes!).

#if !defined(HANDMADE_H)

/*
NOTE(casey):

HANDMADE_INTERNAL:
0 - Build for public release
1 - Build for developer only

HANDMADE_SLOW:
0 - No slow code allowed!
1 - Slow code welcome.
*/
// TODO(casey): Implement sine ourselves #include <math.h> #include <stdint.h> typedef int8_t s8; typedef int16_t s16; typedef int32_t s32; typedef int64_t s64; typedef s32 b32; typedef uint8_t u8; typedef uint16_t u16; typedef uint32_t u32; typedef uint64_t u64; typedef float f32; typedef double f64; #define internal static #define local_persist static #define global_variable static #define Pi32 3.14159265359f
#if HANDMADE_SLOW #define Assert(Expression) if (!(Expression)) { *(int *)0 = 0; } #else #define Assert(Expression) #endif
 Listing 5: [handmade.h] Adding common #defines, typedefs and #includes.
   

Update API

Let's try to compile and let the compiler errors guide us. The first error that we get is actually a compiler warning! It's a good thing that we increased our warning level and asked the compiler to treat all warnings as errors:

handmade.h(178) : warning C4505: 'GameUpdateAndRender' : unreferenced local function has been removed
handmade.h(178) : warning C4505: 'GameGetSoundSamples' : unreferenced local function has been removed

That's a good warning to catch! The keyword we're looking at here is local. We already asked build.bat to export these functions as publicly available, but in our code we still mark them as internal (or static if we go back to internal definition). Let's fix that! We need to update both handmade.h and handmade.cpp:

void GameUpdateAndRender(game_memory *Memory, game_input *Input, game_offscreen_buffer* Buffer);
// NOTE(casey): At the moment, this has to be a very fast function, it cannot be // more than a millisecond or so. // TODO(casey): Reduce the pressure on this function's performance by measuring it // or asking about it, etc.
void GameGetSoundSamples(game_memory *Memory, game_sound_output_buffer *SoundBuffer);
 Listing 6: [handmade.h] Removing internal from the function declarations.
void
GameUpdateAndRender(game_memory* Memory, game_input *Input, game_offscreen_buffer* Buffer) { //... }
void
GameGetSoundSamples(game_memory* Memory, game_sound_output_buffer *SoundBuffer) { // ... }
 Listing 7: [handmade.cpp] Removing internal from the function definitions.

If we look at the compiler errors further down, you'll see that we have similar warnings for the debug services we provide to the game: for now, it's only file I/O (reading/writing files), but this might change in the future. Let's update make them non-static, as well.

debug_read_file_result DEBUGPlatformReadEntireFile(char *Filename); void DEBUGPlatformFreeFileMemory(void *Memory); b32 DEBUGPlatformWriteEntireFile(char *Filename, u32 MemorySize, void *Memory);
 Listing 8: [handmade.h] Updating debug services declarations.
void
DEBUGPlatformFreeFileMemory(void *Memory) { // ... }
debug_read_file_result
DEBUGPlatformReadEntireFile(char *Filename) { //... }
b32
DEBUGPlatformWriteEntireFile(char *Filename, u32 MemorySize, void *Memory) { // ... }
 Listing 9: [win32_handmade.cpp] Updating debug services definitions.

We don't need to export these functions through build.bat. We'll do something else to pass them to the game further down the line.

   

Inspect Linker Errors

If you've done everything correctly, the compiler will spit out something scary-looking like this:

handmade.cpp
handmade.obj : error LNK2019: unresolved external symbol "struct debug_read_file_result __cdecl DEBUGPlatformReadEntireFile(char *)" (?DEBUGPlatformReadEntireFile@@YA?AUdebug_read_file_result@@PEAD@Z) referenced in function "void __cdecl GameUpdateAndRender(struct game_memory *,struct game_input *,struct game_offscreen_buffer *)" (?GameUpdateAndRender@@YAXPEAUgame_memory@@PEAUgame_input@@PEAUgame_offscreen_buffer@@@Z)
handmade.obj : error LNK2019: unresolved external symbol "void __cdecl DEBUGPlatformFreeFileMemory(void *)" (?DEBUGPlatformFreeFileMemory@@YAXPEAX@Z) referenced in function "void __cdecl GameUpdateAndRender(struct game_memory *,struct game_input *,struct game_offscreen_buffer *)" (?GameUpdateAndRender@@YAXPEAUgame_memory@@PEAUgame_input@@PEAUgame_offscreen_buffer@@@Z)
handmade.obj : error LNK2019: unresolved external symbol "int __cdecl DEBUGPlatformWriteEntireFile(char *,unsigned int,void *)" (?DEBUGPlatformWriteEntireFile@@YAHPEADIPEAX@Z) referenced in function "void __cdecl GameUpdateAndRender(struct game_memory *,struct game_input *,struct game_offscreen_buffer *)" (?GameUpdateAndRender@@YAXPEAUgame_memory@@PEAUgame_input@@PEAUgame_offscreen_buffer@@@Z)
handmade.dll : fatal error LNK1120: 3 unresolved externals

Don't worry, these are simply linker errors, and it's good news for us! First, it means that compiler doesn't have anything to complain about, and it passed the work to the linker. Linker, on the other hand, has its own set of errors marked by LNK[error number].

In this case, each error says:

Hey, you tried to call this function “bla bla bla” (symbol name %@scary#looking#something!%!), you even declared it here: “bla bla bla” (more scary symbols), but I didn't find it anywhere. Where is it? — Linker
These are the places where the two halves of our code call each other, and they cannot find the respective code to execute. We have to find a way to resolve these.

   

Load Game Code

On win32_handmade.cpp side, we pretty much have the infrastructure ready to load the game code: we'll simply replicate the behavior of Win32LoadXInput function and repeat the magic of the stub functions.

Let's start with the stub functions first. If you remember, this is what we needed to do:

We can do it for both GameUpdateAndRender and GameGetSoundSamples. Also, we can remove the function declarations we had earlier because they are no longer necessary.

#define GAME_UPDATE_AND_RENDER(name) void name(game_memory *Memory, game_input *Input, game_offscreen_buffer* Buffer) typedef GAME_UPDATE_AND_RENDER(game_update_and_render); GAME_UPDATE_AND_RENDER(GameUpdateAndRenderStub) { }
void GameUpdateAndRender(game_memory *Memory, game_input *Input, game_offscreen_buffer* Buffer);
// NOTE(casey): At the moment, this has to be a very fast function, it cannot be // more than a millisecond or so. // TODO(casey): Reduce the pressure on this function's performance by measuring it // or asking about it, etc.
#define GAME_GET_SOUND_SAMPLES(name) void name(game_memory *Memory, game_sound_output_buffer *SoundBuffer) typedef GAME_GET_SOUND_SAMPLES(game_get_sound_samples); GAME_GET_SOUND_SAMPLES(GameGetSoundSamplesStub) { }
void GameGetSoundSamples(game_memory *Memory, game_sound_output_buffer *SoundBuffer);
 Listing 10: [handmade.h] Implementing stub functions for game functions.

We now also have an added benefit in that, should our function signature ever change, we only need to modify it in one place (instead of editing both the .h file and the .cpp file.). Let's edit our GameUpdateAndRender and GameGetSoundSamples function declarations one last time to enable this:

GAME_UPDATE_AND_RENDER(GameUpdateAndRender)
{ // ... }
GAME_GET_SOUND_SAMPLES(GameGetSoundSamples)
{ // ... }
 Listing 11: [handmade.cpp] Using signature shortcuts.

Hopefully, you can see what's going on here. Instead of writing void GameUpdateAndRender(...) we say GAME_UPDATE_AND_RENDER(GameUpdateAndRender) and define the actual function signature elsewhere.

Now that we have the necessary types and stubs, we can easily use them in our platform code. Let's write a function to load the game code.

internal void Win32LoadGameCode() { HMODULE GameCodeDLL = LoadLibraryA("handmade.dll"); if(GameCodeDLL) { GameUpdateAndRender = (game_update_and_render *)GetProcAddress(GameCodeDLL, "GameUpdateAndRender"); GameGetSoundSamples = (game_get_sound_samples *)GetProcAddress(GameCodeDLL, "GameGetSoundSamples"); } }
internal void Win32LoadXInput()
 Listing 12: [win32_handmade.cpp] Loading game code from the DLL.

For XInput, we used global variables to store the function calls. Let's try something different this time. Instead of using globals, we can think of a win_game_code structure that initialized and returned directly from this function. Furthermore, this allows us to store the DLL module handle and a failsafe to ensure our code is valid. Let's add all this to win32_handmade.h:

struct win32_debug_time_marker
{
    // ...
};
struct win32_game_code { HMODULE GameCodeDLL; game_update_and_render *UpdateAndRender; game_get_sound_samples *GetSoundSamples; b32 IsValid; };
 Listing 13: [win32_handmade.h] Introducing win32_game_code.

We can now use this new structure in Win32LoadGameCode: we will return it as the result of our function, and we'll also make sure that this result is valid.

The validity will be determined by whether or not we successfully load the functions. If GetProcAddress returns 0 at least for one of the exports, the whole win32_game_code will be flagged as invalid.

internal win32_game_code
Win32LoadGameCode() {
win32_game_code Result = {};
Result.GameCodeDLL = LoadLibraryA("handmade.dll"); if(Result.GameCodeDLL)
{
Result.UpdateAndRender = (game_update_and_render *)GetProcAddress(Result.GameCodeDLL, "GameUpdateAndRender"); Result.GetSoundSamples = (game_get_sound_samples *)GetProcAddress(Result.GameCodeDLL, "GameGetSoundSamples");
Result.IsValid = Result.GetSoundSamples && Result.UpdateAndRender;
}
if (!Result.IsValid) { Result.UpdateAndRender = GameUpdateAndRenderStub; Result.GetSoundSamples = GameGetSoundSamplesStub; } return(Result);
}
 Listing 14: [win32_handmade.cpp] Using win32_game_code.

Let's load the game code before going into GlobalRunning. Later we might need to move it since we'll be swapping it on the fly, but for the time being, it should suffice. We'll then replace the previous calls to the game code we had to use our Game.

While we're at it, let's remove our debug code for determining sound update frequency.

#if 0 // NOTE(casey): This tests the PlayCursor/WriteCursor update frequency // On the Handmade Hero machine, it was 480 samples. while (GlobalRunning) { DWORD PlayCursor; DWORD WriteCursor; GlobalSecondaryBuffer->GetCurrentPosition(&PlayCursor, &WriteCursor); char TextBuffer[256]; _snprintf_s(TextBuffer, sizeof(TextBuffer), "PC:%u WC:%u\n", PlayCursor, WriteCursor); OutputDebugStringA(TextBuffer); } #endif
win32_game_code Game = Win32LoadGameCode();
u64 LastCycleCount = __rdtsc(); while (GlobalRunning) { // ... game_offscreen_buffer Buffer = {}; Buffer.Memory = GlobalBackbuffer.Memory; Buffer.Width = GlobalBackbuffer.Width; Buffer.Height = GlobalBackbuffer.Height; Buffer.Pitch = GlobalBackbuffer.Pitch;
Game.UpdateAndRender(&GameMemory, NewInput, &Buffer);
// ... game_sound_output_buffer SoundBuffer = {}; SoundBuffer.SamplesPerSecond = SoundOutput.SamplesPerSecond; SoundBuffer.SampleCount = BytesToWrite / SoundOutput.BytesPerSample; SoundBuffer.Samples = Samples;
Game.GetSoundSamples(&GameMemory, &SoundBuffer); // ... }
 Listing 15: [win32_handmade.cpp > WinMain] Loading Game Code during the game's execution.

Now our Win32 platform layer should compile correctly. When compiling you should see the output like this:

handmade.cpp
   Creating library handmade.lib and object handmade.exp
handmade.obj : error LNK2019: [linker error description]
handmade.obj : error LNK2019: [linker error description]
handmade.obj : error LNK2019: [linker error description]
handmade.dll : fatal error LNK1120: 3 unresolved externals
win32_handmade.cpp

This means that we couldn't compile handmade.dll because there were 3 linker errors. On the other hand, win32_handmade.cpp compiled without any issues.

Win32_handmade.exe will be loading game code if the latter is available. But, if you run it, the program should execute its main loop anyway. It display only the debug sound lines because these are all calculated on the platform side. Of course, all the DLL-side functionality will be missing: you'll also a distinct lack of the gradient or sound, and the lines will eventually overlap because we don't clear the screen to black at each frame.

   

Services to the Game

We have a significant roadblock to overcome for what might concern the game code: how would we load platform-specific services (like reading and writing files)?

We could load the executable in the DLL, load a handle for it and then reverse call the functions. But is it indispensable?

The thing is, when we call GameUpdateAndRender or GameGetSoundSamples, we pass a bunch of different services: things like memory or the buffers to fill. We could simply pass the pointers to whichever platform function the game might want to call. We've seen this already: if you think about the DirectSound buffers, they also come with the respective function pointers to call (things like the famous SecondaryBuffer->GetCurrentPosition(...)).

In practice, this will result in the exact same syntax we have for things like XInput or GameUpdateAndRender. Except for this time, instead of calling Windows' function GetProcAddress, we'll straight up pass the function pointers to the game.

As an added benefit, we won't even need the stub functions.

#if HANDMADE_INTERNAL
struct debug_read_file_result
{
    u32 ContentsSize;
    void *Contents;
};
#define DEBUG_PLATFORM_FREE_FILE_MEMORY(name) void name (void *Memory) typedef DEBUG_PLATFORM_FREE_FILE_MEMORY(debug_platform_free_file_memory); #define DEBUG_PLATFORM_READ_ENTIRE_FILE(name) debug_read_file_result name (char *Filename) typedef DEBUG_PLATFORM_READ_ENTIRE_FILE(debug_platform_read_entire_file); #define DEBUG_PLATFORM_WRITE_ENTIRE_FILE(name) b32 name (char *Filename, u32 MemorySize, void *Memory) typedef DEBUG_PLATFORM_WRITE_ENTIRE_FILE(debug_platform_write_entire_file);
debug_read_file_result DEBUGPlatformReadEntireFile(char *Filename); void DEBUGPlatformFreeFileMemory(void *Memory); b32 DEBUGPlatformWriteEntireFile(char *Filename, u32 MemorySize, void *Memory);
#endif
 Listing 16: [handmade.h] Introducing types for our file signatures.

These somewhat verbose names should, however, be quite descriptive in what they do. They are Debug, so they shouldn't be used in user-facing code, They're platform-independent, and they do a specific operation, like reading a file, writing to a file, or freeing its memory.

This allows passing the function pointers as if they were other types: int, DWORD, u32... debug_platform_write_entire_file has now become just another type.

Again, as an optional step to potentially prevent headaches in the future, let's replace the signature of the function definitions in win32_handmade.cpp with our macros:

DEBUG_PLATFORM_FREE_FILE_MEMORY(DEBUGPlatformFreeFileMemory)
{ // ... }
DEBUG_PLATFORM_READ_ENTIRE_FILE(DEBUGPlatformReadEntireFile)
{ // ... }
DEBUG_PLATFORM_WRITE_ENTIRE_FILE(DEBUGPlatformWriteEntireFile)
{ // ... }
 Listing 17: [win32_handmade.cpp] Using signature shortcuts.

Now, how do we pass these to the game? Well, we are passing the memory by packaging it in game_memory structure. We can expand it to include the pointers to these functions as follows:

struct game_memory
{
    u64 PermanentStorageSize;
    void *PermanentStorage;
    u64 TransientStorageSize;
    void *TransientStorage;
    b32 IsInitialized;
    
debug_platform_free_file_memory *DEBUGPlatformFreeFileMemory; debug_platform_read_entire_file *DEBUGPlatformReadEntireFile; debug_platform_write_entire_file *DEBUGPlatformWriteEntireFile;
};
 Listing 18: [handmade.h] Introducing type pointers for our file signatures.

Of course, we should remember to actually pass the pointers so that we don't crash and burn painfully (Attempting to call a function located at sector 0 is a good way to crash, that's how we do our Assert right now).

game_memory GameMemory = {};
GameMemory.PermanentStorageSize = Megabytes(64);
GameMemory.TransientStorageSize = Gigabytes(1);
GameMemory.DEBUGPlatformFreeFileMemory = DEBUGPlatformFreeFileMemory; GameMemory.DEBUGPlatformReadEntireFile = DEBUGPlatformReadEntireFile; GameMemory.DEBUGPlatformWriteEntireFile = DEBUGPlatformWriteEntireFile;
u64 TotalStorageSize = (GameMemory.PermanentStorageSize + GameMemory.TransientStorageSize); GameMemory.PermanentStorage = VirtualAlloc(BaseAddress, (size_t)TotalStorageSize, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE); GameMemory.TransientStorage = ((u8 *)GameMemory.PermanentStorage + GameMemory.PermanentStorageSize);
 Listing 19: [win32_handmade.cpp] Passing function pointers.

Finally, we need to actually use these functions in the game. We aren't doing anything useful with them right now, we simply have this block for testing:

debug_read_file_result FileData = DEBUGPlatformReadEntireFile(__FILE__);
if (FileData.Contents)
{
    DEBUGPlatformWriteEntireFile("test.out", FileData.ContentsSize, FileData.Contents);
    DEBUGPlatformFreeFileMemory(FileData.Contents);
}
[handmade.cpp]

We pass game_memory as a pointer named Memory. So, in order to use the functions we will simply dereference them from the pointer (with the -> operator).

debug_read_file_result FileData = Memory->DEBUGPlatformReadEntireFile(__FILE__);
if (FileData.Contents) {
Memory->DEBUGPlatformWriteEntireFile("test.out", FileData.ContentsSize, FileData.Contents); Memory->DEBUGPlatformFreeFileMemory(FileData.Contents);
}
 Listing 20: [handmade.cpp] Using function pointers.

And... that's it.

What we did here is a much simpler version of something called a VTable in C++, almost a Memory Dispatch of sorts. The major difference is that, in C++, all of this happens under the hood. Here, we remove this functionality from its proverbial black box so that we can see what it does and do whatever we want with it.

But there's still one caveat we haven't discussed yet: name mangling.

   

Guard Against Name Mangling

If you remember, we recently encountered the absolutely out-of-the-world names in our linker errors: something like GameUpdateAndRender@@YAXPEAUgame_memory@@PEAUgame_input@@PEAUgame_offscreen_buffer@@@Z. As we discussed on day 16, this is called name mangling, and it's a compiler technique to distinguish functions with the same name but different signatures. It becomes even more necessary in a universe where you have classes and namespaces; each may have multiple instances of the same function name... Something that you see a lot in C++.

We don't need any of this, even less so in our exported functions. Luckily, there's a way to disable name mangling for specific functions (or even bigger code blocks). To do that, you need to use a very unique function prefix extern "C". This effectively regresses whatever code block you're using from C++ to C, and the latter doesn't support function overloading nor allows name mangling. Problem solved, right?

Well, yes and no. You see, now this code cannot be compiled in C anymore. So what you need to do is verify first if the program is being compiled in C++ mode! The final code becomes like this:

#if defined __cplusplus extern "C" #endif
GAME_UPDATE_AND_RENDER(GameUpdateAndRender) { // ... }
#if defined __cplusplus extern "C" #endif
GAME_GET_SOUND_SAMPLES(GameGetSoundSamples) { // ... }
 Listing 20: [handmade.cpp] Avoiding name mangling.

Finally, we can compile everything without errors. If you want to verify that the necessary functions are exported correctly, Visual Studio packages the utility dumpbin that you can call for this exact purpose. Simply type `dumpbin /exports [path-to-dll]/handmade.dll in your console to hopefully see the following:

W:\handmade>dumpbin /exports build\handmade.dll
Microsoft (R) COFF/PE Dumper Version XX.XX.XXXXX.X Copyright (C) Microsoft Corporation. All rights reserved. Dump of file build\handmade.dll File Type: DLL Section contains the following exports for handmade.dll 00000000 characteristics FFFFFFFF time date stamp 0.00 version 1 ordinal base 2 number of functions 2 number of names ordinal hint RVA name 1 0 000014D0 GameGetSoundSamples = GameGetSoundSamples 2 1 00001270 GameUpdateAndRender = GameUpdateAndRender Summary 2000 .data 1000 .pdata A000 .rdata 1000 .reloc C000 .text 1000 _RDATA
[Command Prompt]

If you see the exported functions GameUpdateAndRender and GameGetSoundSamples, great! If not, the game will not see them and GetProcAddress will fail. In such case verify if, for example, you included %dll_link% in your build.bat file.

If everything's in order, run the game. Success!

   

Dynamic Reloading

So far, we didn't do a whole lot. We basically replicated the functionality that we already had of the game functions loading with the rest of the game. Now we can add the thing we set off doing in the first place: dynamic code reloading!

Today we won't make the entire thing. Some parts of it will need to be fleshed out next time, we'll just make the quick and dirty change.

   

Unload Game Code

To load a new version of the DLL (and even try to recompile it), we need to unload the old version first. You can imagine it by simply calling a function like Win32UnloadGameCode.

internal win32_game_code
Win32LoadGameCode()
{
    // ...
}
internal void Win32UnloadGameCode(win32_game_code *GameCode) { if (GameCode->GameCodeDLL) { FreeLibrary(GameCode->GameCodeDLL); GameCode->GameCodeDLL = 0; } GameCode->IsValid = false; GameCode->UpdateAndRender = GameUpdateAndRenderStub; GameCode->GetSoundSamples = GameGetSoundSamplesStub; }
 Listing 21: [win32_handmade.cpp] Introducing Win32UnloadGameCode.

Let's do something ridiculous and do the game loading and unloading at each frame.

win32_game_code Game = Win32LoadGameCode();
            
u64 LastCycleCount = __rdtsc();
while (GlobalRunning)
{
Win32UnloadGameCode(&Game); Game = Win32LoadGameCode();
// ... }
 Listing 22: [win32_handmade.cpp] Reloading the game code at each frame.

It works! Somewhat. We still cannot reap the benefits since the change happens so quickly that we don't have time to recompile the code. Also, you might uncover a sound bug. Let's investigate it.

Feel free to find the bug yourself and then check the solution in subsection 6.1.

   

Slow Down Reloading

Right now we are reloading our DLL 30 times per second. That's a lot of unnecessary I/O. In the future, we'll try to find a way to detect changes in the DLL, but for now, let's simply add a timer, so that the DLL reload attempt happens only every few seconds:

win32_game_code Game = Win32LoadGameCode();
u32 LoadCounter = 0;
u64 LastCycleCount = __rdtsc(); while (GlobalRunning) {
if (LoadCounter++ > 120) {
Win32UnloadGameCode(&Game); Game = Win32LoadGameCode();
LoadCounter = 0; }
// ... }
 Listing 23: [win32_handmade.cpp > WinMain] Adding LoadCounter.
   

Allow DLL Rewriting

Even if the reloading is happening every few seconds, we still can't recompile the DLL because unloading and loading occur back to back.

We can add a quick and dirty solution now and flesh it out next time. This solution won't work inside the debugger for several reasons, but it will work if you simply run win32_handmade.exe directly from Windows.

The solution is simply to copy our DLL to another location and load it from there. To do this, we'll use the aptly named CopyFile function (MSDN reference) inside Win32LoadGameCode:

win32_game_code Result = {};
    
CopyFile("handmade.dll", "handmade_temp.dll", FALSE);
Result.GameCodeDLL = LoadLibraryA("handmade_temp.dll");
 Listing 24: [win32_handmade.cpp > Win32LoadGameCode] Copying file for loading.

If you've done everything correctly, you can now do live code editing as on the video below!

 Figure 4: Live code editing!

   

Recap

We can now edit our game on the fly, and the state is preserved! We don't need to do anything, almost as good as having a scripting language.

The system is not perfect yet, though. For the time being, we can't use our debugger; besides, there're other things to potentially tune up (like waiting 4 seconds before reloading).

We'll do it all next time.

   

Side Debugging

   

Fix Sound Issue

Let's inspect our sound code inside handmade.cpp:

internal void
GameOutputSound(game_sound_output_buffer *SoundBuffer, int ToneHz)
{
    local_persist f32 tSine;
    s16 ToneVolume = 3000;
    int WavePeriod = SoundBuffer->SamplesPerSecond / ToneHz;
    
    s16 *SampleOut = SoundBuffer->Samples;
    for (int SampleIndex = 0;
         SampleIndex < SoundBuffer->SampleCount;
         ++SampleIndex)
    {
        f32 SineValue = sinf(tSine);
        s16 SampleValue = (s16)(SineValue * ToneVolume);
        
        *SampleOut++ = SampleValue;
        *SampleOut++ = SampleValue;
        tSine += 2.0f * Pi32 * 1.0f / (f32)WavePeriod;
        
        if (tSine > 2.0f * Pi32)
        {
            tSine -= 2.0f * Pi32;
        }
    }
}

After a short inspection, it's clear to us that the culprit is tSine, a variable we use to calculate the sound oscillation in our debug code. It's a static variable (local_persist), and, as such, it has its own block of memory allocated on the stack. When the DLL is reloaded, all of its static data is reset, tSine resets to zero, thus producing sound bug.

It's time for our tSine to move to GameState, the dedicated block of memory that we take from the persistent memory and use for our game code.

struct game_state
{
    int ToneHz;
f32 tSine;
int XOffset; int YOffset; };
 Listing 25: [handmade.h] Updating game_state structure.

Now we can change our GameOutputSound code to actually use the correct tSine. We also need to pass the pointer to the game state, which you'll grow to do by default as this course progresses.

internal void
GameOutputSound(game_state *GameState, game_sound_output_buffer *SoundBuffer, int ToneHz)
{
local_persist f32 tSine;
s16 ToneVolume = 3000; int WavePeriod = SoundBuffer->SamplesPerSecond / ToneHz; s16 *SampleOut = SoundBuffer->Samples; for (int SampleIndex = 0; SampleIndex < SoundBuffer->SampleCount; ++SampleIndex) {
f32 SineValue = sinf(GameState->tSine);
s16 SampleValue = (s16)(SineValue * ToneVolume); *SampleOut++ = SampleValue; *SampleOut++ = SampleValue;
GameState->tSine += 2.0f * Pi32 * 1.0f / (f32)WavePeriod; if (GameState->tSine > 2.0f * Pi32) { GameState->tSine -= 2.0f * Pi32; }
} } // ... GAME_GET_SOUND_SAMPLES(GameGetSoundSamples) { game_state *GameState = (game_state*)Memory->PermanentStorage
GameOutputSound(GameState, SoundBuffer, GameState->ToneHz);
}
 Listing 26: [handmade.cpp] Using tSine stored in the game state.

Once the code is recompiled, you'll notice that the sound bug has gone away. Modern computers are fast (and our code is small)!

(Continue to Subsection 4.2)

   

Navigation

Previous: Day 20. Debugging the Audio Sync

Up Next: Day 21. Instantaneous Live Code Editing

Back to Index

Glossary

MSDN

CopyFile

/EXPORT linker switch

/LD compiler switch

FreeLibrary

Terms and definitions

Monolithic architecture

Name Mangling

formatted by Markdeep 1.10