C Macros: Pseudo-generic methods and structs

Using the Powers of C Pre-processor to write generic methods and classes.

Nishant Aanjaney Jalan
CodeX
4 min readJul 25, 2023

--

While programming in C, my peers and I noticed something that is missing from the main C construct of the language — generic functionality. This became an issue for us in the project as otherwise we had to duplicate a dozen functions to implement the same function. With the power of macros and the C pre-processor, we overcame this blocker. Although this may not be the best way to write C — considering debugging and code-style issues — it was definitely something to consider.

Photo by La-Rel Easter on Unsplash

How we arrived at this problem?

Our project was to write an emulator for the ARMv8 ISA. This meant there were 31 general-purpose registers and 4 extra registers (zero, flags, stack pointer and program counter) inside the main processor. All of the general-purpose register could be referenced in either 32-bit mode or 64-bit mode. This called for using a union, which is not a commonly used struct in C.

typedef union {
int32_t wn; // 32-bit mode
int64_t xn; // 64-bit mode
} reg_t;

The ISA of ARMv8 have instructions to perform arithmetic and logical operations such add/subtract and and/or.

int32_t add(int32_t a, int32_t b) {
return a + b;
}

int32_t or(int32_t a, int32_t b) {
return a | b;
}

// [...] many more functions

But this was just for 32-bit functions! We needed to write the same code, replacing all int32_t with int64_t. If we had to extend our emulator to support 16-bit versions of the registers, that would be another duplicated set of code. We were not able to use any generic functionally out-of-the-box.

C Macros to the rescue!

We solved this problem by using macros to define the generic functionality. Before executing the code, the C pre-processor would expand this to different functions that our code could use.

// generic_functions.h

#include <stdint.h>

#ifndef GENERIC_FUNCTIONS
#define GENERIC_FUNCTIONS

#define generic_operations(T) \
extern T add_##T(T, T); \
extern T sub_##T(T, T); \
extern T and_##T(T, T); \
extern T or_##T(T, T);


#define generic_operations_impl(T) \
T add_##T(T val1, T val2) { \
return val1 + val2; \
} \
T sub_##T(T val1, T val2) { \
return val1 - val2; \
} \
T and_##T(T val1, T val2) { \
return val1 & val2; \
} \
T or_##T(T val1, T val2) { \
return val1 | val2; \
}

generic_operations(int32_t)

generic_operations(int64_t)

#endif

generic_operations is a macro defined such that when we use the macro in the code, we would have to pass in a parameter (could be anything) and the pre-processor would replace that line with the next 4 lines according to the passed parameter.

After the pre-processor is done with it’s job, the intermediate C header file will look something like this:

// [...] everything from stdint.h

extern int32_t add_int32_t(int32_t, int32_t);
extern int32_t sub_int32_t(int32_t, int32_t);
extern int32_t and_int32_t(int32_t, int32_t);
extern int32_t or_int32_t(int32_t, int32_t);

extern int64_t add_int64_t(int64_t, int64_t);
extern int64_t sub_int64_t(int64_t, int64_t);
extern int64_t and_int64_t(int64_t, int64_t);
extern int64_t or_int64_t(int64_t, int64_t);

Notice that double-hash (##) in the macro means No! I do not mean expand to add_T; instead, I want you to append whatever the value of T is to add_. That is why the C pre-processor writes the header file where the type is part of the name of every function.

Now we also need to write down the implementation for this as well.

// generic_functions.c

#include "generic_functions.h"

generic_operations_impl(int32_t)

generic_operations_impl(int64_t)

which is, in turn, compiled to:

// [...] everything from generic_functions.h

int32_t add_int32_t(int32_t val1, int32_t val2) {
return val1 + val2;
}
int32_t sub_int32_t(int32_t val1, int32_t val2) {
return val1 - val2;
}
int32_t and_int32_t(int32_t val1, int32_t val2) {
return val1 & val2;
}
int32_t or_int32_t(int32_t val1, int32_t val2) {
return val1 | val2;
}

int64_t add_int64_t(int64_t val1, int64_t val2) {
return val1 + val2;
}
int64_t sub_int64_t(int64_t val1, int64_t val2) {
return val1 - val2;
}
int64_t and_int64_t(int64_t val1, int64_t val2) {
return val1 & val2;
}
int64_t or_int64_t(int64_t val1, int64_t val2) {
return val1 | val2;
}

The macro expanded exactly how we would have written the functions. We can proceed to use these functions in our code appropriately without any major code duplication in the source file.

Conclusion — Is this a boon or bane?

There are a couple of drawbacks to using C macros. They are hard to debug; some LSPs do not identify expanded macros correctly; there is no check to see if the parameters passed are valid or not. For one could write generic_operations(haha_dummy) and the compiler would not complain until the pre-processor does its work.

From the developers' point of view, one must be careful how they are using macros as the code is free and unrestricted. Otherwise, it is quite useful to use macros, especially in scenarios where you need to use some generic functionality among various types.

I hope you enjoyed reading my article and learned something. Thank you!

Want to connect?

My GitHub profile.
My Portfolio website.

--

--

Nishant Aanjaney Jalan
CodeX
Editor for

Undergraduate Student | CS and Math Teacher | Android & Full-Stack Developer | Oracle Certified Java Programmer | https://cybercoder-naj.github.io