andersch.dev

<2022-05-04 Wed>

C (Programming Language)

C is a compiled programming language created in the 1970s. The most common C compilers are gcc, clang & MSVC.

Programming Idioms

Macros: scoped

A begin-end macro that runs the first argument after entering the following scope and the second argument before exiting it.

#define token_paste(a, b) a##b
#define concat(a,b) token_paste(a,b)
#define macro_var(name) concat(name, __LINE__)

#define scoped(start, end)          \
    int macro_var(_i_) = 0;         \
    for(start; !macro_var(_i_) != 0; (macro_var(_i_)++, end)) \
      for(; !macro_var(_i_) != 0; macro_var(_i_)++)

// usage
scoped(printf("Hel"), printf("rld\n"))
{
    printf("lo Wo");
    break; // still runs end expression
}

Here a version that only runs what's in the scope if the begin expression returns true (it will still evaluate the end expression, however):

#define DeferLoopChecked(begin, end) for(int _i_ = 2 * !(begin); (_i_ == 2 ? ((end), 0) : !_i_); _i_ += 1, (end))

/* usage */
DeferLoopChecked((0 == 1), printf("World\n"))
{
    printf("Hello\n");
}

Macros: with

Statement similar to python's with using macros:

#define token_paste(a, b) a##b
#define concat(a,b) token_paste(a,b)
#define macro_var(name) concat(name, __LINE__)
#define with(declare, cleanup)                                      \
    int macro_var(_i_) = 0;                                         \
    for(declare; !macro_var(_i_) != 0; (macro_var(_i_)++, cleanup)) \
      for(; !macro_var(_i_) != 0; macro_var(_i_)++)

#define withif(declare, cond, cleanup)                                      \
    int macro_var(_i_) = 0;                                                 \
    for(declare; !macro_var(_i_) != 0; (macro_var(_i_)++, (cond ? cleanup : (void)0))) \
    if (cond) for(; !macro_var(_i_) != 0; macro_var(_i_)++)

int main () {

    with (FILE *fp = fopen("c.org", "r"), fclose(fp)) {
        printf("with: %p\n", fp);
        break; // cleanup still runs
        // return; // cleanup will NOT run
    }

    withif (FILE *fp = fopen("c.org", "r"), fp != NULL, fclose(fp)) {
        // won't enter because fp == NULL
        printf("withif: %p\n", fp);
    } else {
        printf("withif: no\n");
    }
}

Take for example the SDL initialization boilerplate:

if (SDL_Init(SDL_INIT_VIDEO) < 0) { SDL_Log("Init error\n"); }

SDL_Window* window = SDL_CreateWindow("window", 800, 600, 0);
if (!window) {
  SDL_Log("Window error\n");
  SDL_Quit();
}

SDL_Renderer* renderer = SDL_CreateRenderer(window, NULL);
if (!renderer) {
  SDL_Log("Renderer error\n");
  SDL_DestroyWindow(window);
  SDL_Quit();
}

// main loop

SDL_DestroyRenderer(renderer);
SDL_DestroyWindow(window);
SDL_Quit();

Now with the withif statement:

withif (int init = SDL_Init(SDL_INIT_VIDEO), init >= 0, SDL_Quit()) {
  withif (SDL_Window* window = SDL_CreateWindow("window", 800, 600, 0), window != NULL, SDL_DestroyWindow(window)) {
    withif (SDL_Renderer* renderer = SDL_CreateRenderer(window, NULL), renderer != NULL, SDL_DestroyRenderer(renderer)) {
      // main loop
    } else { SDL_Log("Renderer error\n"); }
  } else { SDL_Log("Window error\n"); }
} else { SDL_Log("Init error\n"); }

Macros: #define next to related things

You can put a #define close to the thing that relates to it:

enum renderer_entry_type
{
    RENDERER_ENTRY_NULL,

    #define RENDERER_MAX_LINES 16384
    RENDERER_ENTRY_LINE,

    #define RENDERER_MAX_TEXTURES 32768
    RENDERER_REQUEST_texture,
};

Macros: Comma-Operator in Expressions

Use comma operator to include e.g. an assertion in a macro that is supposed to expand to an expression:

#define GET_FIRST_ELEM(a) (assert(a->length > 0), a->array[0])

How to switch between game modes

How to easily switch between e.g. game modes:

b32 keep_running = false;
do
{
    switch(game_state->current_mode)
    {
        case GAME_MODE_MAIN_MENU:
        {
            keep_running = game_update_main_menu(game_state, input, game_state->title_screen);
        } break;

        case GAME_MODE_CUTSCENE:
        {
            keep_running = game_update_cutscene(game_state, input, game_state->cutscene);
        } break;

        case GAME_MODE_WORLD:
        {
            keep_running = game_update_world(game_state, input, game_state->world);
        } break;
        default:
        {
            UNREACHABLE("invalid game mode\n");
        } break;
    }
} while(keep_running);

A good way to add a discriminator to a discriminated union

Accessing the discriminator in a discriminated union without casting or having to specify an access qualifier:

/* library side */
typedef struct { int type, size; } mu_BaseCommand;
typedef struct { mu_BaseCommand base; void *dst; } mu_JumpCommand;
typedef struct { mu_BaseCommand base; mu_Rect rect; mu_Color color; } mu_RectCommand;

typedef union {
  int type;
  mu_BaseCommand base;
  mu_JumpCommand jump;
  mu_RectCommand rect;
  /* ... */
} mu_Command;

/* user side */
switch (cmd->type)
{
    case RECT_COMMAND: /* ... */ break;
    case JUMP_COMMAND: /* ... */ break;
    // ...
}

Arrays: Store 2D array in a 1D array

                x 12345
                  -----  y
height = 3       |     | 1
width  = 5       |   X | 2
idx    = 9       |     | 3
                  -----

x = idx % width       = 4
y = ceil(idx / width) = 2

Arrays: Cycle through a ring buffer

u8 ring_buf[RING_BUF_SIZE];

for (int i = 0; i < ENTRIES_TO_ADD; i++)
{
    ring_buf[i % RING_BUF_SIZE] = entries[i];
}

Memory: Header and Data in a Single allocation

struct header_and_data_t
{
    u64 cap;
    u64 len;
    u8* data;
};
header_and_data_t = malloc(sizeof(header_and_data) + cap * sizeof(u8))

Memory: Handles instead of Pointers

From Handles are the better pointers:

  • move memory management into centralized systems (rendering, physics, …)
  • systems are the sole owner of their memory allocations
  • group items of same type into arrays, treat base pointer as system-private
  • creating an item only returns an 'index-handle' to the outside, not a pointer
  • index-handles use as many bits as needed for the array index
  • use remaining bits for additional memory safety checks
  • convert a handle to a pointer when needed, but don't store pointer anywhere

Pseudo Type-Safety with void* APIs

When a C API implements e.g. a generic data structure like a dynamic array or hash table using void*, all type information is stripped - thus no real type safety can be given. However, by restraining the usage of the API to some bespoke macros, we can still have a compile-time type check to avoid passing the wrong type:

/* implementation */
void* grow(void* ptr, int* len, int* cap);

/* defined in either usage or implementation code */
#define vec_push(V) ((V)->data = grow(&(V)->data, &(V)->len, &(V)->cap), &(V)->data[(V)->len++])

/* usage code */
*vec_push(v) = 5;

Strong Typing for typedef

typedef by default only creates a weak type alias:

typedef int meters_t;
typedef int hours_t;

meters_t m = 1;
hours_t  h = m; // not an error

Wrapping the types into a struct makes the code properly typesafe:

typedef struct { int val; } meters_t;
typedef struct { int val; } hours_t;

meters_t m = { 1 };
hours_t h = m;    // compile error

Writing out a string literal comfortably

Writing a glsl shader string or similar comfortably with a macro

#define SHADER_STR(x) "#version 330\n" #x // workaround to include strings starting with #

    const char* vs = SHADER_STR(
        uniform mat4 u_mvp;

        in vec2 in_pos;
        in vec2 in_uv;

        out vec2 v_uv;

        void main( )
        {
            v_uv = in_uv;
            gl_Position = u_mvp * vec4(in_pos, 0, 1);
        }
    );

Struct: Default & named parameters

#include <stdio.h>

typedef struct thing_t {
    int    foo;
    float  bar;
    size_t width;
    float  zero_by_default;
} thing_t;

void default_and_named_args_(thing_t* thing)
{
    printf("Width: %zu\n", thing->width);
    printf("Foo:   %i\n",  thing->foo);
    printf("Bar:   %f\n",  thing->bar);
    printf("Zero:  %f\n",  thing->zero_by_default);
}

#define default_and_named_args(...) \
        default_and_named_args_(&(thing_t) { .width = 5,  __VA_ARGS__ })

//#pragma GCC diagnostic push
//#pragma GCC diagnostic ignored "-Winitializer-overrides"
int main(void)
{
    //thing_t thing = { .foo = 3, .bar = 0.141f };
    //default_and_named_args(&thing);

    default_and_named_args(.bar = 1.1f, .foo = 4);

    // override default argument
    default_and_named_args(.width = 100, .foo = 22);

    return 0;
}
//#pragma GCC diagnostic pop

Struct: C/C++ initialization using ctor()

C and C++ differ in their syntax for initializing structs:

#ifdef __cplusplus
    #define ctor(TYPE, ...) (TYPE {__VA_ARGS__})
#else
    #define ctor(TYPE, ...) ((TYPE){__VA_ARGS__})
#endif

typedef struct aabb_t { float min, max; } aabb_t;
#define aabb_t(...)   ctor(aabb_t, __VA_ARGS__)

Struct: Default Values using ctor()

Previous approach can be further used to introduce default values for members:

#ifdef __cplusplus
    #define ctor(TYPE, ...) (TYPE {__VA_ARGS__})
#else
    #define ctor(TYPE, ...) ((TYPE){__VA_ARGS__})
#endif

typedef struct aabb_t { float min, max; } aabb_t;
#define aabb_t(...)   ctor(aabb_t, __VA_ARGS__)

typedef struct line_t { float a, b; int foo;  } line_t;
#define line_t(...)   ctor(line_t, .foo = 5, __VA_ARGS__) // default value *before* __VA_ARGS__

int main() {
    line_t line = line_t();
    printf("%f %f %i\n", line.a, line.b, line.foo);

    line = line_t(.foo = 10); // override default value
    printf("%f %f %i\n", line.a, line.b, line.foo);
}

This is useful to set nil struct pointers:

#ifdef __cplusplus
    #define ctor(TYPE, ...) (TYPE {__VA_ARGS__})
#else
    #define ctor(TYPE, ...) ((TYPE){__VA_ARGS__})
#endif

typedef struct aabb_t { float min, max; } aabb_t;
#define aabb_t(...)   ctor(aabb_t, __VA_ARGS__)
typedef struct entity_t {
    int transform;
    struct entity_t* next;
} entity_t;

entity_t nil_entity = ctor(entity_t, .transform = 0, .next = 0);

#define entity_t(...) ctor(entity_t, .next = &nil_entity, __VA_ARGS__)

int entity_is_nil(entity_t* entity) { return (entity == &nil_entity); }

int main() {
    entity_t ent = entity_t(.next = 0);
}

Caveats for this approach:

  • C++ won't allow overriding the default member, since specifying a designated initializer multiple times is an error (g++, cl) or a warning (clang++)

Struct: Defining a "common data struct"

Use #define anonymous structs to have a bundle of data at a centralized location and easily add it to other structs without having to add an access qualifier (like a C version of inheritance):

// in renderer.h
#define RENDERER_COMMON_DATA        \
struct                              \
{                                   \
    RendererRequest active_request; \
    i32 flags;                      \
}

// in opengl_renderer.h
struct opengl_renderer_t
{
    RENDERER_COMMON_DATA;
    // ...
};

// in software_renderer.h
struct software_renderer_t
{
    RENDERER_COMMON_DATA;
    // ...
};

// accessing the common data:
opengl_renderer_t ogl_renderer = {};
ogl_renderer.active_request = ...;
if (ogl_renderer.flags) ...

Compare this to:

// in renderer.h
struct renderer_common_data_t
{
    RendererRequest active_request;
    i32 flags;
}

// in opengl_renderer.h
struct opengl_renderer
{
    renderer_common_data_t common;
    // ...
};

// in software_renderer.h
struct software_renderer
{
    renderer_common_data_t common;
    // ...
};

// accessing
opengl_renderer_t ogl_renderer = {};
ogl_renderer.common.active_request = ...;
if (ogl_renderer.common.flags) ...

Shortcomings of C

  • Header files in general
  • The preprocessor in general
  • No module system (#include is just a copy-paste)
  • Null-terminated strings
  • Array to pointer decay
  • No built-in dynamic array
  • No built-in hashtable
  • No multiple return values
  • No (built-in) coroutines
  • No defer statement
  • Can't chain break statements or pass in number of breaks
  • No introspection
  • No built-in matrices or vectors
  • Bad syntax for function pointers
  • Ambiguous grammar makes parsing difficult
  • No methods or Uniform Function Call Syntax (UFCS)
  • No nested functions (only with GNU extensions)

Generics in C11

The _Generic macro in C11 acts like a switch statement that switches on types:

float minf(float a, float b) { if (a > b) { return b; } else { return a; } }
int mini(int a, int b) { if (a > b) { return b; } else { return a; } };

/* _Generic ( controlling-expression , association-list ) */
#define min(a,b)       _Generic((a), float: minf(a,b), int: mini(a,b))
#define print_value(a) _Generic((a), float: printf("%f\n",a), int: printf("%i\n",a))

print_value(min(4.2f,2.8f));
print_value(min(3,8));

It can include a default case as a fallback. See Workarounds for C11 _Generic.

To check for support of _Generic:

/* check based on compiler support */
#if ((__GNUC__*10000+__GNUC_MINOR__*100+__GNUC_PATCHLEVEL__)>=40900) || ((__clang_major__*10000+__clang_minor__*100+__clang_patchlevel__)>=30000) || (__xlC__>=0x1201)
printf("Generic supported\n");
#endif

/* check based on C11 standard */
#if __STDC__==1 && __STDC_VERSION >= 201112L
#endif

X-macros can be used in combination with _Generic:

#include <stdio.h>

#define TYPES(X) \
    X(int)        \
    X(float)      \

#define TYPE_TO_STRING(type,...) type : serialize_##type,

void serialize_int(int s)     { printf("i: %i\n", s); }
void serialize_float(float s) { printf("i: %f\n", s); }
void dummy() {};

#define serialize(i)                \
_Generic(i,                       \
    TYPES(TYPE_TO_STRING) \
    default : dummy)(i)

int main() {
    int a = 1;
    float b = 2;
    serialize(a);
    serialize(b);
}

Resources