11

I am writing a C program that is expected to be compiled with all major compilers. Currently I am developing on GCC on a linux machine and will compile on MSVC before committing the code. To make the cross-compiling easy, I am compiling with -ansi and -pedantic flags. This worked well until I started using snprintf which is not available in C89 standard. GCC can compile this without the -ansi switch but MSVC will fail always as it doesn't have C99 support.

So I did something like,

#ifdef WIN32 
#define snprintf sprintf_s
#endif

This works well because snprintf and sprintf_s has same signatures. I am wondering is this the correct approach?

Qix - MONICA WAS MISTREATED
  • 14,451
  • 16
  • 82
  • 145
Navaneeth K N
  • 15,295
  • 38
  • 126
  • 184

5 Answers5

17

I found this on using _snprintf() as an alternative, and the gotchas involved if the buffer overrun protection actually triggers. From what I could see at a quick glance, similar caveats apply to sprintf_s.

Can you see the problem? In the Linux version, the output is always null-terminated. In MSVC, it's not.

Even more subtle is the difference between the size parameter in Linux and count parameter in MSVC. The former is the size of the output buffer including the terminating null and the latter is the maximum count of characters to store, which excludes the terminating null.

Oh, and don't forget to send a mail to Microsoft demanding they support current language standards. (I know they already announced they have no plan to support C99, but bugger them anyway. They deserve it.)

Bottom line, if you want to play it really safe, you'll have to provide your own snprintf() (a wrapper around _snprintf() or sprintf_s() catching their non-standard behaviour) for MSVC.

DevSolar
  • 67,862
  • 21
  • 134
  • 209
  • I suggest a "contract wrapper" around existing implementations (as mentioned in the answer) instead of dragging any third-party code into your project (as suggested by pmg). A complete `*printf()` implementation is quite large. – DevSolar Oct 20 '10 at 11:15
  • Actually a complete `printf` implementation can be very small. Normally I would agree with your principle of wrapping broken implementations rather than reimplementing them, but since Windows' `*printf` also has some broken things you can't easily wrap away (like backwards interpretation of `%s` and `%ls` in the wide variants), I wonder if just replacing it might be the best approach. – R.. GitHub STOP HELPING ICE Oct 20 '10 at 12:25
  • Actually another thing you can fix at the same time is MS's inexact floating point printing. – R.. GitHub STOP HELPING ICE Oct 20 '10 at 12:27
  • 3
    A *naive* `*printf()` implementation can be very small. A *complete* one can become quite something. My own implementation (admittedly written in a not-very-terse style) has a bit over 500 lines already, *without* support for %e, %f, %g, wide chars, or multibyte format strings. – DevSolar Oct 20 '10 at 14:29
15

Your proposal can work if you are being careful. The problem is that both function behave slightly different, if that is not a problem for you, you are good to go, otherwise think about a wrapper function:

Differences between MSVCs _snprintf and official C99 (gcc,clang) snprintf:

Return value:

  • MSVC: return -1 if buffer size not enough to write everything (not including terminating null!)
  • GCC: return number of characters that would have been written if buffer large enough

Written bytes:

  • MSVC: write as much as possible, do not write NULL at end if no space left
  • GCC: write as much as possible, always write terminating NULL (exception: buffer_size=0)

Interesting %n subtlety: If you use %n in your code, MSVC will leave it unitialized! if it it stops parsing because buffer size is to small, GCC will always write number of bytes which would have been written if buffer would have been large enough.

So my proposal would be to write your own wrapper function mysnprintf using vsnprintf / _vsnprintf which gives same return values and writes the same bytes on both platforms (be careful: %n is more difficult to fix).

eci
  • 2,294
  • 20
  • 18
  • It's a little easier to wrap at the next level up. I had to do this a while ago, and the approach I used had an inner function which would alloca() a buffer of a specified size, and call vsnprintf; if the result fit, then it was disposed of to other storage, and the function would return OK. If not, the function would return a 'try again with specified larger buffer size' code. For gcc the 'try-again' size was known from the vsnprintf return, and for MSVC it would just keep increasing based on some heuristics. – greggo Mar 09 '17 at 19:09
1

You could open the NUL special file for MSVC and write to that. It will always tell you how many bytes are needed, and won't write to anything. Like so:

int main (int argc, char* argv[]) { 
  FILE* outfile = fopen("nul", "wb");
  int written;

  if(outfile == NULL) {
    fputs ("could not open 'nul'", stderr);
  }
  else {
    written = fprintf(outfile, "redirect to /dev/null");
    fclose(outfile);
    fprintf(stdout, "didn't write %d characters", written);
  }

  return 0;
}

You then should know how many bytes to allocate to use sprintf sucessfully.

Paul Humphreys
  • 338
  • 2
  • 9
0

the most complete answer (you can improve if you wish), put that into a sticker

#if __PLATFORM_WIN_ZERO_STANDARD__

    static inline
    int LIBSYS_SNPRINTF(char * str, size_t size, const char * format, ...)
    {
        int retval;
        va_list ap;
        va_start(ap, format);
        retval = _vsnprintf(str, size, format, ap);
        va_end(ap);
        return retval;
    }

    static inline
    int LIBSYS_VASPRINTF(char **ret, char * format, va_list ap)
    {
        int wanted = vsnprintf(*ret = NULL, 0, format, ap);
        if((wanted > 0) && ((*ret = LIBSYS_MALLOC(1 + wanted)) != NULL)) {
            return vsprintf(*ret, format, ap);
        }
        return wanted;
    }

    static inline
    int LIBSYS_ASPRINTF(char **ret, char * format, ...)
    {
        int retval;
        va_list ap;
        va_start(ap, format);
        retval = LIBSYS_VASPRINTF(ret, format, ap);
        va_end(ap);
        return retval;
    }

#else
    #define LIBSYS_SNPRINTF snprintf
    #define LIBSYS_VASPRINTF vasprintf
    #define LIBSYS_ASPRINTF asprintf
#endif
-9

No. Your approach is doomed to failure.

sqrt and cos have the same prototype. Do you think you can swap them in a program and obtain the same behaviour from before / after the change?


You probably should write your own snprintf, or download an implementation from the internet (google is your friend) and use that both in Linux and Windows.

Qix - MONICA WAS MISTREATED
  • 14,451
  • 16
  • 82
  • 145
pmg
  • 106,608
  • 13
  • 126
  • 198
  • 8
    This answer is not useful. The question put in a different way: Since snprintf and sprintf_s have the same signature, do they also have the same functionality ? Your answer basically says: I don't know. – Ingo Blackman Jun 16 '15 at 17:35