分类: C/C++
2014-05-31 12:26:15
X-macros
Reduce C-language coding errors with X macros - Part 1
Andrew Lucas
January 01, 2013
Editor's note: Andrew Lucas describes how to use X macros to take advantage of the C-language pre-processor to eliminate several classes of common bugs. He also describes how to use X macros to improve developer productivity through automatic code generation.
X macros are a powerful coding technique that makes extensive use of the C-language pre-processor. This technique has the capability to eliminate several classes of common bugs.
It seems to me that the C preprocessor gets a bad rap. Granted, there are ways to use the preprocessor inappropriately, but to limit its use because of that constrains a valuable tool that can reduce coding errors and improve developer productivity though automatic code generation.
Code Ordering Dependencies
I discovered X macros a few years ago when I started making use of function pointers in my code. Frequently I would write code like this:
/* declare an enumeration of state codes */
enum{STATE_0, STATE_1, STATE_2, ... , STATE_N, NUM_STATES};
/* declare a table of function pointers */
p_func_t jumptable[NUM_STATES] = {func_0, func_1, func_2, ... , func_N};
The issue with this type of code is maintainability. The ordering of the array initializers has to match the ordering of the state code enumeration exactly. Historically I would comment this type of code liberally to warn future users about this dependency, but protection based on commenting is really no protection at all. What I needed was a tool that would automatically enforce the dependency.
I began investigating solutions for this problem and discovered that in the C99 standard there was a new way to initialize arrays. An improved way to write the above code is as follows:
/* declare an enumeration of state codes */
enum{STATE_0, STATE_1, STATE_2, ... , STATE_N, NUM_STATES}
/* declare a table of function pointers */
p_func_t jumptable[NUM_STATES] = {
[STATE_1] = func_1,
[STATE_0] = func_0,
[STATE_2] = func_2,
... ,
[STATE_N] = func_N
};
Now even if I change the ordering of the enumeration, the jumptable logic doesn’t break. Much better. My only problem was that the C compiler I was working with was not compliant with the C99 standard. Back to square one.
X macros to the Rescue
One day while talking shop with a friend of mine, I explained my problem and he suggested using the C preprocessor to enforce the ordering. He explained the basic concept: Use preprocessor directives to define a table in the form of a macro and then redefine how the macro is expanded, as required.
Here's how this technique enforces my code ordering dependency:
#define STATE_TABLE \
ENTRY(STATE_0, func_0) \
ENTRY(STATE_1, func_1) \
ENTRY(STATE_2, func_2) \
... \
ENTRY(STATE_X, func_X)
/* declare an enumeration of state codes */
enum{
#define ENTRY(a,b) a,
STATE_TABLE
#undef ENTRY
NUM_STATES
};
/* declare a table of function pointers */
p_func_t jumptable[NUM_STATES] = {
#define ENTRY(a,b) b,
STATE_TABLE
#undef ENTRY
};
In the case of the enumeration the table expands to ‘a’ which is the first column of the state table; the state code. In the case of the array, the table expands to ‘b’ which is the second column, the name of the function pointer.
The code based on the X macro table is expanded in the same order for both the enumeration and the array. The preprocessor now enforces the dependency!
Cleaning up the code
One thing I don’t like about this implementation is the presence of #define and #undef throughout the code, which to me is ugly and makes the code less readable. Let’s look at a technique for getting rid of them.
You will notice that in my definition of the STATE_TABLE macro I don’t take any parameters. There is nothing to prevent me from passing the definition of ENTRY directly to the STATE_TABLE macro instead of defining it separately:
#define EXPAND_AS_ENUMERATION(a,b) a,
#define EXPAND_AS_JUMPTABLE(a,b) b,
#define STATE_TABLE(ENTRY) \
ENTRY(STATE_0, func_0) \
ENTRY(STATE_1, func_1) \
ENTRY(STATE_2, func_2) \
... \
ENTRY(STATE_X, func_X)
/* declare an enumeration of state codes */
enum{
STATE_TABLE(EXPAND_AS_ENUMERATION)
NUM_STATES
}
/* declare a table of function pointers */
p_func_t jumptable[NUM_STATES] = {
STATE_TABLE(EXPAND_AS_JUMPTABLE)
}:
Much better, but is there anything else that we could use the X macro table for? Since every function pointer corresponds to an actual function, we could use the table to generate function prototypes for us:
#define EXPAND_AS_PROTOTYPES(a,b) static void b(void);
STATE_TABLE(EXPAND_AS_PROTOTYPES);
Now I no longer need to remember to add a prototype when I add new states. The preprocessor can take care of it and will expand the table into the following code automatically:
static void func_0(void);
static void func_1(void);
static void func_2(void);
...
static void func_X(void);
Register Initialization
That's not the only way X macros can be used. In my code I commonly have to interface to custom FPGAs. These devices usually have many memory mapped registers that need initialization. It's easy to forget to initialize a newly defined register, but using X macros, this is another task we can automate.
#define EXPAND_AS_INITIALIZER(a,b) a = b;
#define REGISTER_TABLE(ENTRY) \
ENTRY(reg_0, 0x11) \
ENTRY(reg_1, 0x55) \
ENTRY(reg_2, 0x1b) \
... \
ENTRY(reg_X, 0x33)
static void init_registers(void){
REGISTER_TABLE(EXPAND_AS_INITIALIZER)
}
Simple; and as new registers are added, no code needs to be updated to initialize it - we just add a row to the table and the preprocessor does the rest. We can further improve this code to take into account not only the initialization, but the declaration of the registers:
#define FPGA_ADDRESS_OFFSET (0x8000)
#define EXPAND_AS_INITIALIZER(a,b,c) a = c;
#define EXPAND_AS_DECLARATION(a,b,c) volatile uint8_t a _at_ b;
#define REGISTER_TABLE(ENTRY) \
ENTRY(reg_0, FPGA_ADDRESS_OFFSET + 0, 0x11) \
ENTRY(reg_1, FPGA_ADDRESS_OFFSET + 1, 0x55) \
ENTRY(reg_2, FPGA_ADDRESS_OFFSET + 2, 0x1b) \
... \
ENTRY(reg_X, FPGA_ADDRESS_OFFSET + X, 0x33)
/* declare the registers */
REGISTER_TABLE(EXPAND_AS_DECLARATION)
This code uses a compiler specific directive _at_ to place the variables at absolute addresses. This may not be possible with other compilers. Secondly, more than one table may be required to take into account different types of register declarations. You may need to have a read-only register table, a write-only register table, an uninitialized register table, etc.
I hope that this introduction to X macros
has provided a glimpse into the power of this coding technique. In Part 2 I dig
a little deeper and show some more advanced uses of X macros to facilitate
automatic code generation.
Reduce C- language coding errors with X-macros - Part 2
This article continues the discussion in Part 1 regarding the use of X macros. Here I will examine several more uses of X macros that relieve the programmer from having to manually enforce dependencies and perform data structure sizing calculations.
Counting Entries
Consider the following use case for populating enumerations and jump tables:
/* declare an enumeration of state codes */
enum{
STATE_TABLE(EXPAND_AS_ENUMERATION)
NUM_STATES
}
/* declare a table of function pointers */
p_func_t jumptable[NUM_STATES] = {
STATE_TABLE(EXPAND_AS_JUMPTABLE)
}
There is one thing that I still don’t like about this implementation; the NUM_STATES hack. I call it a hack because although a common technique for defining a count of items, NUM_STATES does not belong inside the enumeration. We could define it as a separate macro, but this requires the developer to remember to update it whenever the enumeration changes.
We can use X macros to place the NUM_STATES macro outside of the enumeration, while at the same time have the pre-processor update it automatically. We start off by declaring a struct as follows:
#define EXPAND_AS_STRUCT(a,b) uint8_t b,
/* declare a struct with a 1-byte field for every entry */
typedef struct{
STATE_TABLE(EXPAND_AS_STRUCT)
} size_struct_t;
Now if we apply the size of operator to this struct, the compiler will calculate the number of states, since each state is represented by a one-byte field and the sizeof operator returns the total number of bytes:
#define NUM_STATES sizeof(size_struct_t)
The concept is that there are valid reasons to define a struct but not instantiate it. We don’t need to create a variable of type size_struct_t for it to be useful.
A little more verbose perhaps than the previous hack, but we succeeded in having the preprocessor automatically generate a NUM_STATES macro without having to pollute the enumeration. A note of caution; this method depends on the compiler not inserting any padding bytes into size_struct_t, so understand your compiler.
Communication Handlers
In my mind this is the “killer app” when it comes to X macro usage. The benefit it provides to the programmer is significant. Here is the general technique:
First create a table containing command names and codes. For our purposes we will ensure that command codes are contiguous:
/* ------ NAME ------- FUNCTION --- CODE --- */
#define COMMAND_TABLE \
ENTRY(COMMAND0, command0, 0x00) \
ENTRY(COMMAND1, command1, 0x01) \
ENTRY(COMMAND2, command2, 0x02) \
...
ENTRY(COMMANDX, commandX, 0x0X
Both uppercase and lowercase names are in the table, because the upper case names will be used for enumerations while the lowercase will be used for function names.
Next define the command code enumeration:
#define EXPAND_AS_COMMAND_CODE_ENUM(a,b,c) a##_CMD = c,
enum{
COMMAND_TABLE(EXPAND_AS_COMMAND_CODE_ENUM)
};
If you are not familiar with token concatenation syntax, the ‘##’ simply appends ‘_CMD’ onto the end of each enumeration name. In this case the table would be expanded as follows:
enum{
COMMAND0_CMD,
COMMAND1_CMD,
COMMAND2_CMD,
...
COMMANDX_CMD,
};
Next define structs for each command and response:
typedef struct {uint8_t command; ...}command1_cmd_t;
typedef struct {uint8_t command; ...}command2_cmd_t;
etc...
typedef struct {uint8_t command; ...}command1_resp_t;
typedef struct {uint8_t command; ...}command2_resp_t;
etc...
Only the command code fields are shown, but presumably there will be other fields defined for each command (length field, optional data fields, crc field etc.).
Now use X macros to define an enumeration of command and response lengths by applying the sizeof operator to the structs. These enumerations will be used in the message handler functions to verify that incoming message lengths are correct and to populate potential length fields in outgoing messages:
#define EXPAND_AS_COMMAND_LEN_ENUM(a,b,c) \
a##_CMD_LEN = sizeof(b##_cmd_t),
enum{
COMMAND_TABLE(EXPAND_AS_COMMAND_LEN_ENUM)
};
#define EXPAND_AS_RESPONSE_LEN_ENUM(a,b,c)
a##_RESP_LEN = sizeof(b##_resp_t),
enum{
COMMAND_TABLE(EXPAND_AS_RESPONSE_LEN_ENUM)
};
As previously explained, determine how many commands there are:
#define EXPAND_AS_STRUCT(a,b,c) uint8_t b,
typedef struct{
COMMAND_TABLE(EXPAND_AS_STRUCT)
} size_struct_t;
#define NUMBER_OF_COMMANDS sizeof(size_struct_t)
Finally generate the table of function pointers and prototypes. As a reminder, the jumptable will only be correct if the command codes are contiguous.
#define EXPAND_AS_JUMPTABLE(a,b,c) process_##b,
#define EXPAND_AS_PROTOTYPES(a,b,c) void process_##b(void);
p_func_t jump_table[NUMBER_OF_COMMANDS] = {
COMMAND_TABLE(EXPAND_AS_JUMPTABLE)
}
COMMAND_TABLE(EXPAND_AS_PROTOTYPES)
With this implementation, when a new command needs to be added the following three steps should be followed.
Add a new row to COMMAND_TABLE.
Define the structure of the command and response messages called commandX_cmd_t and commandX_resp_t.
Write a function to handle the new command called “process_commandX()”.
The beauty of this is that even though there are three steps, if any are forgotten we will get warnings or errors when attempting to compile. If the structures are not defined, the compile will fail during creation of the enumeration of data lengths. If the command handler function is not written, depending on the compiler it may fail during compilation, but if not it should provide a warning during linking that there is an unresolved external.
One More Thing
Every comms handler is going to have buffers it uses to hold the sent and received messages. The question is, how big should the buffers be? Here is yet another instance where X macros can do the heavy lifting for us.
Since we have defined the format of every command and response, what we want is for the compiler to determine what the largest message is so we can size the buffers appropriately. Here is how you can do that.
#define EXPAND_AS_BUFFERS(a,c,b) uint8_t b##_buf[sizeof(b##_cmd_t)];
typedef union{
COMMAND_TABLE(EXPAND_AS_BUFFERS)
}tx_buf_t
In this case we are using a union, because by definition the size of a union is as large as its largest member. Inside the union we drop via X macros an array of bytes for each command, where the size of the array is the size of the command’s struct. This union is like the size struct - it is not instantiated. Instead we can use the sizeof operator to declare our transmit buffer size.
/* declare transmit buffer */
uint8_t tx_buf[sizeof(tx_buf_t)];
Now my transmit buffer tx_buf is the optimal size and as I add commands to the comms handler table, my buffer will always be adjusted to the perfect size.
Next Time
In my next article I will discuss how to handle a case where command codes are not contiguous. I will also examine a memory optimization technique where we can use X macros to save valuable memory when jump tables are sparsely populated.
Reduce C-language coding errors with X macros - Part 3
Part 2 in this series showed how to develop the following x macro table with the caveat that the command codes needed to be contiguous:
/* ------ NAME ------- FUNCTION --- CODE --- */
#define COMMAND_TABLE \
ENTRY(COMMAND0, command0, 0x00) \
ENTRY(COMMAND1, command1, 0x01) \
ENTRY(COMMAND2, command2, 0x02) \
...
ENTRY(COMMANDX, commandX, 0x0X)
One nice thing about having contiguous codes is that the test for index validity is simple:
ASSERT(command < N_COMMANDS);
command_jump_table[command]();
When command codes are not contiguous, you need to ensure that you don’t jump to a non-existing function. The simplest way to do this is to make the jump table large enough to support every possible index. However, this is at the expense of memory. You will need 512 bytes (assuming an 8-bit command code size) if the architecture uses 16-bit pointers or 1024 bytes if it uses 32-bit pointers.
With this implementation, instead of the simple ASSERT statement, define an error handling function that can be embedded into the jump table for all invalid commands.
Here is how to do that with x macros, together with some other preprocessor trickery:
#define INIT_X1 process_reserved,
#define INIT_X2 INIT_X1 INIT_X1
#define INIT_X4 INIT_X2 INIT_X2
#define INIT_X8 INIT_X4 INIT_X4
#define INIT_X16 INIT_X8 INIT_X8
#define INIT_X32 INIT_X16 INIT_X16
#define INIT_X64 INIT_X32 INIT_X32
#define INIT_X128 INIT_X64 INIT_X64
#define INIT_X256 INIT_X128 INIT_X128
#define EXPAND_JUMP_TABLE(a,b,c) [c] = process_##b,
static const p_func_t command_jump_table[256] = {
/* initialize all pointer to the reserved function */
INIT_X256,
/* overwrite pointers to valid functions */
COMMAND_TABLE(EXPAND_AS_JUMP_TABLE)
};
This requires the use of a C99 compiler for the designated initializer syntax, furthermore the compiler may warn you that you are overriding a previously initialized value. If a C99 compiler is not available, then the valid function pointers can be updated at run time, at the expense of not being able to place the jump table in ROM. Here's how to this:
#define EXPAND_JUMP_TABLE(a,b,c) \
command_jump_table[c] = process_##b;
/* during run-time initialization */
COMMAND_TABLE(EXPAND_AS_JUMP_TABLE)
Optimizing jump tables
For many applications where memory is limited, this implementation is not practical. What can be done instead is to add an extra level of indirection as follows:
command_jump_table[command_offset_table[command]]();
In this case a second table is utilized that has one entry for every possible command code. Each entry contains an offset into the actual jump table. The memory requirements for this implementation are significantly less than the initial implementation. We need 256 bytes for our offset table, but only 2 or 4 bytes for each function pointer in the jump table. Therefore implementing a communications handler with a dozen commands would need 280/304 bytes instead of 512/1024.
One side-effect of this implementation is that we need to reserve one of the command codes since invalid commands need to resolve to a valid offset so that the correct error handler can be called. The most logical offset value to use is zero (simplifies initialization) and thus the new command table looks like this:
/* ------ NAME ------- FUNCTION --- CODE --- */
#define COMMAND_TABLE \
ENTRY(RESERVED, reserved, 0x00) \
ENTRY(COMMANDA, commandA, 0x02) \
ENTRY(COMMANDB, commandB, 0x09) \
...
ENTRY(COMMANDZ, commandZ, 0xef)
A nice side-effect of this implementation is that it is actually easier to automate the creation of these two separate tables than it is to automate the creation of the single jump table.
The creation of the jump table has not changed from our original example with contiguous command codes. The caveat being that we can no longer use the table directly via the command code.
#define EXPAND_AS_JUMP_TABLE(a,b,c) process_##b,
static const p_func_t command_jump_table[N_COMMANDS] = {
COMMAND_TABLE(EXPAND_AS_JUMP_TABLE)
};
The creation of the offset table is as follows (recall the following struct from last month):
#define EXPAND_AS_STRUCT(a,b,c) uint8_t b,
typedef struct{
COMMAND_TABLE(EXPAND_AS_STRUCT)
} size_struct_t;
#define N_COMMANDS sizeof(size_struct_t)
This struct can be used for multiple purposes. In addition to taking the size of the struct, we can calculate the offsets of each element in the struct to use when initializing the offset table:
#define EXPAND_AS_OFFSET_TABLE_INITIALIZER(a,b,c) \
[c] = (uint8_t) offsetof(size_struct_t, b);
…where offsetof() is a standard library macro defined in "stddef.h". If the reader is unfamiliar with the offsetof() macro I highly recommend reading the article by Nigel Jones, Learn a new trick with the offsetof() macro. Use this new x macro do the following:
/* statically declare our offset table, valid commands initialized to the correct offsets and invalid command initialized to 0 */
uint8_t command_offset_table[256] = {
COMMAND_TABLE(EXPAND_AS_OFFSET_TABLE_INITIALIZER)
};
As before, this macro expansion relies on a C99 compiler. If a C99 is not available, do the following:
uint8_t command_offset_table[256] = {0};
#define EXPAND_AS_OFFSET_TABLE_INITIALIZER(a,b,c) \
command_offset_table[c] = (uint8_t) offsetof(size_struct_t, b);
/* during run-time initialization */
COMMAND_TABLE(EXPAND_AS_OFFSET_TABLE_INITIALIZER)
A nice side-effect of the offset table implementation is that if we need to implement a function which identifies if a given code is a valid command; the resulting function is a one-liner:
bool command_is_valid(uint8_t command){
return command_offset_table[command];
}
Timeout Tables
Another common table that can be similarly initialized is a timeout table. In the case of a master processor instead of a slave processor, you need to allow a set period of time for the slave to respond before determining that a communications fault has occurred. Usually different commands will have differing requirements for this time, so implementing a look-up table for each command is necessary. Modifying the x macro table, we can add this functionality easily:
/* ------ NAME ------- FUNCTION --- CODE --- TIMEOUT(ms) ---*/
#define COMMAND_TABLE \
ENTRY(RESERVED, reserved, 0x00, 0) \
ENTRY(COMMANDA, commandA, 0x02, 100) \
ENTRY(COMMANDB, commandB, 0x09, 500) \
...
ENTRY(COMMANDZ, commandZ, 0xef, 20)
#define EXPAND_AS_TIMEOUT_TABLE_INITIALIZER(a,b,c,d) d,
uint16_t timeout_table[N_COMMANDS] = {
COMMAND_TABLE(EXPAND_AS_TIMEOUT_TABLE_ININTIALIZER)
};
Here is how I would use this table:
int get_command_timeout(uint8_t command) {
return timeout_table[command_offset_table[command]];
};
GPIO Configuration
One final area is the use of x macros to allow for code reuse by making it simple to change what gpio pins are used by a module. As an example consider the following table:
#define SWITCHES_GPIO_TABLE(ENTRY) \
ENTRY(SWITCHA, GPIOE, GPIO_Pin_0) \
ENTRY(SWITCHB, GPIOA, GPIO_Pin_0)
ENTRY(SWITCHC, GPIOC, GPIO_Pin_5)
ENTRY(SWITCHD, GPIOE, GPIO_Pin_1)
I will leave it to the reader as an exercise, but from this table, as long as the supporting code follows a few conventions, all you have to do is change the port and pin name in the table and all of the initialization code adjusts accordingly, making it easy to port over to a new design.
Final Thoughts
I hope you have enjoyed this overview of x macros and that you have learned something new in the process. Though an advanced technique, once in place the use of x macros can reduce errors and make the embedded programmer's life easier. I would love to read in the comments how readers are using x macros in their designs.
Andrew Lucas leads a team of firmware developers at NCR Canada. He is responsible for the firmware architecture of their intelligent deposit modules found inside NCR's line of ATMs.