The C Path — learn C, visually

🎩 The Preprocessor

#error, #warning, #pragma, #line — and a taste of #embed

⏱ 12 min · free interactive lesson · quizzes, visualizations & a real compiler

▶ Open the interactive lesson — free, no signup
Why you're learning this

Sometimes an install fails with one clear line — "requires a 64-bit system" — instead of pages of gibberish. That's a program refusing to build on purpose, with a message a human wrote, and #error is how you'll do the same. Along the way you'll learn how to make a struct match a file format or network message byte-for-byte, and how C23 finally lets you drop an image straight into a program with one line.

You've now met the two big preprocessor jobs: pasting (#include, #define) and choosing (#if). This lesson collects the remaining directives — the ones that talk to the compiler and to whoever is reading its output.

#error: refuse to compile

Sometimes the right move is to stop the build with a human-readable message. Paired with #if, it turns silent assumptions into loud requirements:

requirements.c
#include <limits.h>

#if !defined(__STDC_VERSION__) || __STDC_VERSION__ < 201112L
    #error "This project requires C11 or newer (try -std=c17)"
#endif

#if LONG_MAX < 9223372036854775807
    #error "This code assumes 64-bit long — 32-bit targets unsupported"
#endif

int main(void) { return 0; }
terminal
$ gcc -std=c17 requirements.c && echo OK
OK
$ gcc -std=c90 requirements.c
requirements.c:4:6: error: #error "This project requires C11 or newer (try -std=c17)"
# the build stops immediately, with YOUR message

Details worth knowing: with -std=c90, __STDC_VERSION__ isn't defined at all, so it evaluates to 0 and the check fires. And notice we tested LONG_MAX from <limits.h> rather than sizeof(long) — because the preprocessor cannot evaluate sizeof. It runs before the compiler, so it has no idea how big anything is; limit macros are the workaround. (Part 5's static_assert handles compile-time checks that do need real type knowledge.)

#warning "message" is the gentler sibling: it prints the message and compilation continues. It was a common extension for decades and became standard in C23. Classic use: #warning "TODO: replace this stub before release".

🧠 Checkpoint: Why check LONG_MAX instead of sizeof(long) in a #if?

  • LONG_MAX is faster to evaluate
  • sizeof is spelled differently in the preprocessor
  • The preprocessor runs before the compiler and cannot evaluate sizeof at all
  • sizeof(long) is undefined behavior
Show answer

The preprocessor runs before the compiler and cannot evaluate sizeof at all — Preprocessing is pure text-and-integer work with no type knowledge; sizeof in a #if is just an undefined identifier (= 0) followed by a syntax error. Limit macros from limits.h are the preprocessor-friendly mirror of type sizes.

#pragma: vendor-specific dials

#pragma is the standard's official escape hatch: implementation-defined instructions to the compiler, ignored if unrecognized. Three you'll actually meet:

1. #pragma once

The include-guard alternative from the #include lesson — first line of a header, done.

2. #pragma pack: squeezing struct padding

Compilers insert invisible padding bytes into structs so each member sits at its natural alignment (full story in Part 5). #pragma pack(1) tells the compiler: no padding, pack tight — essential when a struct must mirror a file format or network packet byte-for-byte:

pack.c
#include <stdio.h>

struct loose {            /* natural alignment (default) */
    char  tag;            /* 1 byte  + 3 padding         */
    int   value;          /* 4 bytes                     */
    short id;             /* 2 bytes + 2 tail padding    */
};

#pragma pack(push, 1)     /* save state, then pack tight */
struct tight {
    char  tag;
    int   value;
    short id;
};
#pragma pack(pop)         /* restore normal alignment    */

int main(void) {
    printf("loose: %zu bytes\n", sizeof(struct loose));
    printf("tight: %zu bytes\n", sizeof(struct tight));
    return 0;
}
terminal
$ gcc pack.c -o pack && ./pack
loose: 12 bytes
tight: 7 bytes
# 5 of the 12 bytes were invisible padding

This spot has an interactive memgrid widget — open the interactive lesson to play with it.

💀

Packed structs are not free: on some CPUs, misaligned loads are slow; on a few (and with vector instructions) they can crash outright. Worse, taking a pointer to a misaligned member and passing it around is undefined behavior territory. Pack only at the I/O boundary, then copy into normal structs.

🧠 Checkpoint: When is #pragma pack(1) genuinely the right tool?

  • Always — padding wastes memory
  • When a struct must match an on-disk or on-wire byte layout exactly
  • To speed up member access
  • To make sizeof portable across compilers
Show answer

When a struct must match an on-disk or on-wire byte layout exactly — Packing exists for I/O boundaries: file headers, network packets, memory-mapped hardware. Everywhere else, padding is your friend — aligned members are faster and pointer-safe.

3. #pragma GCC diagnostic: surgical warning control

diagnostic.c — scoped warning silence
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
    legacy_init(cfg);      /* we know, we know — scheduled for v3 */
#pragma GCC diagnostic pop
/* from here on, the warning is active again */

push saves the current warning state, ignored silences one warning, pop restores everything — so the exception stays scoped to the few lines that earn it, instead of nuking the warning project-wide with a compiler flag.

_Pragma: the operator form

#pragma has one fatal flaw: a macro can't expand into a directive — # lines aren't produced by expansion. C99 added the operator _Pragma("string"), which is usable in macros:

pragma-op.c — pragmas from macros
/* impossible with #pragma: macros cannot expand into '#' lines */
#define SILENCE_DEPRECATED \
    _Pragma("GCC diagnostic push") \
    _Pragma("GCC diagnostic ignored \"-Wdeprecated-declarations\"")
#define RESTORE_DIAGNOSTICS \
    _Pragma("GCC diagnostic pop")

SILENCE_DEPRECATED
/* ... legacy calls ... */
RESTORE_DIAGNOSTICS

#line: lying about where you are

#line 42 "original.src" sets what __LINE__ and __FILE__ report from that point on. Why would anyone want that? Code generators. Tools like bison/yacc, or anything that compiles another language into C, emit #line markers pointing back at the user's source — so when the generated C fails to compile, the error message points at parser.y:17 (the file the human wrote), not parser.tab.c:3841 (the machine-made noise). The linemarkers you saw in gcc -E output are the same mechanism.

lineliar.c
#include <stdio.h>

int main(void) {
    printf("really at %s:%d\n", __FILE__, __LINE__);
#line 500 "grammar.y"
    printf("now 'at' %s:%d\n", __FILE__, __LINE__);
    return 0;
}
terminal
$ gcc lineliar.c -o lineliar && ./lineliar
really at lineliar.c:4
now 'at' grammar.y:500
# compile errors after the #line would ALSO blame grammar.y

🧠 Checkpoint: Why do code-generating tools (bison, etc.) emit #line directives into the C they produce?

  • To make the generated file shorter
  • So compiler errors point at the file the human wrote, not the generated C
  • To speed up preprocessing
  • To renumber lines after macros expand
Show answer

So compiler errors point at the file the human wrote, not the generated C — A syntax error in generated C is almost always caused by the source the tool consumed. #line redirects __FILE__/__LINE__ — and therefore every diagnostic — back to that original file and line.

C23 teaser: #embed

For fifty years, shipping a binary asset (icon, font, firmware blob) inside a C program meant writing a script to convert it into a giant {0x89, 0x50, ...} initializer. C23 finally builds that in:

logo.c — C23
/* C23: the file's bytes become the initializer, at preprocess time */
static const unsigned char logo_png[] = {
#embed "logo.png"
};

/* before C23 you needed xxd -i logo.png > logo.h, or objcopy */

The preprocessor expands the file into a comma-separated list of byte values. Freshly landed in GCC 15 and Clang 19 — more C23 goodies await in Part 5.

Try it

This spot has an interactive editor widget — open the interactive lesson to play with it.

That's every directive in the language — the finale of this part puts them all to work designing clean multi-file projects.

▶ Practice this lesson interactively (with live gcc)