Clever use of X-Macros (https://en.wikipedia.org/wiki/X_Macro ) allow to decorrelate a set of data from the code which will handle them. For example, I had recently to implement a parser, which would analyse an incoming stream, isolate the embedded command and act upon.
So in this typical situation, you generally get the stream matched against some predefined-strings, arranged in an ordered way. Then, you get an ID, parameters and call a specific handler. Nothing fancy.
However, if you want this code to be maintainable (for example, you decide to change the handler name), and upgradable (adding new commands ID), you certainly would like some mechanism which would gather all the changeable bits (the commands, the ID’s, etc ..) from the pure parser structure. This is where X-Macros become handy.
To start with, let’s define what a command handler looks like:
typedef bool(*cmdHandler)(uint8_t*);
Then, I need to define what are the command related items, which make sense to group together. In my specific case, I can list the following (keeping the main ones)
- The command actual name
- command ID
- command handler
- length of payload (in our example, in byte unit)
Then, we create a central file (say cmd_tbl.h), which contains all these items together. The syntax is very simple, we simply define an arbitrary Macro (DEF), which would take all these items as parameters
/* DEF(cmd name, cmd ID, cmd handler, length) */ DEF(RESET, 0xA4, handler_Reset, 0) DEF(PTUPAGETCURRENT, 0xA4, handler_pa_current_get, 0) DEF(PTUPAOVRCURRENT, 0xA4, handler_pa_current_override, 2) DEF(PTUPAGETVOLTAGE, 0xA4, handler_pa_voltage_get, 0) DEF(PTUSETPAITXCOIL, 0xA4, handler_pa_voltage_override, 2) DEF(PTUCOILGETCURRENT, 0xA4, handler_coil_current_get, 0) DEF(PTUCOILOVRCURRENT, 0xA4, handler_coil_current_override, 2)
As the command names suggest, these commands related to an A4WP device for which I was writing a command parser (https://en.wikipedia.org/wiki/Rezence_(wireless_charging_standard))
The first advantage shows up clearly: we have semantically grouped all the items related to our commands altogether. At a glance, we can say what a “RESET” command is made of … and hopefully, we will be able to change this by only altering this file.
Now, let’s use these X-Macros in our parser code. We first define a command table like that
// -------------- // Command table // -------------- const cmdTbl_t commandTable = { #undef DEF #define DEF(cmd_str, cmd_id_msb, cmd_id_lsb, cmd_handler, cmd_len) {.cmd=((((uint8_t)(cmd_id_msb))<<8)|(cmd_id_lsb)),.handler=cmd_handler, .len=cmd_len}, #include "cmd_tbl.h" DEF(0xFF,0xFF,0xFF, defaultHandler,0) };
The principle is that, we redefine the DEF X-Macro each time to parse the table differently. Here, to build the command table, we don’t need the cmd_str parameter, so the DEF output would not include it.
If later, we need to build a different list, with parameters ordered differently, we simply UNDEF/DEFINE the X=Macro and include again the same file.
VERY IMPORTANT: the file cmd_tbl.h SHOULD NOT be guarded against multiple inclusion, otherwise the first table will be processed correctly … and all the subsequent ones will be empty 🙁
Then, the parser code can be kept completely generic, so we have reached our design goal. For example, the following function would find the command index in the table based upon a detected CMD_ID
static uint8_t getCommandIndex (const uint16_t opcode) { int i=0; while ((commandTable[i].cmd != opcode) && (i <NB_OF_COMMANDS)) i++; if (i >= NB_OF_COMMANDS) return INVALID_INDEX; else return i; }
This one would get the handler
cmdHandler getCommandHandler (const uint16_t opcode) { uint8_t index; if ((index = getCommandIndex(opcode)) == INVALID_INDEX) return (cmdHandler)NULL; return commandTable[index].handler; }