Getting started: using existing macros

Getting started: using existing macros#

Before writing our own macros, it helps to see what using them looks like. This chapter surveys macros that require no external dependencies: constants from the standard library and special macros provided by the compiler.

Constants#

The most basic use of macros is as constants:

 1#include <stdio.h>  // dprintf
 2#include <stdlib.h> // atoi, NULL, EXIT_*
 3#include <string.h> // strchr
 4#include <unistd.h> // *_FILENO
 5
 6/* Output the environment as a markdown table */
 7int main(int arg_count, char** arg_values, char** environment)
 8{
 9    char*    equal;
10    unsigned max = arg_count > 1 ? atoi(arg_values[1]) : 7;
11
12    dprintf(STDOUT_FILENO, "__Environment__:\n| Name | Value |\n|:-|:-|\n");
13    do
14    {
15        if ((equal = strchr(*environment, '=')) == NULL)
16        {
17            dprintf(STDERR_FILENO, "Expected an assignment but got \"%s\"\n", *environment);
18            return EXIT_FAILURE;
19        }
20        *equal = '\0';
21        dprintf(STDOUT_FILENO, "|`%s`|`%1.30s`|\n", *environment, equal + 1);
22    } while (--max > 0 && *++environment);
23    return EXIT_SUCCESS;
24}
 1#include <stdio.h>  // dprintf
 2#include <stdlib.h> // atoi, NULL, EXIT_*
 3#include <string.h> // strchr
 4#include <unistd.h> // *_FILENO
 5
 6/* Output the environment as a markdown table */
 7int main(int arg_count, char** arg_values, char** environment)
 8{
 9    char*    equal;
10    unsigned max = arg_count > 1 ? atoi(arg_values[1]) : 7;
11
12    dprintf(STDOUT_FILENO, "__Environment__:\n| Name | Value |\n|:-|:-|\n");
13    do
14    {
15        if ((equal = strchr(*environment, '=')) == NULL)
16        {
17            dprintf(STDERR_FILENO, "Expected an assignment but got \"%s\"\n", *environment);
18            return EXIT_FAILURE;
19        }
20        *equal = '\0';
21        dprintf(STDOUT_FILENO, "|`%s`|`%1.30s`|\n", *environment, equal + 1);
22    } while (--max > 0 && *++environment);
23    return EXIT_SUCCESS;
24}
/* Output the environment as a markdown table */
int main(int arg_count, char** arg_values, char** environment)
{
    char*    equal;
    unsigned max = arg_count > 1 ? atoi(arg_values[1]) : 7;

    dprintf(1 /* Standard output.  */, "__Environment__:\n| Name | Value |\n|:-|:-|\n");
    do
    {
        if ((equal = strchr(*environment, '=')) == ((void*)0))
        {
            dprintf(2 /* Standard error output.  */,
                    "Expected an assignment but got \"%s\"\n",
                    *environment);
            return 1 /* Failing exit status.  */;
        }
        *equal = '\0';
        dprintf(1 /* Standard output.  */, "|`%s`|`%1.30s`|\n", *environment, equal + 1);
    } while (--max > 0 && *++environment);
    return 0 /* Successful exit status.  */;
}

Environment:

Name

Value

GITHUB_STATE

/home/runner/work/_temp/_runne

DOTNET_NOLOGO

1

USER

runner

CI

true

GITHUB_ENV

/home/runner/work/_temp/_runne

PIPX_HOME

/opt/pipx

USE_BAZEL_FALLBACK_VERSION

silent:

As shown in the preprocessed tab, these constants are evaluated before compilation, and replaced by plain literals:

Macros used in the example

Macro

Expansion

STDOUT_FILENO

1

STDERR_FILENO

2

EXIT_FAILURE

1

EXIT_SUCCESS

0

NULL

((void*)0)

Why are macros used in this situation ?

Why wasn’t a constant used instead ? For instance, extern FILE *const stderr; is a constant, why wasn’t STDERR_FILENO defined as extern int const stderr_fileno; ?

An extern constant comes with several drawbacks compared to a macro:

  • Linkage: it must be defined in a .c file, compiled into a library, and linked against — adding a dependency for a single integer value.

  • Runtime cost: the value and its use are in different compilation units, so the compiler cannot inline it; an extra fetch is required at runtime. (With a static library, link-time optimization may eliminate this — provided both the library and the caller were compiled with -flto.)

  • Dynamic libraries: with a .so/.dll, the runtime fetch is unavoidable — the value cannot be inlined across the dynamic link boundary, and may legitimately differ between runs (e.g. a version number). This can be intentional, but it means the caller has no compile-time guarantee of the value.

  • No user customization: a macro’s value can depend on macros defined by the user at include time; a constant’s value is fixed for all users.

The linkage dependency is the most consequential: libraries that are header-only cannot define extern constants at all. Macros sidestep this entirely.

There is an ill-advised third option: define the constant as static in the header. This avoids the linkage problem but creates a separate copy with its own address in each compilation unit that includes it.

Debugging constants#

Some macro constants are special: their value changes depending on where in the source they appear. Such behavior can only be achieved from within the compiler, and we will not be able to create our own.

The C standard requires compilers to define certain special macros, without the need to include any header, including:

  • __FILE__ expands to the name of the file currently compiled, as a C string literal

  • __LINE__ expands to the line number currently compiled, as an integer literal

Source: GNU

Additionally, the GNU C extension include:

  • __FILE_NAME__ similar to __FILE__ but only includes the file name, excluding the path

Source: GNU Clang

Related compiler-provided identifiers include:

  • __FUNCTION__ (also __func__) is a magic constant character array that contains the name of the current function

  • __PRETTY_FUNCTION__ is similar but includes the whole signature, including return type and parameters

Warning

__FUNCTION__ and __PRETTY_FUNCTION__ are not macros, but runtime constants. As the GNU documentation puts it:

These identifiers are variables, not preprocessor macros, and may not be used to initialize char arrays or be concatenated with string literals.

Source: GNU

 1#include <stdbool.h> // bool
 2#include <stdio.h>   // printf
 3
 4void foo(void)
 5{
 6    printf("%s@%s:%i\n", __FILE_NAME__, __FUNCTION__, __LINE__);
 7}
 8
 9bool bar(int arg)
10{
11    printf("In file %s in function %s at line %i: arg=%i\n", __FILE__, __PRETTY_FUNCTION__, __LINE__, arg);
12    return true;
13}
14
15int main()
16{
17    foo();
18    bar(42);
19}
 1#include <stdbool.h> // bool
 2#include <stdio.h>   // printf
 3
 4void foo(void)
 5{
 6    printf("%s@%s:%i\n", __FILE_NAME__, __FUNCTION__, __LINE__);
 7}
 8
 9bool bar(int arg)
10{
11    printf("In file %s in function %s at line %i: arg=%i\n", __FILE__, __PRETTY_FUNCTION__, __LINE__, arg);
12    return true;
13}
14
15int main()
16{
17    foo();
18    bar(42);
19}
void foo(void)
{
    printf("%s@%s:%i\n", "02_debug.c", __FUNCTION__, 6);
}

_Bool bar(int arg)
{
    printf("In file %s in function %s at line %i: arg=%i\n",
           "samples/02_debug.c",
           __PRETTY_FUNCTION__,
           11,
           arg);
    return 1;
}

int main()
{
    foo();
    bar(42);
}
02_debug.c@foo:6
In file samples/02_debug.c in function _Bool bar(int) at line 11: arg=42

Function-like#

Macros can also take parameters. Unlike functions, they expand at compile-time: there is no call overhead and no type constraints:

 1#include <termios.h> // struct termios
 2
 3#include <stddef.h> // offsetof
 4#include <stdio.h>  // printf
 5
 6#if __has_include("linux/stddef.h")
 7#   include <linux/stddef.h> // sizeof_field
 8#endif
 9#ifndef sizeof_field
10#   define sizeof_field(Type, Member) sizeof(((Type){}).Member)
11#endif
12
13/** Learn about the memory layout of the termios struct */
14int main()
15{
16    printf("__Termios structure__:\n| Name | Position | Size |\n|:-|-:|-:|\n");
17    printf("|`c_iflag`|%zu|%zu|\n",
18           offsetof(struct termios, c_iflag),
19           sizeof_field(struct termios, c_iflag));
20    printf("|`c_cflag`|%zu|%zu|\n",
21           offsetof(struct termios, c_cflag),
22           sizeof_field(struct termios, c_cflag));
23    printf("|`c_lflag`|%zu|%zu|\n",
24           offsetof(struct termios, c_lflag),
25           sizeof_field(struct termios, c_lflag));
26    printf("|`c_cc`|%zu|%zu|\n",
27           offsetof(struct termios, c_cc),
28           sizeof_field(struct termios, c_cc));
29}
 1#include <termios.h> // struct termios
 2
 3#include <stddef.h> // offsetof
 4#include <stdio.h>  // printf
 5
 6#if __has_include("linux/stddef.h")
 7#   include <linux/stddef.h> // sizeof_field
 8#endif
 9#ifndef sizeof_field
10#   define sizeof_field(Type, Member) sizeof(((Type){}).Member)
11#endif
12
13/** Learn about the memory layout of the termios struct */
14int main()
15{
16    printf("__Termios structure__:\n| Name | Position | Size |\n|:-|-:|-:|\n");
17    printf("|`c_iflag`|%zu|%zu|\n",
18           offsetof(struct termios, c_iflag),
19           sizeof_field(struct termios, c_iflag));
20    printf("|`c_cflag`|%zu|%zu|\n",
21           offsetof(struct termios, c_cflag),
22           sizeof_field(struct termios, c_cflag));
23    printf("|`c_lflag`|%zu|%zu|\n",
24           offsetof(struct termios, c_lflag),
25           sizeof_field(struct termios, c_lflag));
26    printf("|`c_cc`|%zu|%zu|\n",
27           offsetof(struct termios, c_cc),
28           sizeof_field(struct termios, c_cc));
29}
/** Learn about the memory layout of the termios struct */
int main()
{
    printf("__Termios structure__:\n| Name | Position | Size |\n|:-|-:|-:|\n");
    printf("|`c_iflag`|%zu|%zu|\n",
           __builtin_offsetof(struct termios, c_iflag),
           sizeof(((struct termios){}).c_iflag));
    printf("|`c_cflag`|%zu|%zu|\n",
           __builtin_offsetof(struct termios, c_cflag),
           sizeof(((struct termios){}).c_cflag));
    printf("|`c_lflag`|%zu|%zu|\n",
           __builtin_offsetof(struct termios, c_lflag),
           sizeof(((struct termios){}).c_lflag));
    printf("|`c_cc`|%zu|%zu|\n",
           __builtin_offsetof(struct termios, c_cc),
           sizeof(((struct termios){}).c_cc));
}

Termios structure:

Name

Position

Size

c_iflag

0

4

c_cflag

8

4

c_lflag

12

4

c_cc

17

32

With the compiler-provided constants in hand, the next chapter puts them to use in a logging utility.

Recap#

This chapter covered:

  1. Macros used as constants are replaced by their value before compilation — no runtime cost, no linking required

  2. Some compiler-provided macros (__FILE__, __LINE__) change value depending on where they appear in the source

  3. __func__ is not a macro but a runtime constant — it cannot be concatenated with string literals

  4. Function-like macros take parameters and expand at compile-time, with no type constraints