Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 8

Understanding C Project Compilation

Recommended resources

Hello! Welcome to the final lesson of our first module on C programming.

In our last few lessons, we've focused on organizing a C project into multiple source and header files. As you probably noticed, compiling a project with main.c, config.c, and potentially many others by manually typing gcc commands can quickly become repetitive and error-prone.

This lesson bridges the gap between writing C code and creating a final, runnable program efficiently. We'll demystify what happens when you hit "compile."

Today, we'll cover the roles of a compiler, a linker, and a build system. Understanding this toolchain is a cornerstone of professional C/C++ development and is essential for managing a project as complex as a game engine.

From Source Code to Executable: The Toolchain

When you run a command like gcc main.c -o my_game, it seems like a single action. In reality, gcc acts as a driver for a sequence of tools, often called a "toolchain," that work together. This process typically involves four main stages.

Let's start with a high-level overview of these stages.

Compiling C programs with Multiple Files

The video 'Compiling C programs with Multiple Files' by Jacob Sorber provides a clear, concise breakdown of the C compilation process. This first segment introduces the key stages.

Watch the section from 00:55 to 01:34. Focus on identifying the four main stages: preprocessor, compiler, assembler, and linker.

Now, let's examine each of those stages in more detail.

GCC Compilation Process Flowchart
This diagram illustrates the four stages of transforming C source code into an executable file. It shows the input and output for each stage: Preprocessing (`.c` to `.i`), Compilation (`.i` to `.s`), Assembly (`.s` to `.o`), and Linking (`.o` to an executable).

This excellent diagram is from the resource "GCC and Make - A Tutorial...". We will be referring to it as we break down the process.

GCC and Make - A Tutorial...

The article 'GCC and Make - A Tutorial...' by ntuhomer provides a detailed explanation of the compilation process that complements the diagram above.

Read section 1.7, 'GCC Compilation Process'. This section walks through each of the four steps shown in the diagram, explaining what happens at each stage and the commands you could use to perform each step individually.

Let's summarize those stages:

  1. Preprocessing: The preprocessor (cpp) handles directives that start with #. It replaces #include <stdio.h> with the actual content of the stdio.h header file and expands any macros defined with #define. The output is a single, large C source file (with a .i extension).
  2. Compilation: The compiler proper (gcc) takes the preprocessed .i file and translates it into assembly language (.s). Assembly is a low-level, human-readable language that corresponds directly to the CPU's instruction set.
  3. Assembly: The assembler (as) takes the assembly code and translates it into machine code—the raw binary 0s and 1s that the CPU executes. This output is stored in an object file (with a .o extension).

An object file contains the machine code for only one source file. It's not yet a complete program. For example, if your main.c calls a function from player.c, the main.o object file will have a "hole" or a placeholder where the player function's code should be. It also has a hole for the printf function, since that code lives in the C standard library, not your source file.

This is where the linker comes in.


The Role of the Linker

The linker (ld) has one of the most important jobs: it takes all the individual pieces and joins them into a single, complete executable file.

Its two primary responsibilities are:

  1. Combine object files: It merges all your project's object files (main.o, player.o, etc.) into one unit.
  2. Resolve symbols: It "fills in the holes" mentioned earlier. When it sees that main.o needs the player_update function, it finds the machine code for that function in player.o and connects the call. When it sees a call to printf, it finds the code for printf in an external library and links it into your program.

The following video gives a great visual explanation of this process.

Compiling, assembling, and linking

The video 'Compiling, assembling, and linking' by EngMicroLectures provides a slightly more technical look at each stage, with a particularly good explanation of the linker's role.

Watch from 00:33 to 05:51. This covers: Compilation to Assembly (0:33): See what the output looks like and notice the mention of 'relocation information' for printf - this is the 'hole' we discussed. Assembling to Object File (2:17): Understand that an object file is binary and still contains this relocation information. Linking (3:38): This is the key part. See how the linker combines multiple object files and libraries to resolve symbols like sub and printf and create the final executable.

A Note on Libraries

The linker works with two types of libraries:

  • Static Libraries (.a on Linux/macOS, .lib on Windows): The linker copies the necessary code from the library directly into your final executable. This makes your executable larger but self-contained.
  • Shared Libraries (.so on Linux, .dll on Windows): The linker leaves a reference in your executable. When you run the program, the operating system loads the shared library into memory so your program can use it. This results in smaller executables and saves memory, as multiple running programs can use the same shared library.

This concept will become very important when you start using external libraries for graphics (OpenGL), windowing (GLFW), and audio.


The Power of Separate Compilation

Now we can fully appreciate why we split our code into multiple files. It's not just for organization; it's for compilation efficiency.

For a small project, gcc main.c player.c -o my_game is fine. But a game engine might have hundreds of files. If you change one line in one file, do you want to recompile everything? No! That could take a very long time.

Instead, the professional workflow is:

  1. Compile each .c file that has changed into its corresponding .o object file.
  2. Link all the .o object files together to create the new executable.

Compiling a single file is fast. Linking is also relatively fast. This "separate compilation" model saves enormous amounts of development time.

The first video explains this benefit clearly.

Compiling C programs with Multiple Files

Let's return to Jacob Sorber's video, which explains the practical benefits of this separate compilation workflow.

Watch from 01:58 to 04:40. He explains why we compile C files into object files independently and then link them, focusing on organization and compilation speed for large projects.


The Role of a Build System

So, how do you manage this process? Do you have to manually check which files you changed and run gcc -c on them? This is where a build system comes in.

A build system automates the process of compiling and linking. You tell it what the final executable is, what source files it depends on, and it does the rest.

The most classic build system for C/C++ is Make. It uses a file named Makefile that defines the rules for building your project.

A Makefile consists of rules, each with three parts:

  • A target: The file to be built (e.g., game.exe or main.o).
  • Prerequisites (or dependencies): The files needed to build the target (e.g., main.o needs main.c).
  • A command: The shell command to run to build the target from its prerequisites (e.g., gcc -c main.c).

The make utility is smart. It looks at the modification times of the files. If a target is older than any of its prerequisites, make knows it's out-of-date and runs the command to rebuild it. If the target is already up-to-date, it does nothing.

GCC and Make - A Tutorial...

The 'GCC and Make' tutorial provides an excellent, simple example of a Makefile.

Read section 2.1, 'First Makefile By Example'. It shows you a simple Makefile and explains how targets, prerequisites, and commands work together. Don't worry about memorizing the syntax; focus on understanding the concept of how it automates the build process.

While Make is fundamental, modern C++ projects (including the game engine we're aiming for) often use more powerful, cross-platform build systems like CMake. CMake is a "meta" build system: it uses a script (CMakeLists.txt) to generate Makefiles for Linux/macOS or Visual Studio projects for Windows. We will explore CMake much later in the course, but it's built upon the same core principles of targets and dependencies that you see in Make.


Conclusion

Congratulations on completing the first module! You've built a solid foundation in C programming, from syntax and memory management to project organization. Today's lesson ties it all together, explaining the machinery that turns your code into a reality.

Key Takeaways:

  • The Compiler is part of a toolchain that transforms a single source file into an intermediate object file (.o), which contains machine code but isn't a complete program.
  • The Linker is the final stage that combines all your project's object files and necessary libraries to produce a single, executable file. It resolves references between files.
  • A Build System (like Make) automates the compilation and linking process. It understands the dependencies between your files and only recompiles what is necessary, saving significant time.

You now have all the C-specific knowledge needed to start building the foundational layers of our game engine.

In our next module, 3D Math Fundamentals, we will switch gears. We'll leave the intricacies of C behind for a while and focus on the mathematical concepts that are the heart of all 3D graphics: vectors and matrices. We'll begin by exploring 2D and 3D vectors and the essential operations you can perform on them.