R0KernelPath
module 018 / 262

The C Preprocessor & Macros

Master the C preprocessor from basic #define to kernel-grade macro idioms like container_of, X-macros, and compile-time assertions — the invisible layer that makes Linux portable across hundreds of architectures.

~12h focused study12 concepts

Overview

The C preprocessor is a text-transformation engine that runs before your compiler ever sees a single token of your code. It is not part of the C language — it is a separate, purely textual macro processor that was bolted onto C in the early 1970s and has since become one of the most powerful (and treacherous) tools in a systems programmer's arsenal.

Understanding the preprocessor deeply is not optional for kernel work. Browse any Linux kernel source file and you will find #include, #define, #ifdef, likely(), container_of(), BUILD_BUG_ON(), and dozens of other macro-driven constructs doing things that look almost magical: generating entire function families from a single template, performing compile-time assertions, encoding type safety without runtime cost, and conditionally compiling entire subsystems in or out of a build.

This module builds your mental model from the ground up. You will learn what the preprocessor actually does to your source text, why macros are powerful but dangerous, the classic pitfalls that have caused real security bugs, and finally the sophisticated idioms the kernel uses every day. By the end you will be able to read and write non-trivial kernel macros with confidence — and more importantly, you will understand why they are written the way they are.

One recurring theme: the preprocessor operates on text, not on types or values. This is both its strength (extreme flexibility) and its weakness (no type checking, no scoping, surprising expansion rules). Keeping this distinction sharp in your mind will save you from most macro bugs.

why this matters for the kernel

The Linux kernel cannot be compiled without thousands of macro definitions working correctly. Macros are the kernel's primary tool for zero-cost abstraction: they provide type-generic algorithms, compile-time safety checks, hardware-specific code paths, and highly readable APIs — all without the overhead of function calls or the limitations of C's type system. When you read list_for_each_entry(), READ_ONCE(), WARN_ON(), or MODULE_LICENSE(), you are reading macros. Understanding them is not a prerequisite for "someday kernel hacking" — it is prerequisite for reading the very first kernel source file you open.

Beyond the kernel, the preprocessor is the foundation of build systems, feature-flag architectures, cross-platform compatibility layers, and embedded firmware. The patterns you learn here — X-macros for dispatch tables, include guards for modular builds, token pasting for name generation — appear in every serious C/C++ codebase. Mastering this layer makes you a far more effective contributor to any large C project, and it is the gateway to understanding how the kernel's extraordinary portability across hundreds of architectures is actually achieved.

C Preprocessor Translation Pipeline

rendering diagram…

The preprocessor pipeline transforms raw source text through several distinct phases before the C compiler proper ever sees a token. Note that #include triggers recursive re-entry into the same pipeline, and macro expansion involves a rescan loop that continues until no more macros can be expanded.

The concepts

01What the Preprocessor Actually Does

The C build pipeline has four logical stages: preprocessing, compilation, assembly, and linking. The preprocessor is stage one. It takes your .c file as raw text and produces a new, expanded text file (a translation unit) that the compiler proper then parses.

The preprocessor knows nothing about C types, variable scopes, function signatures, or operator precedence. It sees a stream of tokens — sequences of characters separated by whitespace — and applies a set of text-substitution rules you have defined with # directives.

You can see exactly what the preprocessor produces with gcc -E:

gcc -E myfile.c -o myfile.i

The output is pure C with all macros expanded, all #include files inlined, all conditional blocks resolved. Looking at .i files is the single most useful debugging technique for macro problems.

Key preprocessing operations, in order:

  1. Trigraph replacement (archaic, usually disabled)
  2. Line splicing: backslash-newline pairs are joined into one logical line
  3. Tokenization: the source is split into preprocessing tokens
  4. Directive execution: #include, #define, #if, etc. are processed
  5. Macro expansion: defined identifiers are replaced with their expansions
  6. _Pragma processing
  7. Concatenation of adjacent string literals

The output feeds directly into the C parser, which knows nothing about where things came from.

/* See preprocessing in action */
#define ANSWER 42
#define GREET(name) "Hello, " name "!"

int x = ANSWER;          /* becomes: int x = 42; */
const char *s = GREET("world");  /* becomes: const char *s = "Hello, " "world" "!"; */
/* Adjacent string literals are then merged by the compiler: "Hello, world!" */

02Object-Like Macros: #define for Constants

The simplest use of #define creates an object-like macro — a name that expands to a token sequence every time the preprocessor sees it.

#define MAX_CPUS 512
#define PAGE_SHIFT 12
#define PAGE_SIZE (1UL << PAGE_SHIFT)

Why use macros instead of const int? Several reasons that matter in kernel code:

  1. No storage allocated: a const int lives in memory (or a register); a macro is pure text and disappears after preprocessing.
  2. Usable in any constant-expression context: array sizes, switch case labels, #if conditions. A const variable in C (unlike C++) cannot be used as an array size in standard C89/C90.
  3. Cross-file sharing without linker symbols: a #define in a header costs nothing.

However, macros have no type. #define MAX_CPUS 512 creates no int, no unsigned, nothing. This is why you see patterns like:

#define MAX_CPUS 512U   /* hint: unsigned */

Or in the kernel, typed enum constants or static const variables are preferred where type safety matters.

Always parenthesize numeric macro bodies when they contain expressions:

#define WRONG  1 + 2
#define RIGHT  (1 + 2)

int a = WRONG * 3;   /* expands to: 1 + 2 * 3 = 7, not 9! */
int b = RIGHT * 3;   /* expands to: (1 + 2) * 3 = 9 */

This is one of the most classic C bugs. The kernel style guide mandates parenthesization.

/* kernel-style constant macros */
#define HZ          1000
#define MSEC_PER_SEC    1000L
#define USEC_PER_MSEC   1000L
#define NSEC_PER_USEC   1000L
#define NSEC_PER_MSEC   1000000L
#define USEC_PER_SEC    1000000L
#define NSEC_PER_SEC    1000000000L

/* Always wrap multi-token bodies in parens */
#define ALIGN(x, a)     (((x) + (a) - 1) & ~((a) - 1))
/* Usage: ALIGN(13, 4) -> (13+3) & ~3 -> 16 & 0xFFFFFFFC -> 16 */

03Function-Like Macros: Power and Peril

A function-like macro takes arguments, just like a function call. The preprocessor replaces each argument occurrence in the body with the token sequence passed by the caller.

#define MAX(a, b)  ((a) > (b) ? (a) : (b))

This looks harmless and is incredibly useful — it works on any type without generics. But it has a lethal flaw: double evaluation.

Because a and b are text substituted, not values passed, if you call:

int x = MAX(i++, j++);

This expands to:

int x = ((i++) > (j++) ? (i++) : (j++));

One of i or j is incremented twice. This is undefined behavior and a real source of bugs. The rule: never pass expressions with side effects to function-like macros.

The fix: modern GCC/Clang offer statement expressions (a GNU extension used heavily in the kernel):

#define MAX(a, b) ({		\
    typeof(a) _a = (a);		\
    typeof(b) _b = (b);		\
    _a > _b ? _a : _b;		\
})

Now each argument is evaluated exactly once into a local variable. typeof() is a GCC extension that captures the type of an expression.

Parenthesization rule for arguments: every use of a macro parameter in the body must be wrapped in parentheses, because the caller might pass an expression:

#define DOUBLE(x)  x + x      /* WRONG */
#define DOUBLE(x)  ((x) + (x)) /* right (still double-eval, but at least arithmetic is correct) */

int a = DOUBLE(3) * 2;  /* WRONG: 3 + 3 * 2 = 9, not 12 */
/* Safe type-generic min using GNU statement expression + typeof */
#define min(x, y) ({                \
    typeof(x) _min1 = (x);          \
    typeof(y) _min2 = (y);          \
    (void)(&_min1 == &_min2);       \
    _min1 < _min2 ? _min1 : _min2; \
})
/* The (void)(&_min1 == &_min2) line is a type-safety check:
   it causes a compiler WARNING if x and y have different types,
   because you cannot compare pointers to different types. */

04Conditional Compilation: #if, #ifdef, #ifndef, #else, #elif, #endif

Conditional compilation lets you include or exclude entire blocks of code based on preprocessor conditions. This is how a single Linux kernel source tree compiles correctly on ARM, x86, RISC-V, PowerPC, and MIPS — each architecture conditionally compiles only the code relevant to it.

#ifdef CONFIG_SMP
    /* multi-processor code */
#else
    /* single-processor code */
#endif

#ifdef NAME is true if NAME has been defined (even as empty). #ifndef NAME is true if it has not been defined.

#if EXPR evaluates a constant integer expression (macros are expanded first). You can use defined(NAME) inside #if for more complex conditions:

#if defined(CONFIG_X86_64) && !defined(CONFIG_PARAVIRT)
    /* native 64-bit x86, no hypervisor */
#endif

#elif chains conditions:

#if LINUX_VERSION_CODE >= KERNEL_VERSION(5, 10, 0)
    /* new API */
#elif LINUX_VERSION_CODE >= KERNEL_VERSION(4, 14, 0)
    /* old API */
#else
    #error "Kernel too old, minimum 4.14"
#endif

The #error directive halts compilation with a message. #warning emits a warning without stopping.

Practical rule: keep #ifdef blocks small and isolated. Large swaths of #ifdef-guarded code become unmaintainable. The kernel style guide recommends moving architecture-specific code into separate files selected by the build system (Kconfig/Makefiles) rather than littering .c files with #ifdef.

/* Portable integer type selection without stdint.h */
#if defined(__LP64__) || defined(_LP64)
    typedef unsigned long   u64;
    typedef long            s64;
#elif defined(_WIN64)
    typedef unsigned long long u64;
    typedef long long          s64;
#else
    #error "Cannot determine 64-bit type for this platform"
#endif

/* Compile-time feature detection */
#ifdef __has_builtin
#  if __has_builtin(__builtin_expect)
#    define likely(x)   __builtin_expect(!!(x), 1)
#    define unlikely(x) __builtin_expect(!!(x), 0)
#  endif
#endif
#ifndef likely
#  define likely(x)   (x)
#  define unlikely(x) (x)
#endif

05Include Guards and #pragma once

Headers in C are textually included. If a.h includes b.h, and main.c includes both a.h and b.h, then b.h's contents appear twice in the translation unit — causing duplicate definitions and compile errors.

The canonical solution is the include guard:

#ifndef MYHEADER_H
#define MYHEADER_H
/* ... all your declarations ... */
#endif /* MYHEADER_H */

The first time the file is included, MYHEADER_H is not defined, so the block is entered and the symbol is defined. The second time, MYHEADER_H is already defined, so the entire body is skipped.

Naming convention: use the file path mangled to uppercase with underscores: linux/list.h → _LINUX_LIST_H. The kernel uses a leading underscore: #ifndef _LINUX_LIST_H.

An alternative is #pragma once, a widely-supported but non-standard directive that tells the compiler to include a file only once:

#pragma once
/* ... */

It is simpler and immune to copy-paste name-collision bugs, but the Linux kernel uses traditional include guards for maximum portability and standards compliance. You will see #pragma once in many other large C/C++ projects.

The subtle bug: if you copy a header file and forget to change the guard symbol, both files will have the same guard name, and one of them will always be silently ignored. This is a real, hard-to-debug problem in large codebases. Always generate guard names from the full file path.

/* linux/types.h style include guard */
#ifndef _LINUX_TYPES_H
#define _LINUX_TYPES_H

#include <uapi/linux/types.h>

#ifndef __ASSEMBLY__

typedef __u32  __kernel_dev_t;
/* ... many more typedefs ... */

#endif /* __ASSEMBLY__ */

#endif /* _LINUX_TYPES_H */
/* Note the nested guard: __ASSEMBLY__ lets this header be safely
   included from assembly (.S) files, where C typedefs are illegal. */

06Stringizing (#) and Token Pasting (##)

The preprocessor has two special operators that work inside macro bodies:

# (stringizing): converts a macro argument to a string literal.

#define STRINGIFY(x)  #x
STRINGIFY(hello world)  // → "hello world"
STRINGIFY(1 + 2)        // → "1 + 2"

Critical: #x stringizes the unexpanded token sequence. To stringize after expansion, use the double-macro trick:

#define STR(x)   #x
#define XSTR(x)  STR(x)   /* expand x first, then stringize */

#define VERSION  42
STR(VERSION)   // → "VERSION"  (unexpanded!)
XSTR(VERSION)  // → "42"       (expanded first)

This is used in the kernel for BUILD_BUG_ON error messages, module version strings, and logging.

## (token pasting / concatenation): glues two tokens together into one.

#define CONCAT(a, b)  a##b
CONCAT(foo, bar)   // → foobar  (a single identifier)
CONCAT(x, 1)       // → x1

Token pasting is used extensively to generate unique identifiers and function names:

#define DEFINE_MUTEX(name)  struct mutex mutex_##name
DEFINE_MUTEX(io);  // → struct mutex mutex_io;

In the Linux kernel, ## powers the entire device driver model's registration macros, the per-CPU variable infrastructure, and the container_of type-safety trick.

/* Classic double-expansion pattern for XSTR */
#define STR(x)    #x
#define XSTR(x)   STR(x)
#define KVER_MAJ  6
#define KVER_MIN  8
#define KVER_STR  XSTR(KVER_MAJ) "." XSTR(KVER_MIN)
/* KVER_STR → "6" "." "8" → merged by compiler to "6.8" */

/* Token pasting to generate typed read/write accessors */
#define DEFINE_REG_ACCESSORS(reg, offset)           \
    static inline u32 read_##reg(void __iomem *base) \
    { return readl(base + (offset)); }               \
    static inline void write_##reg(void __iomem *base, u32 val) \
    { writel(val, base + (offset)); }

DEFINE_REG_ACCESSORS(control, 0x00)
DEFINE_REG_ACCESSORS(status,  0x04)
/* Generates: read_control(), write_control(), read_status(), write_status() */

07The do { ... } while (0) Idiom

A classic mistake is writing a multi-statement function-like macro like this:

#define SWAP(a, b)  tmp = a; a = b; b = tmp;  /* WRONG */

If used in an if without braces:

if (cond)
    SWAP(x, y);

This expands to:

if (cond)
    tmp = x;    /* inside if */
x = y;          /* ALWAYS executes! */
y = tmp;        /* ALWAYS executes! */

The standard fix is wrapping in do { ... } while (0):

#define SWAP(a, b)  do {    \
    typeof(a) _t = (a);     \
    (a) = (b);              \
    (b) = _t;               \
} while (0)

Now the entire macro is a single statement, so it works correctly in every context:

  • if (cond) SWAP(x, y); — correct
  • if (cond) SWAP(x, y); else ... — correct (the semicolon after while(0) closes the statement)
  • Standalone with semicolon — correct

The do { } while (0) compiles to zero extra instructions — the optimizer removes the dead loop immediately. It is purely a syntactic device.

This idiom appears everywhere in the Linux kernel. Any multi-statement macro that is not a pure expression should use it. The kernel's WARN_ON, BUG_ON, list_del, and thousands of others all use this pattern.

/* Without do-while: broken in if-else */
#define BAD_DBG(fmt, ...)   \
    printk(fmt, ##__VA_ARGS__);  \
    log_count++;

/* With do-while: always correct */
#define DBG(fmt, ...)   do {            \
    printk(fmt, ##__VA_ARGS__);         \
    log_count++;                        \
} while (0)

/* Proof it generates no overhead — gcc -O2 output is identical
   to a plain { } block; the while(0) condition is a compile-time
   false and the loop is eliminated entirely. */

/* Kernel example: include/linux/kernel.h */
#define pr_debug(fmt, ...)          \
    do {                            \
        if (unlikely(debug_level))  \
            printk(KERN_DEBUG fmt, ##__VA_ARGS__); \
    } while (0)

08Variadic Macros: __VA_ARGS__

C99 introduced variadic macros, which accept a variable number of arguments using ... in the parameter list and __VA_ARGS__ in the body.

#define LOG(fmt, ...)  printf("[LOG] " fmt, __VA_ARGS__)
LOG("x=%d y=%d", x, y);  // → printf("[LOG] x=%d y=%d", x, y);

The empty __VA_ARGS__ problem: If you call LOG("hello") with no extra arguments, the expansion becomes printf("[LOG] hello",) — a trailing comma, which is a syntax error.

GCC/Clang solve this with the ##__VA_ARGS__ extension: if __VA_ARGS__ is empty, the ## also removes the preceding comma:

#define LOG(fmt, ...)  printf("[LOG] " fmt, ##__VA_ARGS__)
LOG("hello");   // → printf("[LOG] hello")  — comma removed
LOG("x=%d", x); // → printf("[LOG] x=%d", x)

C23 standardizes this as __VA_OPT__:

#define LOG(fmt, ...)  printf("[LOG] " fmt __VA_OPT__(,) __VA_ARGS__)

Variadic macros are used throughout the kernel for its logging infrastructure (pr_info, pr_err, dev_warn, etc.), tracepoints, and assertion macros. The pattern ##__VA_ARGS__ is ubiquitous in kernel headers.

/* Kernel-style logging macros with level prefix */
#define pr_fmt(fmt)  "mydriver: " fmt

#define pr_info(fmt, ...)   \
    printk(KERN_INFO pr_fmt(fmt), ##__VA_ARGS__)

#define pr_err(fmt, ...)    \
    printk(KERN_ERR  pr_fmt(fmt), ##__VA_ARGS__)

/* Usage */
pr_info("probe: found device at 0x%lx\n", base_addr);
pr_err("failed to allocate DMA buffer\n");

/* Expands to:
   printk(KERN_INFO "mydriver: probe: found device at 0x%lx\n", base_addr);
   printk(KERN_ERR  "mydriver: failed to allocate DMA buffer\n"); */

09X-Macros: Data-Driven Code Generation

The X-macro pattern is one of the most powerful and elegant techniques in advanced C. It solves a common maintenance problem: you have a list of items (error codes, states, opcodes, hardware registers) and need to express that list in multiple ways — as an enum, as a string array, as a dispatch table — while keeping all of them in sync.

The idea: define your data in one place using a macro that applies an operation to each item. That operation (conventionally called X) is redefined before each use.

/* Define the data once */
#define COLORS  \
    X(RED,   0xFF0000)  \
    X(GREEN, 0x00FF00)  \
    X(BLUE,  0x0000FF)  \

/* Use 1: generate enum */
#define X(name, hex)  name,
typedef enum { COLORS } Color;
#undef X

/* Use 2: generate string name array */
#define X(name, hex)  [name] = #name,
static const char *color_names[] = { COLORS };
#undef X

/* Use 3: generate hex value array */
#define X(name, hex)  [name] = hex,
static const unsigned color_values[] = { COLORS };
#undef X

Now color_names[RED] is "RED" and color_values[RED] is 0xFF0000, and both stay in sync automatically when you add a new color to the COLORS list.

In the Linux kernel, this pattern appears in:

  • arch/x86/include/asm/syscalls.h: system call tables
  • include/linux/errno.h style error code tables in some drivers
  • drivers/*/: opcode/command tables
  • Hardware register maps

This is genuinely elegant engineering: you have achieved a form of metaprogramming — code that writes code — using nothing but the C preprocessor.

/* X-macro for a simple state machine */
#define STATES          \
    X(IDLE,    "idle")  \
    X(RUNNING, "running") \
    X(PAUSED,  "paused") \
    X(DONE,    "done")

/* Generate enum */
typedef enum {
#define X(state, str)  STATE_##state,
    STATES
#undef X
    STATE_COUNT
} State;

/* Generate name lookup table (stays in sync automatically) */
static const char *state_names[STATE_COUNT] = {
#define X(state, str)  [STATE_##state] = str,
    STATES
#undef X
};

/* Now: state_names[STATE_RUNNING] == "running"
   Adding a new state: add one line to STATES, nothing else. */

10Kernel Macro Idioms: container_of, BUILD_BUG_ON, likely/unlikely

The Linux kernel contains some of the most sophisticated macro engineering in existence. Three idioms are essential to understand:

container_of(ptr, type, member): Given a pointer to a struct member, compute the pointer to the containing struct. This is the foundation of the kernel's generic linked list implementation.

#define container_of(ptr, type, member) ({          \
    const typeof(((type *)0)->member) *__mptr = (ptr); \
    (type *)((char *)__mptr - offsetof(type, member)); \
})

Analogy: you found someone's left shoe. container_of tells you which person it belongs to, by measuring how far the left shoe is from the top of a person (offsetof) and walking backwards from the shoe pointer.

The typeof(((type *)0)->member) line is a compile-time type check: if ptr has the wrong type for member, you get a compiler warning.

BUILD_BUG_ON(condition): a compile-time assertion that fails the build if condition is true. Used to catch impossible-to-test-at-runtime invariants:

BUILD_BUG_ON(sizeof(struct my_header) != 64);
BUILD_BUG_ON(PAGE_SIZE < sizeof(struct critical_data));

In older kernels this was implemented via negative-size array trick; modern kernels use _Static_assert or a GCC built-in.

likely(x) / unlikely(x): hints to the CPU branch predictor via __builtin_expect:

#define likely(x)   __builtin_expect(!!(x), 1)
#define unlikely(x) __builtin_expect(!!(x), 0)

The !! double-negation normalizes any value to 0 or 1 (boolean). __builtin_expect(expr, expected) tells GCC the branch is likely to take the expected value, arranging generated machine code so the likely path has no branch penalty. In tight kernel paths, this measurably improves performance.

/* container_of in action: kernel linked list */
struct task_struct {
    pid_t pid;
    struct list_head tasks;  /* embedded list node */
    /* ... many more fields ... */
};

/* Given a pointer to the 'tasks' list_head inside a task_struct,
   recover the task_struct pointer: */
struct list_head *node = get_some_list_node();
struct task_struct *task = container_of(node, struct task_struct, tasks);

/* What this computes:
   (char *)node  - offsetof(struct task_struct, tasks)
   = start address of the task_struct
   Cast to (struct task_struct *) and you have the full struct. */

/* BUILD_BUG_ON: catch struct layout assumptions at compile time */
static int __init mydriver_init(void)
{
    BUILD_BUG_ON(sizeof(struct dma_descriptor) % 64 != 0);
    /* ... */
    return 0;
}

11Macro Hygiene and Common Pitfalls

Macros are not functions. Forgetting this causes subtle, hard-to-debug problems. Here is the complete catalog of classic pitfalls:

1. Operator precedence surprises (missing parens around body or arguments) — covered earlier, worth repeating: wrap everything.

2. Double evaluation — covered earlier. Use typeof + local variables in GNU C.

3. Name shadowing: A macro's local variables (in statement expressions) can shadow variables in the caller's scope. Prefix them with _ or __ and use obscure names:

/* BAD: if caller has a variable named 'min', this breaks */
#define MIN(a,b) ({ int min = a; min < b ? min : b; })
/* GOOD: */
#define MIN(a,b) ({ typeof(a) _min1=(a); typeof(b) _min2=(b); _min1<_min2?_min1:_min2; })

4. Macros do not respect scope: there is no block scope, no namespace. A #define in effect at any point in a translation unit applies everywhere after it (until #undef).

5. Debugging is harder: gdb and stack traces refer to macro expansion locations, not the macro definition. -E output helps enormously. Compile with -g3 to include macro definitions in DWARF debug info.

6. Cannot take the address of a macro: you cannot write &MAX to get a function pointer. If you need a function pointer, write a real function.

7. Recursive macros do not expand recursively: the C standard prohibits recursive macro expansion. A macro name encountered during its own expansion is left unexpanded. This is usually the right behavior but can surprise you.

8. #undef a macro before redefining it: redefining a macro without #undef is only legal if the new definition is token-for-token identical. Otherwise it is undefined behavior (and GCC warns).

/* Pitfall demo: missing parens */
#define SQUARE(x)  x*x          /* WRONG */
int a = SQUARE(1+2);            /* expands: 1+2*1+2 = 5, not 9 */

#define SQUARE(x)  ((x)*(x))    /* WRONG: still double-eval */
int b = SQUARE(i++);            /* i incremented twice! */

/* Correct: GNU statement expression with typeof */
#define SQUARE(x) ({ typeof(x) _x=(x); _x*_x; })
int c = SQUARE(i++);            /* i incremented exactly once */

/* Pitfall: no do-while, breaks in if-else */
#define RESET(p)  (p)->val = 0; (p)->flags = 0;  /* WRONG */
if (cond) RESET(ptr);           /* second stmt always runs! */

#define RESET(p)  do { (p)->val=0; (p)->flags=0; } while(0)  /* correct */

12Predefined Macros and the Build System Interface

The preprocessor defines several standard macros automatically, and build systems inject many more via -D flags:

Standard predefined macros (always available):

  • __FILE__: current source file name as a string literal
  • __LINE__: current line number as an integer
  • __func__: current function name (C99, technically a variable not a macro)
  • __DATE__, __TIME__: compilation timestamp
  • __STDC__: 1 if compiling as standard C
  • __STDC_VERSION__: 199901L (C99), 201112L (C11), etc.
  • __GNUC__, __GNUC_MINOR__: GCC version (also defined by Clang for compatibility)

Compiler-injected macros:

  • __x86_64__, __aarch64__, __riscv: target architecture
  • __linux__, __APPLE__: target OS
  • __BYTE_ORDER__: endianness

Build system injection via -D:

gcc -DDEBUG=1 -DCONFIG_SMP -DKBUILD_MODNAME=\"mydriver\" myfile.c

This is exactly how the Linux Kbuild system works: make generates a include/generated/autoconf.h from your .config file, containing thousands of #define CONFIG_* lines. Every source file that includes <linux/config.h> gets all these flags.

For debugging, __FILE__ and __LINE__ together are invaluable:

#define ASSERT(cond)  do {                              \
    if (!(cond))                                        \
        panic("assert failed: %s, %s:%d\n",            \
              #cond, __FILE__, __LINE__);               \
} while (0)

This gives you the exact source location of any failed assertion — a pattern used throughout the kernel.

/* Build-system-controlled debug logging */
#ifdef DEBUG
#  define dpr(fmt, ...)  \
     printk(KERN_DEBUG "%s:%d " fmt, __FILE__, __LINE__, ##__VA_ARGS__)
#else
#  define dpr(fmt, ...)  do { } while (0)  /* compiles to nothing */
#endif

/* Version check macro using compiler-injected LINUX_VERSION_CODE */
#include <linux/version.h>
#if LINUX_VERSION_CODE < KERNEL_VERSION(5, 4, 0)
#  error "This driver requires Linux 5.4 or newer"
#endif

/* __COUNTER__: a GCC extension, increments each use, for unique names */
#define UNIQUE_ID   __COUNTER__
#define PASTE2(a,b) a##b
#define UNIQUE_VAR(prefix) PASTE2(prefix, __COUNTER__)

Key insights

  • ✦The preprocessor is a text-substitution engine, not a C-aware compiler. It operates before the C parser sees anything. Keeping this mental model sharp prevents almost all macro bugs.
  • ✦Every macro argument must be parenthesized in the body, and the entire body must be parenthesized. No exceptions. One missing pair of parens can silently change arithmetic results.
  • ✦Function-like macros evaluate their arguments at every point of use. Passing an expression with side effects (like i++) to a macro that uses the argument twice causes undefined behavior. The typeof + statement-expression pattern is the correct fix in GNU C.
  • ✦do { ... } while (0) is not a loop. It is a syntactic trick to make a multi-statement macro behave as a single statement in all contexts, including if-else without braces.
  • ✦X-macros are one of the most powerful patterns in C: define data once, generate multiple consistent representations by redefining the X operation. This is genuine metaprogramming.
  • ✦container_of() is the heart of Linux's generic data structures. It lets you embed list nodes inside structs and recover the containing struct pointer — enabling type-safe, allocation-free intrusive linked lists.
  • ✦likely() and unlikely() are not just comments — they generate different machine code layouts that keep the fast path branch-prediction-friendly, measurably improving performance in hot paths.
  • ✦gcc -E is your best friend for macro debugging. Always look at the preprocessed output when a macro behaves unexpectedly.

Common pitfalls

  • ⚠Missing parentheses around a macro body or its arguments — the most common macro bug. Always wrap: #define FOO(x) ((x) + 1), never #define FOO(x) x + 1.
  • ⚠Passing side-effecting expressions (i++, func()) to macros that use the argument more than once, causing double evaluation and undefined behavior.
  • ⚠Writing a multi-statement macro without do { } while (0), which silently breaks when used as the body of an if statement without braces.
  • ⚠Forgetting to #undef a macro before redefining it, which is undefined behavior unless the new definition is token-for-token identical.
  • ⚠Copy-pasting a header file and forgetting to change the include guard symbol, causing one of the two files to always be silently skipped.
  • ⚠Using STRINGIFY(MACRO_NAME) instead of the double-expansion XSTR(MACRO_NAME) pattern, getting the literal text 'MACRO_NAME' instead of its expanded value.
  • ⚠Expecting macros to have function scope or namespacing — they do not. A #define is global to the translation unit from its point of definition. Name collisions across headers are a real problem.
  • ⚠Attempting to take the address of a function-like macro, or use it as a function pointer. Macros are not functions; if you need a pointer, write a static inline function.

Practice

  1. 01
    easy

    Write a header file 'vec2.h' for a 2D vector type. Define object-like macros for common constants (VEC2_ZERO, VEC2_UNIT_X, VEC2_UNIT_Y) and function-like macros for dot product and vector length squared. Use correct parenthesization, include guards, and the do-while(0) idiom where needed. Test it from a .c file and verify expansion with gcc -E.

  2. 02
    medium

    Implement a type-safe, side-effect-safe CLAMP(val, lo, hi) macro that constrains a value to [lo, hi]. It must work correctly on integers and floats, must evaluate each argument exactly once, and must generate a compiler warning if val, lo, and hi have different types. Use GNU extensions (typeof, statement expressions). Verify with a test that passes i++, j++, k++ as arguments.

  3. 03
    medium

    Build a complete X-macro-based error handling system: define a table of 6 error codes (each with a name, an integer value, and a human-readable description). Generate: an enum, a function err_name(int code) returning the name string, and a function err_desc(int code) returning the description. All three must update automatically when a new error is added to the table.

  4. 04
    medium

    Implement a simplified version of the kernel's container_of macro from scratch. Define a struct 'Node' embedded inside a struct 'Task' (alongside other fields). Write a function that receives a Node pointer and returns the containing Task pointer using your container_of. Verify that the pointer arithmetic is correct by checking that the returned Task pointer matches the original.

  5. 05
    hard

    Write a STATIC_ASSERT macro that works in C99 (no _Static_assert). It should produce a compile error with a meaningful name visible in the error message when the condition is false. Verify it correctly rejects sizeof(int) == 8 on a 32-bit platform (or any always-false condition you choose) and accepts sizeof(char) == 1.

Resources

📄GCC Preprocessor Manual
docsfree

The definitive reference for all preprocessor directives, macro expansion rules, and GNU extensions used throughout the kernel.

📄Linux Kernel Coding Style
docsfree

Chapter 12 covers macros specifically, including the do-while(0) rule and parenthesization requirements.

📕The C Programming Language, 2nd Edition
bookpaidBrian Kernighan & Dennis Ritchie — Prentice Hall. Chapter 4.11 covers the preprocessor from the language designers themselves.

Still the most precise and concise treatment of C fundamentals; the preprocessor chapter is a must-read.

🔧Compiler Explorer (Godbolt)
toolfree

Paste a macro and see generated assembly instantly. Use the 'Preprocessor output' option to see expanded macros.

✎Linux kernel include/linux/kernel.h
articlefree

Bootlin's cross-referenced kernel source — read container_of, min, max, ARRAY_SIZE, and BUILD_BUG_ON in their natural habitat.

✎Beej's Guide to C Programming — Preprocessor chapter
articlefree

Friendly, thorough walkthrough of all preprocessor features with good examples; excellent for beginners.

📕C Interfaces and Implementations
bookpaidDavid R. Hanson — Addison-Wesley. Demonstrates advanced macro techniques for building type-generic data structures in C.

Shows X-macros and code generation patterns in real, production-quality library code.

✎LWN: Kernel development articles on macros
articlefree

LWN has covered container_of, BUILD_BUG_ON, and other macro innovations as they were added to the kernel. Primary sources.

Checkpoints — you can now…

  • ✓You can explain exactly what 'gcc -E' does and use it to debug any macro that behaves unexpectedly.
  • ✓You can write a function-like macro that evaluates each argument exactly once using typeof and GNU statement expressions.
  • ✓You can write a multi-statement macro that works correctly in all contexts (if, if-else, standalone) using do { } while (0).
  • ✓You can implement include guards correctly, explain why they work, and identify the copy-paste bug that breaks them.
  • ✓You can use the # and ## operators to generate string literals and unique identifiers from macro arguments, including the double-expansion trick for XSTR.
  • ✓You can design and implement an X-macro table for a real use case, generating at least two consistent data structures from one source-of-truth definition.
  • ✓You can read and explain container_of(), including what the offsetof computation does and why the typeof line provides type safety.
  • ✓You can list at least five classic macro pitfalls and describe the canonical fix for each one.