Using X-Macros to manage a command interface

Published: Friday, 25 June 2010 12:29

Command interfaces are essential to embedded systems and any applications without a GUI.  This article will introduce X-Macros as a means of creating a clean and tidy command parser, which I regularly use in systems to respond to serial and ethernet requests.  X-Macros are a way of using the C and C++ preprocessor which results in code "writing itself" in a pre-defined manner, perfect for repetitive but subtly different programming tasks.

In our command example, our command list is used in two places, the "help" menu (which lists all commands and what they do) and the command parser (which checks if the command entered exists, and what to do when it is called).  This quickly gets messy.  If a command is added or removed, it is common for the code to require updating in several places.  This is obviously not ideal, it takes time and incurs the possibility of introducing mistakes (mismatched command and command response for example).  More adventurous programmers may try ideas such as multi-dimensional arrays, function pointers, and even standard library containers, these all result in something similar to what I am trying to achieve, but just isn"t as nice.

I believe a better option would be to maintain a single "map" (possibly in a seperate config file) containing the following:

Throughout this article, I'll assume our system has read a command string from "somewhere" (keyboard, RS232, speech-to-text engine etc), and that this will be passed to a function called "parseCommandString" which takes a string "sRecvdString"

Using the Pre-Processor, Overview

Firstly, and most importantly, we must understand how the preprocessor will be used.

I"m assuming you know what #define works, and that defines can span multiple lines by ending them with a backslash.  Consider this code;

  1. #define STRINGLIST \
  2. STRING("shoalresearch") \
  3. STRING("articles") \
  4. STRING("and") \
  5. STRING("tutorials")

That should be obvious, when you write "STRINGLIST" the three lines following it will substituted in your code when it is compiled.

Next, have a look at this one;

  1. #define STRING(text) text,
  2. STRINGLIST
  3. #undef STRING

This is the preprocessor feature that we will be making use of.  Think of it as defining a function called "STRING" which takes a parameter called "text".  The output of this function (after the closing bracket) is "text,"  which is the paramter text followed by a comma.  Following this is the STRINGLIST we defined earlier, which you will notice now become calls to the STRING function.  The "code" of the STRING(text) function is defined as "text," (the code is after the function definition closing bracket).  In this case, a call to STRING("test code") will output test code,

Here is how it will look the first time the preprocessor parses our example;;

  1. #define STRING(text) text,
  2. STRING("shoalresearch")
  3. STRING("articles")
  4. STRING("and")
  5. STRING("tutorials")
  6. #undef

This will then expand out to;

  1. shoalresearch,articles,and,tutorials,

Callbacks, Overview

Callbacks are something that many people ignore, but I think they are great, especially in embedded applications as a means to decouple the hardware level code (eg interrupts) and the code that some people refer to asbusiness logic .  If you have never used them, essentially they are a pointer to a function, so that when we make a call to the pointer, we are actually calling the function.  It means we can pass around functions like variables, so we don"t have to hard-code function calls, they can be determined at runtime if required.  In our example, we need to maintain a list of functions that relate to particular commands.  This could be done using a hugeswitch/case block, but it is nowhere near as maintainable as we want.

To create a pointer to a function, assign the correct function to the pointer, then call the pointer - take a look at the following example:

  1. int testFunction( void )
  2. {
  3. /* Do nothing. */
  4. return 1;
  5. }
  6. int main(int argc, char* argv[])
  7. {
  8. /* Define function pointer called 'pFunc' */
  9. int (*pFunc)(void);
  10. /* Point the pointer to the function */
  11. pFunc = &testFunction;
  12. /* Call the test function using the pointer (returns 1) */
  13. pFunc();
  14. return 0;
  15. }

Putting it all together

First things first, decide what information we require for our commands.  First is the command, then a description of what the command does (for our "help" output!) and we will also need a callback to the commands handler function.  All of this data will need to go into a XMacro map called COMMAND_LIST:

  1. #define COMMAND_LIST \
  2. COMMAND("help", "Displays this help", cmdHelp) \
  3. COMMAND("diagnostics", "Print diagnostics", cmdDiag) \
  4. COMMAND("shutdown", "Turns the device off", cmdShutdown)

As you can probably tell, our command list contains 3 commands ("help", "diagnostics" and "shutdown").  They will map to the 3 functions (cmdHelp, cmdDiag, cmdShutdown) respectively.  The 3 functions need to be prototyped and the functionality can be added by you later.  For now, this will suffice:

  1. int cmdHelp() { return 0; };
  2. int cmdDiag() { return 0; };
  3. int cmdShutdown() { return 0; };

Next, we will need pointers to all of the functions, so we can call the correct function when a certain command is received.  We will also need a way to find out what the correct function is.  To do this, the easiest way is to put everything in arrays, then match them up by array index.  So, first up, the function pointer array;

  1. /* Typedef'ing function pointers makes them MUCH simpler! */
  2. typedef int(*fnPtr)(void);
  3. /* Create an array of functions pointers from the COMMAND_MAP */
  4. fnPtr cmdPtr[] = {
  5. #define COMMAND(cmd, descr, ptr) &ptr,
  6. COMMAND_LIST
  7. #undef COMMAND
  8. };

Next we"ll need the same for the command and the description.Note that a final termination string has been added to the endto make it easier to iterate through the array (loop until null string)

  1. /* Command list */
  2. char* cmdCMD[] = {
  3. #define COMMAND(cmd, descr, ptr) cmd,
  4. COMMAND_LIST
  5. #undef COMMAND
  6. "\0"
  7. };
  8. /* Description of commands */
  9. char* cmdDescr[] = {
  10. #define COMMAND(cmd, descr, ptr) descr,
  11. COMMAND_LIST
  12. #undef COMMAND
  13. "\0"
  14. };

This now means that when we select index "0"  from each of the arrays, cmdCMD, cmdDescr, cmdPtr, we will get the command "help", description "Displays this help" and the pointer to cmdHelp.

To illustrate this, here is an example of how the cmdHelp function might look:

  1. /*
  2. * Output a help message to stdout.
  3. * Input: None
  4. * Output: 1 on success (assumed)
  5. */
  6. int cmdHelp(void)
  7. {
  8. int i = 0;
  9. /* Iterate through all commands in list */
  10. while(cmdCMD[i] != '\0')
  11. {
  12. /* Print the details */
  13. printf(cmdCMD[i]);
  14. printf("\t\t");
  15. printf(cmdDescr[i]);
  16. printf("\n");
  17. /* Move on to the next cmd */
  18. i++;
  19. }
  20. return 1; /* Assume 1 means success */
  21. }

In our example, this would output the following:

  1. help Displays this help
  2. disagnostics Print diagnostics
  3. shutdown Turns the device off

Now all that is left to do is integrate this in with your command receiving code, perhaps keyboard input, RS232 or Ethernet.  Here is a quick example to get you going;

  1. /*
  2. * Input: char* received_cmd - the command we have received
  3. *
  4. * Output: int - 0 if not found, or whatever the fn pointer returns.
  5. */
  6. int DoCommand(char* received_cmd)
  7. {
  8. int i = 0;
  9. /* Iterate through all of the commands */
  10. while(cmdCMD[i] != '\0')
  11. {
  12. /* Find out if command matches */
  13. if(strcmp(cmdCMD, received_cmd) == 0)
  14. {
  15. /* If it matches, call the corresponding function */
  16. return cmdPtr[i]();
  17. }
  18. /* String didnt match so continue looping */
  19. i++;
  20. }
  21. /* If not found, return 0. */
  22. return 0;
  23. }

Possible Pitfalls and Enhancements

Resources

The most major pitfall when using X-MACROS and taking advantage of the ability of code to be written at compile time is how easy it is to not realise how much code is actually being written, and how much of the available resources are being used.  For example, I once used this in a microcontroller application and although I was keeping track of code size, the RAM filled up with elaborate command description text.

Parameters

It doesnt take much to allow commands to take parameters.  Revisiting the function pointer code above, it doesn't take much to convert it to taking a single INT parameter.

  1. /* The function pointer declatation must match the function prototype */
  2. int (*pFunc)(int);

  1. /* Call the function, giving it an int and getting the int result */
  2. int ret = pFunc( 42 );

To print out the allowed parameters in the help file, it will be easier to not print the command, but re-write the command within the description, and add the parameters there.  For example:

  1. COMMAND("setvar", "setvar [V] Sets the value to 'V' where V is greater than 0 ", cmdSetVar)