Useful C Macros
I generally try to avoid reaching for macros when I program in C because using them inappropriately can lead to unreadable code which does seemingly unpredictable things and is painful to debug (e.g., stepping through macros in a debugger). That being said, there are some macros that can really help make it much nicer to work in C by minimizing a lot of tedious boiler-plate style code. Here I will show some of my favorite C macros for various tasks like iterating over data structures and printing error messages.
Circular List Iteration
Say that we have a circular linked list structure defined like this:
typedef struct List List;
struct List {
List* next;
void* data;
};
Here is a macro to iterate over the elements of such a list:
#define FOR_EACH(p, list) for ( \
List* p = NULL, *next__##p = list; \
p != list && (p = next__##p); \
p = next__##p = p->next)
The macro is used like this, for example:
FOR_EACH(p, my_list) {
process_data(p->data);
}
Locking/Unlocking Mutexes
This macro helps creating critical sections by taking and holding a lock while the statements inside the block following the macro call are executed:
#define WITH_LOCK(lock) for(int xk=1; xk && !pthread_mutex_lock(&lock); \
(pthread_mutex_unlock(&lock)), xk=0)
For example:
pthread_mutex_t sched_lock = PTHREAD_MUTEX_INITIALIZER;
// ...
WITH_LOCK(sched_lock) {
vm_Task* task = task_from_ref(taskref);
if (task && task->state != TASK_STATE_FINISHED) {
state.ffi_result = TASK_BLOCKED;
task_wait_on(vm_current_task(), task);
}
}
Note that the macro relies on normal control flow exiting the block, so that the for-loop increment
statement can unlock the mutex.
In other words, you cannot use return, break, or continue inside the block.
WITH_LOCK(sched_lock) {
break; // DANGER! mutex is left locked
}
Error Reporting
Since C99 it is possible to use variable argument macros. This can be combined with a helper function to include extra information in error messages. Below is a simplified example of how this might be used, adding the function name in the error message.
#include <stdarg.h>
#define FFI_ERROR(...) ffi_error(__func__, __VA_ARGS__)
__attribute__((format(printf,2,3)))
static void ffi_error(const char* func, const char* format, ...)
{
va_list argp;
va_start(argp, format);
printf("%s: ", func);
vprintf(format, argp);
va_end(argp);
}
The macro is used like this:
void copy_to_buffer(int len, void* data)
{
if ((size_t) len + 1 > sizeof(strbuf)) {
FFI_ERROR("length %d is too large for temporary buffer\n", len);
}
}
The __func__ macro expands to the function name where the macro is used, so the error message
looks like this:
copy_to_buffer: length 123 is too large for temporary buffer
The __attribute__ above the function declaration is optional and just helps in checking
for printf argument errors.
Hash Table Iteration
In one project I am working on I wrote the following macro for iterating over a hash table:
/** An iterator for a hash table. */
typedef struct {
uint32_t i; // Current bucket index.
const char* key; // Current bucket key.
void* value; // Current bucket value.
} tbl_Iter;
#define TBL_ITERATE(it, tbl) for ( \
tbl_Iter it = { .i = 0 }; \
it.i < (tbl)->capacity && (it = (tbl_Iter) { \
.i = it.i, \
.key = (tbl)->buckets[it.i].key, \
.value = (tbl)->buckets[it.i].value }, true); \
it.i += 1) if (!(tbl)->buckets[it.i].flag)
This is very specific to one hash table implementation so it might not be easy to adapt to other hash tables, but here is how I would use it in the project where it lives:
HashTable* tbl = new_hashtable();
tbl_insert(tbl, "name", ast_call(...));
TBL_ITERATE(it, tbl) {
const char* name = (const char*) it.key;
AstNode* node = (AstNode*) it.value;
}
Addendum: clang-format
If you use the for-each style macros like the ones above (FOR_EACH or TBL_ITERATE), you can add the ForEachMacros option to your .clang-format so that you get formatting to look better for these macros:
ForEachMacros: ['FOR_EACH', 'TBL_ITERATE']