David Tóth David Tóth - 2 months ago 9
C Question

Using X-lists and preprocessor directives to generate configurable C Code At compile time

I have a codebase already containing repetitive code, with only minor differences, serializable ID-s, indexes, variable arrays.

The codebase is huge, and some components are being activated/deactivated based on simple preprocessor directives and constants(e.g.:

#define CFG_PROJECT cfgAutobot
,
#define CFG_PROJECT cfgUltron
, ..etc).

The functionality is effectively the same, but with varying components and conditionals. Example:

int somedata;
int somecounter;

void main_loop(){
#if(CFG_PROJECT == cfgAutobot)
if(someInterface() == 1){
somedata = some_other_interface();
}
#endif

#if(CFG_PROJECT == cfgUltron)
if(third_if() > 0){
someCounter++;
}
else
{
someCounter = 0;
}
#endif
}

void query_data(int selector){
if(False){
/* Dummy block */
}
#if(CFG_PROJECT == cfgUltron)
else if(selector == 1){
return somedata;
}
#endif
#if(CFG_PROJECT == cfgAutobot)
else if(selector == 2){
return someCounter;
}
#endif
else{
return Err_code;
}
}


Because the data this code works with is much more complicated, than a simple counter and integer, involves multiple components of varying sizes, these code parts are much more complicated. However they can be traced back to a common structure.

I was able to apply the X-list technique as follows:

#define Ultron_implementation X(var_ultron, (someInterface() == 1), update_function_1, selector_id_1)
#define Autobot_implementation X(var_autobot, (third_if() > 0), update_function_2, selector_id_2)

/* (Please note, that this is a simplified example, in the actual
code there are much more components, but the `main_loop`
implementation can be traced back to a few update functions) */
void update_function_1(int var, int selector) {
if(selector == 1){
var++;
}else{
var = 0;
}
}


void update_function_2(int var, int selector) {
if(selector == 1){
var = some_other_interface();
}else{
/* Nothing to do */
}
}

#define X(var_name,condition,func_name,sel_id) int var_name;
Ultron_implementation
Autobot_implementation
#undef X

void main_loop(){

#define X(var_name,condition,func_name,sel_id) \
if(condition){ \
func_name(var_name, true);\
}else{ \
func_name(var_name, false);\
}
Ultron_implementation
Autobot_implementation
#undef X
}

void query_data(int selector){
if(False){
/* Dummy block */
}
#define X(var_name,condition,func_name,sel_id) \
else if(selector == sel_id){ \
return var_name;\
}
Ultron_implementation
Autobot_implementation
#undef X

else{
return Err_code;
}
}


The problem with this is that in spite of it now being a unified implementation, the introduction of new components still needs copy-paste, and filtering via previously defined constants(i.e.:
CFG_PROJECT
) is now excluded from the logic.




Is there a way to minimize the need of copy-pasting into various places in the code and to filter based on defined constants (i.e.
CFG_PROJECT
)?

Answer

Filtering to predefined contstants at compile time would require the prerocessor directives #if, #ifdef, etc.. but there is no way to use these inside #define statements AFAIK.

However writing these outside the #define statements is totally legitimate.

#if(CFG_PROJECT == cfgAutobot)
    #define Autobot_implementation X(var_autobot, (third_if() > 0), update_function_2, selector_id_1)
#else
    #define Autobot_implementation
#endif

#if(CFG_PROJECT == cfgUltron)
    #define Ultron_implementation X(var_autobot, (third_if() > 0), update_function_2, selector_id_2)
#else
    #define Ultron_implementation
#endif

And the former can be compiled into a list(of sorts)

#define MACRO_LIST \
    Autobot_implementation \
    Ultron_implementation

Depending on the defined constants the elements of MACRO_LIST will either contain the X() function definition (i.e.: implementation), or an empty constant.

In the implementation now the following can be used:

void main_loop(){

        #define X(var_name,condition,func_name,sel_id) \
        if(condition){ \
            func_name(var_name, true);\
        }else{ \
            func_name(var_name, false);\
        }
            MACRO_LIST
        #undef X
}

To sum up the activated components, see how many components are activated and to refer to them in the implementation, the concatenate (##) token can be used in relation with e.g. an enumeration definition. Example:

#define X(var_name,condition,func_name,sel_id) var_name ## index, 
    tyepdef enum{
        MACRO_LIST
        components_end
    }component_index;
#undef X

some_struct COMPONENT_FLAGS[components_end];

Basically any related variable, ID or implementation can be "serialized" this way.

Please note:

This solution makes the code harder to comprehend, maintain and really difficult to debug, but once it have been tested and verified it eliminates error possibilites coming from copypasting. The result will be a much cleaner, much more elegant and much smaller codebase, than the alternative.

It actually decreased development time from 3 months to a few hours in production code.