custom sprintf in C

Viewed 135

I'm trying to write a custom sprintf to format strings, with no need to pass a variable to write output to.

What I'm doing is traversing the given string with a for loop, finding % char, moving a char forward, switch-case that next char

Here is the code:

utils.c:

#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <utils.h>

char *concat(char *p_format, ...) {
   char *p_concat_str = calloc(1, sizeof(char));

   va_list args;
   va_start(args, p_format);

   for (unsigned int i = 0; i < strlen(p_format); ++i) {
      if (p_format[i] == '%') {
         i++;

         void *p_arg_str = va_arg(args, char *);
         /* printf("%s\n", (char *)p_arg_str); */
         p_concat_str = realloc(p_concat_str, (strlen(p_arg_str) + 1));
         switch (p_format[i]) {
            case 's':
               strcat(p_concat_str, (char *)p_arg_str);
               /* printf("%s\n", (char *)p_arg_str); */
               break;
         }
      }
      p_concat_str = realloc(p_concat_str, i + 2);
      p_concat_str[i] = p_format[i];
      p_concat_str[i + 1] = '\0';
   }

   return p_concat_str;
}

main.c

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <utils.h>

int main() {
   char *p_world = "World";
        /* Hello World */
   char *p_str = concat("Hello %s", p_world);
   printf("Formatted str: %s | its len %lu\n", p_str, strlen(p_str));
   free(p_str);
   return 0;
}

But I can't realloc p_concat_str length and can't append the argument returned by va_arg to p_concat_str with strcat

This is what I got

-- Configuring done
-- Generating done
-- Build files have been written to: /home/prxvvy/workspace/cutils/cmake-build-debug
[2/2] Linking C executable cutils
Formatted str: Hello Ws | its len 8
2 Answers

you can use vsnprintf() inside your concat() function to detect length of output string. If you don't pass the destination buffer and its size, vsnprintf() will only count the length of the output string and return it.

I think this way is relatively optimal, less malloc/realloc will be required for the program and the code will be slightly better readable.

char *concat(char *p_format, ...)
{
    va_list args, tmp;
    va_start(args, p_format);

#ifdef va_copy
    va_copy(tmp, args);
#else
    memcpy(&tmp, &args, sizeof(va_list));
#endif

    int length = vsnprintf(0, 0, p_format, tmp);
    va_end(tmp);

    if (length <= 0)
    {
        va_end(args);
        return NULL
    }

    char *dst_buf = (char*)malloc(length + 1);
    if (dst_buf == NULL)
    {
        va_end(args);
        return NULL;
    }

    int bytes = vsnprintf(dst_buf, length + 1, p_format, args);
    va_end(args);

    if (bytes <= 0)
    {
        free(dst_buf);
        return NULL;
    }

    dst_buf[bytes] = '\0';
    return dst_buf;
}

P.S. The GNU extension of the C library contains the function vasprintf which can be used to achieve the same result:

#define _GNU_SOURCE
#include <stdio.h>

char* concat(const char *p_format, ...)
{
    va_list args;
    char *dst_buf = NULL;
    int length = 0;

    va_start(args, p_format);
    length = vasprintf(&dst_buf, p_format, args);
    va_end(args);

    if (length <= 0 && dst_buf)
    {
        free(dst_buf);
        return NULL;
    }

    dst_buf[length] = '\0';
    return dst_buf;
}

There are multiple problems in your code:

  • the size you pass to realloc is incorrect: instead of p_concat_str = realloc(p_concat_str, (strlen(p_arg_str) + 1)); you should write:

      p_concat_str = realloc(p_concat_str,
                             strlen(p_concat_str) + strlen(p_arg_str) + 1);
    
  • you always assume there is a string argument, before testing for the s conversion format.

  • you always attempt to concatenate the character from the format string, even if you handled a conversion format.

  • you reallocate p_concat_str = realloc(p_concat_str, i + 2); and copy the character at p_concat_str[i] for this but the destination string may be larger (or even smaller) if some %s conversions have been handled already.

Here is a modified using the same simplistic approach:

utils.h:

char *concat(const char *p_format, ...);

utils.c:

#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "utils.h"

char *concat(const char *p_format, ...) {
    char *p_concat_str = calloc(1, 1);
    if (!p_concat_str)
        return NULL;

    va_list args;
    va_start(args, p_format);
    size_t dlen = 0;
    for (size_t i = 0; p_format[i] != '\0'; ++i) {
        if (p_format[i] == '%' && p_format[i + 1] == 's') {
            const char *p_arg_str = va_arg(args, const char *);
            i++;
            if (p_arg_str == NULL)
                p_arg_str = "(null)";
            slen = strlen(p_arg_str);
            char *newp = realloc(p_concat_str, dlen + slen + 1);
            if (newp == NULL) {
                free(p_concat_str);
                p_concat_str = NULL;
                break;
            }
            p_concat_str = newp;
            memcpy(p_concat_str + dlen, p_arg_str, slen + 1);
        } else {
            char *newp = realloc(p_concat_str, dlen + 1 + 1);
            if (newp == NULL) {
                free(p_concat_str);
                p_concat_str = NULL;
                break;
            }
            p_concat_str = newp;
            p_concat_str[dlen++] = p_format[i];
            p_concat_str[dlen] = '\0';
        }
    }
    va_end(args);
    return p_concat_str;
}

main.c:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "utils.h"

int main() {
   char *p_world = "World";
        /* Hello World */
   char *p_str = concat("Hello %s", p_world);
   printf("Formatted str: %s | its len %zu\n", p_str, strlen(p_str));
   free(p_str);
   return 0;
}

You could improve the code by only reallocating the destination string when you reach a %s or the end of the string, concatenating a whole string fragment at a time.

If you want to handle more than just %s, instead of reinventing the wheel, you might use a wrapper on vnsprintf:

#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "utils.h"

char *vconcat(const char *format, va_list ap) {
    char buf[256];
    va_list args;
    int ret;
    char *str;

    va_copy(args, ap);
    ret = vsnprintf(buf, sizeof buf, format, args);
    va_end(args);

    if (ret < 0 || (str = malloc(ret + 1)) == NULL)
        return NULL;

    if (ret < (int)sizeof(buf)) {
        return memcpy(str, buf, ret + 1);
    } else {
        ret = vsnprintf(str, ret + 1, format, ap);
        if (ret < 0) {
            free(str);
            str = NULL;
        }
        return str;
    }
}

char *concat(const char *format, ...) {
    va_list args;
    char *str;

    va_start(args, format);
    str = vconcat(format, args);
    va_end(args);

    return str;
}
Related