Nested C Preprocessor Macros

The C preprocessor and macros are one of the most useful elements for writing portable C/C++ code . It even can  be used to completely different purposes, like text processing.  However, it suffers one major drawback: it does not support nested macros expansion (put differently, macros within macros).

So for example, the following would not work as you expect:

#include <stdio.h>

#define OUTER_MACRO(a,b) (a ## b)
#define INNER_MACRO(a) (#a)

const char *str1 = "hello world";

void main (void)
{
  // Displays "Hello world"
  printf ("Output is: %s\n", OUTER_MACRO(st,r1)); 

  // Displays "r1"
  printf ("%s", INNER_MACRO (r1)); 
 
  // .. but this one causes compiler error
  printf ("Output is: %s", OUTER_MACRO (st, INNER_MACRO (r1))); 
}

Both OUTER_MACRO and INNER_MACRO work as expected, but not in composition. There are however situations where this is desirable.

For example, in the following code:

*fp = fopen (ACCEL_DATA_FILE, "r");

ACCEL_DATA_FILE refers to, as the name suggest, some acceleration data gathered by an instrumentation. As we are interested that the file name for these “Acceleration data” reflect also some date/time, we need to find a way to once open accel_data_20171013.dat and later accel_data_1027104.dat. However, our client code should not change.This can be achieved with a smart macro toolbox, as follows.

First, we define some utility macros (tools_macros.h)

#ifndef __BIZARRE_MACROS__
#define __BIZARRE_MACROS__

#define STRINGIFY(X) STRINGIFY2(X)
#define STRINGIFY2(X) #X

#define CAT(X,Y) CAT2(X,Y)
#define CAT2(X,Y) X##Y

#define INCLUDE_FILE(HEAD,TAIL) STRINGIFY(CAT(HEAD,TAIL) )

#endif

The whole secret here is to do a “double expansion”, so that, contrary to the small test program above, each level of expansion produces something meaningful.

Now we can write

#include "tools_macros.h"

#define REL_PATH .\\data
#define ACCEL_DATA_FILE STRINGIFY(CAT(REL_PATH,\\session_accel.c))

the outer macro would expand as

“CAT(REL_PATH, \\session_accel.c)”

so our line fopen would then include one single macro which expands correctly :). Following the same principle, we can also write:

const int16_t a_test_data[] = {

#include INCLUDE_FILE(SESSION_PREFIX,_accel.c)

};

where the inner generated file contains only data lines

-1689,1481,288,
-1526,1605,168,
-1564,1063,-417,
-1677,621,-827,
-1737,1157,-243,
-1589,1194,-637,
-1364,790,-584,
-1420,924,-541,
-1610,1386,-453,
-1534,1361,-494,
-1462,1249,-632,
-1487,1354,-551,
-1375,1429,-509,
-1333,1413,-534,
-1237,1359,-623,
-1424,1350,-586,
-1329,1548,-674,
-1352,1472,-841,
-1482,1601,-1215,
-1405,2517,-804,
-1339,3469,378,
-1690,2384,-218,

Those lines are the typical output of some instrumentation (X,Y,Z accelerations). Using these tricky macros once again allowed us to reach the code / data isolation paradigm.

Leave a Reply

Your email address will not be published.