Joakim Skog Lundell

Game from Scratch 001 - Choosing C

Language Choice

In my previous post I talked about having to make a decision on what language to pick between C++ and Odin. In the end, I went with something else entirely: C. The main thing I don’t like with the language I use at work, C#, is that it’s grown bloated. It’s starting to feel messy and it seems like Microsoft no longer knows where they want to take it. That’s also one of the main arguments against C++ as a language, that it’s too messy and has too many features.

What I like about C even though I haven’t used it since university is that it’s a really small language. It’s easy to read because it doesn’t have ten different ways of iterating over a collection for example. It’s low-level so you have a lot of control. It also has lots of documentation, examples and tutorials that will help me when I get stuck. It might seem archaic to choose C over modern alternatives like Odin and Zig but I have a good feeling about this!

Escaping the C Runtime

One cool thing about C is the control it gives you. I found out while reading that you can actually avoid most or all of the C Runtime (CRT). The CRT is what runs before your main function is even called, more on that later. It’s also a bunch of functions that each platform must provide. Things like malloc, printf and standard math functions are included too.

So why skip it? Why ditch those handy cross-platform utilities? Why go through all this extra work? I’ve seen some really experienced developers complain about the quality of the CRT but I can’t really speak to that because I don’t have enough experience with it. Honestly, I see it as a challenge and a way to learn. I also hope to gain three specific things by not using the CRT:

1. Smaller binaries

By avoiding the C Runtime I can reduce the size of my final binary by a lot (yes, I know about dynamic linking but play along). CRT version: 101KB. My version: 4KB. That’s a huge difference! These days, with fast internet a smaller binary size is not that important but I like to believe that people appreciate it. It’s also a fun challenge to keep the binary size small.

2. Startup speed

I wrote earlier that your main function is not the first thing that gets called when your program starts. The CRT runs first to set things up and parse arguments before it even calls your main function. By avoiding all of that I should, in theory, get a much faster startup which will hopefully be noticeable.

3. Full control

What I lose in convenience I gain in control. I won’t be able to use things such as malloc to get a platform independent way of allocating memory. That’s no issue for me though since I intend to write platform specific code and utilize the power of each platform. A good replacement for malloc would be VirtualAlloc on Windows. I’m hoping to separate all game code from the platform specific implementations so this should not be a problem for me.

How

I won’t go into every low-level detail, there are great resources linked below, but here are a few highlights that might surprise you.

Normally main() or WinMain() is the entry point to your application but with the windows subsystem and no CRT you should use this instead.

1
2
3
4
void WinMainCRTStartup() 
{
    ExitProcess(0);
}

If you use floating-point operations in your code, it’s expected that the global variable _fltused is initialized and the CRT does that for you. Without the CRT, we have to do it ourselves. Let’s do it and be on our way!

1
2
3
4
5
6
void WinMainCRTStartup() 
{
    ExitProcess(0);
}

int _fltused = 0x9875; //The value is not important but I just copied it from the CRT

MSVC may emit calls to memset and memcpy when initializing structs or arrays. Normally, the CRT handles these. In our case? You guessed it: DIY!

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
void WinMainCRTStartup() 
{
    ExitProcess(0);
}

int _fltused = 0x9875; //The value is not important but I just copied it from the CRT

#pragma function(memset)
void *memset(void *destination, int value, size_t size)
{
    unsigned char *dest = (unsigned char *)destination;
    while(size--)
    {
        *dest++ = (unsigned char)value;
    }

    return destination;
}

#pragma function(memcpy)
void *memcpy(void *destination, void const *source, size_t size)
{
    unsigned char *src = (unsigned char *)source;
    unsigned char *dest = (unsigned char *)destination;
    while(size--)
    {
        *dest++ = *src++;
    }

    return destination;
}

I also use some compiler flags to skip CRT-dependent features like buffer checks, but more on that later.

References

I want to highlight two great guides that helped me escape the C Runtime. They are really easy to follow so please take a look if you’re interested!

Build.bat

I’m not using an IDE, as can be seen in my previous post so I need a way to build my code. I didn’t want anything overly complex like CMake. Eventually, I found the idea of a single script file used for building an entire project. I wrote my own version and it has honestly been great. Just 20 lines of batch script, building both debug and release versions every time. Compile times are under a second, so there’s no reason not to build both versions.

Here’s my entire build script. Finding out what each flag does is left as an exercise to the reader.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
set input_files=         ..\..\src\game.c
set output_file=         game.exe
set output_dir_debug=    build\debug
set output_dir_release=  build\release

set cl_flags=            /nologo /W3 /WX /FC /Z7 /GS- /Gs999999
set cl_link=             /link /incremental:no /opt:icf /opt:ref /subsystem:windows

if not exist %output_dir_debug%  mkdir %output_dir_debug%
if not exist %output_dir_release% mkdir %output_dir_release%

echo [building debug...]	
pushd %output_dir_debug%
call cl /Od %cl_flags% /Fe: %output_file% %input_files% %cl_link%
popd

echo [building release...]
pushd %output_dir_release%
call cl /O2 %cl_flags% /Fe: %output_file% %input_files% %cl_link%
popd

Next up

Next, I’ll create a window and handle the Windows message loop. With that, we’ll move into some more exciting C code.

Should be fun!