embedded software boot camp

Effective C Tips #1 – Using vsprintf()

Tuesday, February 10th, 2009 by Nigel Jones

I’ve been running a series of tips on Efficient C for a while now. I thought I’d broaden the scope by also offering a series of tips on what I call Effective C. These will be tips that while not necessarily allowing you to write tighter code, will allow you to write better code. I’m kicking the series off on the rarely used standard library function, vsprintf(). First, some preamble…

One of the perverse things I tend to do is look through the C standard library and examine functions that on the face of it seem, well, useless. I do this because I think the folks that worked on this stuff were in general very smart and thus had a very good reason for including some of these ‘weird’ functions. One of these is the function ‘vsprintf’. If you go and look up the definition of this function, e.g. here , then you’ll find a rather brain ache inducing description. Now back when I was a lad I’d look at descriptions such as this and simply shrug and walk away. However, about ten years ago I started to make a concerted effort to see if a function such as vsprintf has a real benefit in embedded systems. Here’s what I discovered in this case:

If you are working on a product that contains a VFD or LCD, then you will almost certainly have code that contains a function for writing a string to the display at a specified position. For example:

static void display_Write(uint8_t row, uint8_t col, char const * buf)
{
 /* Send formatted string to display - hardware dependent*/
}

Then you will also have a plethora of functions that essentially do the same thing. That is accept some data, allocate a buffer on the stack, use sprintf to write formatted data into the buffer, and then call the function that actually writes the buffer to the display at the required position. Here’s some examples:

void display_Temperature(float ambient_temperature)
{
 char buf[10;

 sprintf(buf,"%5.2f", ambient_temperature);
 display_Write(6, 8, buf);
}

...

void display_Time(int hours, int minutes, int seconds)
{
 char buf [12];

 sprintf(buf,"%02d:%02d:%02d", hours, minutes, seconds);
 display_Write(3, 9, buf);
}

There’s nothing really wrong with this approach. However, there is a better way, courtesy of vsprintf().

What one does is to modify display_Write() to take a variable length argument list. Then within display_Write() use vsprintf() to process the variable length argument list and to generate the requisite string. The basic structure for the function is as follows:

void display_Write(uint8_t row, uint8_t column, char const * format, ...)
{
 va_list  args;
 char  buf[MAX_STR_LEN];

 va_start(args, format);
 vsprintf(buf, format, args); /* buf contains the formatted string */

 /* Send formatted string to display - hardware dependent */

 va_end(args);                /* Clean up. Do NOT omit */
}

My objective here is not to explain how to use variadic arguments or indeed how vsprintf() works – there are dozens of places on the web that will do that. Instead I’m interested in showing you the benefit of this approach. The display_Write() function has evidently become more complex; however the functions that call display_Write have become dramatically simplified, as they are now just:

void display_Temperature(float ambient_temperature)
{
 display_Write(6, 8, "%5.2f", ambient_temperature);
}

void display_Time(int hours, int minutes, int seconds)
{
 display_Write(3, 9, "%02d:%02d:%02d", hours, minutes, seconds);
}

Is this more Effective code? I think so, for the following reasons.

  • The higher level functions are now much cleaner and easier to follow.
  • All the heavy lifting is localized in one place, which typically dramatically reduces the probability of errors.

Finally, you’ll typically end up with a nice reduction in code size (even though this wasn’t my objective). All in all, not bad for one obscure function.

Next Tip
Home

9 Responses to “Effective C Tips #1 – Using vsprintf()”

  1. Michael Barr says:

    FYI to readers: MISRA-C Rule 16.1 prohibits the creation of functions with variable numbers of arguments. There are risks involved in doing this in a safety-critical system.

  2. Nigel Jones says:

    Mike is quite right. Of course I’m not sure what MISRA expects one to do when one is writing code for a GUI. Although it’s possible to write string formatters without using variable length argument lists, the results typically aren’t pretty. I think this is one of those cases where the cure may be worse than the illness.

  3. Anonymous says:

    What I've done in the past is to use a macro to achieve the roughly the same result.I use this for my DEBUG statements while I'm developing.#if defined(DEBUG_GLOBAL) && defined(DEBUG_LOCAL) #define DEBUG(__format, __args…) do { printf_P( PSTR(__format), ## __args ); } while (0) #else #define DEBUG(__format, __args…)#endif

  4. Anonymous says:

    oops, I forgot to add that I use stdin/stdout redirection to a custom putchar function that actually does the work of putting the code on to a serial port or LCD screen.

  5. Mans says:

    I would argue vsprintf() should never be used due to the risk of buffer overflow. One should always use (v)snprintf(). If your system doesn't have it, steal a BSD licensed one somewhere.

  6. e says:

    I agree wholeheartedly with Mans: use (v)snprintf(). A public domain version is part of sqlite. — e

  7. Atlant says:

    I’ll second, err, “third” the comment to always use (v)snprintf(). Without that, it’s too easy for a buffer overflow to sneak up on you.

    We had a case recently that Grammatech’s CodeSonar caught for us. We were doing (simplified):

    sprintf( five_byte_buffer, “%04X”, uint16_value );

    and Code Sonar was telling us that the conversion could overflow the buffer. “Nonsense!” we said, “knowing” that 1) the four bytes output by the “%04X” conversion plus the trailing null character couldn’t be more than five bytes and 2) we’d actually tested all reasonable values of uint16_value from 0x0000 to 0xFFFF and our conversion routine had worked fine.

    But it turns out that CodeSonar knew something we didn’t know: the “%x” conversion in some libraries would sign-extend into 32 bits any value being “%x” formatted before doing the formatting and on systems that used *THAT* sort of conversion, “negative” values would then be formatted as eight characters (e.g. “FFFFFFFF”) and *THAT* would blow our buffer. We switched to using snprintf() and that eliminated the buffer overflow. (Code Sonar was still unhappy that on some systems, the conversion would not fit in the buffer (and would be truncated) but since it did fit on our system (and wasn’t truncated), we decided we could live with that.)

    Always use the …nprintf() version of the functions!

    • Nigel Jones says:

      Apologies for the delay in moderating the comment – I have been away on holiday. Anyway, thanks for sharing this, as I wasn’t aware of the sign extension issue. As a matter of interest, what happens if you use the C99 printf formatters, as described here

  8. Lazureus says:

    Atlant, can you tell us on which platform such a conversion takes place ?

    Maybe it’s a lame question but can that happened also with the “%d” formatting ?

    e.g.

    sprintf(four_byte_buffer,”%03d”,uint8_t_value) ;

Leave a Reply to Nigel Jones

You must be logged in to post a comment.