'Minimal implementation of sprintf or printf

I'm working on an embedded DSP where speed is crucial, and memory is very short.

At the moment, sprintf uses the most resources of any function in my code. I only use it to format some simple text: %d, %e, %f, %s, nothing with precision or exotic manipulations.

How can I implement a basic sprintf or printf function that would be more suitable for my usage?



Solution 1:[1]

This one assumes the existence of an itoa to convert an int to character representation, and an fputs to write out a string to wherever you want it to go.

The floating point output is non-conforming in at least one respect: it makes no attempt at rounding correctly, as the standard requires, so if you have have (for example) a value of 1.234 that is internally stored as 1.2399999774, it'll be printed out as 1.2399 instead of 1.2340. This saves quite a bit of work, and remains sufficient for most typical purposes.

This also supports %c and %x in addition to the conversions you asked about, but they're pretty trivial to remove if you want to get rid of them (and doing so will obviously save a little memory).

#include <stdarg.h>
#include <stdio.h>
#include <string.h>
#include <windows.h>

static void ftoa_fixed(char *buffer, double value);
static void ftoa_sci(char *buffer, double value);

int my_vfprintf(FILE *file, char const *fmt, va_list arg) {

    int int_temp;
    char char_temp;
    char *string_temp;
    double double_temp;

    char ch;
    int length = 0;

    char buffer[512];

    while ( ch = *fmt++) {
        if ( '%' == ch ) {
            switch (ch = *fmt++) {
                /* %% - print out a single %    */
                case '%':
                    fputc('%', file);
                    length++;
                    break;

                /* %c: print out a character    */
                case 'c':
                    char_temp = va_arg(arg, int);
                    fputc(char_temp, file);
                    length++;
                    break;

                /* %s: print out a string       */
                case 's':
                    string_temp = va_arg(arg, char *);
                    fputs(string_temp, file);
                    length += strlen(string_temp);
                    break;

                /* %d: print out an int         */
                case 'd':
                    int_temp = va_arg(arg, int);
                    itoa(int_temp, buffer, 10);
                    fputs(buffer, file);
                    length += strlen(buffer);
                    break;

                /* %x: print out an int in hex  */
                case 'x':
                    int_temp = va_arg(arg, int);
                    itoa(int_temp, buffer, 16);
                    fputs(buffer, file);
                    length += strlen(buffer);
                    break;

                case 'f':
                    double_temp = va_arg(arg, double);
                    ftoa_fixed(buffer, double_temp);
                    fputs(buffer, file);
                    length += strlen(buffer);
                    break;

                case 'e':
                    double_temp = va_arg(arg, double);
                    ftoa_sci(buffer, double_temp);
                    fputs(buffer, file);
                    length += strlen(buffer);
                    break;
            }
        }
        else {
            putc(ch, file);
            length++;
        }
    }
    return length;
}

int normalize(double *val) {
    int exponent = 0;
    double value = *val;

    while (value >= 1.0) {
        value /= 10.0;
        ++exponent;
    }

    while (value < 0.1) {
        value *= 10.0;
        --exponent;
    }
    *val = value;
    return exponent;
}

static void ftoa_fixed(char *buffer, double value) {  
    /* carry out a fixed conversion of a double value to a string, with a precision of 5 decimal digits. 
     * Values with absolute values less than 0.000001 are rounded to 0.0
     * Note: this blindly assumes that the buffer will be large enough to hold the largest possible result.
     * The largest value we expect is an IEEE 754 double precision real, with maximum magnitude of approximately
     * e+308. The C standard requires an implementation to allow a single conversion to produce up to 512 
     * characters, so that's what we really expect as the buffer size.     
     */

    int exponent = 0;
    int places = 0;
    static const int width = 4;

    if (value == 0.0) {
        buffer[0] = '0';
        buffer[1] = '\0';
        return;
    }         

    if (value < 0.0) {
        *buffer++ = '-';
        value = -value;
    }

    exponent = normalize(&value);

    while (exponent > 0) {
        int digit = value * 10;
        *buffer++ = digit + '0';
        value = value * 10 - digit;
        ++places;
        --exponent;
    }

    if (places == 0)
        *buffer++ = '0';

    *buffer++ = '.';

    while (exponent < 0 && places < width) {
        *buffer++ = '0';
        --exponent;
        ++places;
    }

    while (places < width) {
        int digit = value * 10.0;
        *buffer++ = digit + '0';
        value = value * 10.0 - digit;
        ++places;
    }
    *buffer = '\0';
}

void ftoa_sci(char *buffer, double value) {
    int exponent = 0;
    int places = 0;
    static const int width = 4;

    if (value == 0.0) {
        buffer[0] = '0';
        buffer[1] = '\0';
        return;
    }

    if (value < 0.0) {
        *buffer++ = '-';
        value = -value;
    }

    exponent = normalize(&value);

    int digit = value * 10.0;
    *buffer++ = digit + '0';
    value = value * 10.0 - digit;
    --exponent;

    *buffer++ = '.';

    for (int i = 0; i < width; i++) {
        int digit = value * 10.0;
        *buffer++ = digit + '0';
        value = value * 10.0 - digit;
    }

    *buffer++ = 'e';
    itoa(exponent, buffer, 10);
}

int my_printf(char const *fmt, ...) {
    va_list arg;
    int length;

    va_start(arg, fmt);
    length = my_vfprintf(stdout, fmt, arg);
    va_end(arg);
    return length;
}

int my_fprintf(FILE *file, char const *fmt, ...) {
    va_list arg;
    int length;

    va_start(arg, fmt);
    length = my_vfprintf(file, fmt, arg);
    va_end(arg);
    return length;
}


#ifdef TEST 

int main() {

    float floats[] = { 0.0, 1.234e-10, 1.234e+10, -1.234e-10, -1.234e-10 };

    my_printf("%s, %d, %x\n", "Some string", 1, 0x1234);

    for (int i = 0; i < sizeof(floats) / sizeof(floats[0]); i++)
        my_printf("%f, %e\n", floats[i], floats[i]);

    return 0;
}

#endif

Solution 2:[2]

I wrote nanoprintf in an attempt to find a balance between tiny binary size and having good feature coverage. As of today the "bare-bones" configuration is < 800 bytes of binary code, and the "maximal" configuration including float parsing is < 2500 bytes. 100% C99 code, no external dependencies, one header file.

https://github.com/charlesnicholson/nanoprintf

I haven't seen a smaller vsnprintf implementation than this that has a comparable feature set. I also released the software in the public domain so it's fully unencumbered.

Here's an example that uses the vsnprintf functionality:

your_project_nanoprintf.c

#define NANOPRINTF_USE_FIELD_WIDTH_FORMAT_SPECIFIERS 1
#define NANOPRINTF_USE_PRECISION_FORMAT_SPECIFIERS 1
#define NANOPRINTF_USE_LARGE_FORMAT_SPECIFIERS 1
#define NANOPRINTF_USE_FLOAT_FORMAT_SPECIFIERS 1
#define NANOPRINTF_USE_WRITEBACK_FORMAT_SPECIFIERS 0

// Compile nanoprintf in this translation unit.
#define NANOPRINTF_IMPLEMENTATION
#include "nanoprintf.h"

your_log.h

void your_log(char const *s);
void your_log_v(char const *fmt, ...);

your_log.c

#include "your_log.h"
#include "nanoprintf.h"

#include <stdarg.h>

void your_log_v(char const *s) {
  // Do whatever you want with the fully formatted string s.
}

void your_log(char const *fmt, ...) {
    char buf[128];

    va_arg args;
    va_start(args, fmt);
    npf_vsnprintf(buf, sizeof(buf), fmt, args); // Use nanoprintf for formatting.
    va_end(args);

    your_log_write(buf);
}

Nanoprintf also provides an snprintf-alike and a custom version that takes a user-provided putc callback for things like UART writes.

Solution 3:[3]

I add here my own implementation of (v)sprintf, but it does not provide float support (it is why I am here...).

However, it implements the specifiers c, s, d, u, x and the non standard ones b and m (binary and memory hexdump); and also the flags 0, 1-9, *, +.

#include <stdarg.h>
#include <stdint.h>

#define min(a,b) __extension__\
    ({ __typeof__ (a) _a = (a); \
       __typeof__ (b) _b = (b); \
       _a < _b ? _a : _b; })

enum flag_itoa {
    FILL_ZERO = 1,
    PUT_PLUS = 2,
    PUT_MINUS = 4,
    BASE_2 = 8,
    BASE_10 = 16,
};

static char * sitoa(char * buf, unsigned int num, int width, enum flag_itoa flags)
{
    unsigned int base;
    if (flags & BASE_2)
        base = 2;
    else if (flags & BASE_10)
        base = 10;
    else
        base = 16;

    char tmp[32];
    char *p = tmp;
    do {
        int rem = num % base;
        *p++ = (rem <= 9) ? (rem + '0') : (rem + 'a' - 0xA);
    } while ((num /= base));
    width -= p - tmp;
    char fill = (flags & FILL_ZERO)? '0' : ' ';
    while (0 <= --width) {
        *(buf++) = fill;
    }
    if (flags & PUT_MINUS)
        *(buf++) = '-';
    else if (flags & PUT_PLUS)
        *(buf++) = '+';
    do
        *(buf++) = *(--p);
    while (tmp < p);
    return buf;
}

int my_vsprintf(char * buf, const char * fmt, va_list va)
{
    char c;
    const char *save = buf;

    while ((c  = *fmt++)) {
        int width = 0;
        enum flag_itoa flags = 0;
        if (c != '%') {
            *(buf++) = c;
            continue;
        }
    redo_spec:
        c  = *fmt++;
        switch (c) {
        case '%':
            *(buf++) = c;
            break;
        case 'c':;
            *(buf++) = va_arg(va, int);
            break;
        case 'd':;
            int num = va_arg(va, int);
            if (num < 0) {
                num = -num;
                flags |= PUT_MINUS;
            }
            buf = sitoa(buf, num, width, flags | BASE_10);
            break;
        case 'u':
            buf = sitoa(buf, va_arg(va, unsigned int), width, flags | BASE_10);
            break;
        case 'x':
            buf = sitoa(buf, va_arg(va, unsigned int), width, flags);
            break;
        case 'b':
            buf = sitoa(buf, va_arg(va, unsigned int), width, flags | BASE_2);
            break;
        case 's':;
            const char *p  = va_arg(va, const char *);
            if (p) {
                while (*p)
                    *(buf++) = *(p++);
            }
            break;
        case 'm':;
            const uint8_t *m  = va_arg(va, const uint8_t *);
            width = min(width, 64); // buffer limited to 256!
            if (m)
                for (;;) {
                    buf = sitoa(buf, *(m++), 2, FILL_ZERO);
                    if (--width <= 0)
                        break;
                    *(buf++) = ':';
                }
            break;
        case '0':
            if (!width)
                flags |= FILL_ZERO;
            // fall through
        case '1'...'9':
            width = width * 10 + c - '0';
            goto redo_spec;
        case '*':
            width = va_arg(va, unsigned int);
            goto redo_spec;
        case '+':
            flags |= PUT_PLUS;
            goto redo_spec;
        case '\0':
        default:
            *(buf++) = '?';
        }
        width = 0;
    }
    *buf = '\0';
    return buf - save;
}

int my_sprintf(char * buf, const char * fmt, ...)
{
    va_list va;
    va_start(va,fmt);
    int ret = my_vsprintf(buf, fmt, va);
    va_end(va);
    return ret;
}

#if TEST
int main(int argc, char *argv[])
{
    char b[256], *p = b;
    my_sprintf(b, "%x %d %b\n", 123, 123, 123);
    while (*p)
        putchar(*p++);
}
#endif

Solution 4:[4]

tl;dr : Considering a smaller, but more complete, sprintf() implementation

https://github.com/eyalroz/printf

The standard library's sprintf() implementation you may be using is probably quite resource-taxing. But it's possible that you could avail yourself of a stand-alone sprintf() implementation, you would get more complete functionality without paying with so much memory use.

Now, why would you choose that if you've told us you only need some basic functionality? Because the nature of (s)printf() use is that we tend to use more aspects of it as we go along. You notice you want to print larger numbers, or differences in far decimal digits; you want to print a bunch of values and then decide you want them aligned. Or somebody else wants to use the printing capability you added to print something you haven't thought of. So, instead of having to switch implementations, you use an implementation where compile-time options configure which features get compiled and which get left out.

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 Jerry Coffin
Solution 2
Solution 3 calandoa
Solution 4