【问题标题】:Using snprintf in a cross-platform application在跨平台应用程序中使用 snprintf
【发布时间】:2011-04-27 22:32:48
【问题描述】:

我正在编写一个预计可以使用所有主要编译器编译的 C 程序。目前我正在 Linux 机器上的 GCC 上进行开发,并将在提交代码之前在 MSVC 上进行编译。为了简化交叉编译,我使用-ansi-pedantic 标志进行编译。在我开始使用 C89 标准中不可用的 snprintf 之前,这一直很好。 GCC 可以在没有-ansi 开关的情况下编译它,但 MSVC 总是会失败,因为它不支持 C99。

所以我做了类似的事情,

#ifdef WIN32 
#define snprintf sprintf_s
#endif

这很好用,因为 snprintfsprintf_s 具有相同的签名。我想知道这是正确的方法吗?

【问题讨论】:

  • snprintf 不是任何平台中所有 C 的标准吗?
  • 没有。 snprintf 是 C99 标准的一部分。 MSVC 没有 C99 实现。
  • sprintf_s 不等价。 snprintf 返回要写入的字符数,而 sprintf_s 在截断时返回 -1。见this discussion
  • 查看类似帖子的答案link

标签: c cross-platform compilation portability


【解决方案1】:

不。你的方法注定要失败。

sqrtcos 具有相同的原型。您认为您可以在程序中交换它们并在更改之前/之后获得相同的行为吗?


您可能应该编写自己的 snprintf,或从 Internet (google is your friend) 下载一个实现并在 Linux 和 Windows 中使用。

【讨论】:

  • 这个答案没用。问题以不同的方式提出:由于 snprintf 和 sprintf_s 具有相同的签名,它们是否也具有相同的功能?你的回答基本上是说:我不知道。
【解决方案2】:

我发现this 使用_snprintf() 作为替代方案,如果缓冲区溢出保护实际触发,则涉及的问题。从我一眼看去,类似的警告适用于sprintf_s

你能看出问题吗?在 Linux 版本中,输出始终以空值结尾。在 MSVC 中,它不是。

更微妙的是 Linux 中的 size 参数和 MSVC 中的 count 参数之间的区别。前者是输出缓冲区的大小,包括终止空值,后者是要存储的最大字符数,不包括终止空值。

哦,别忘了给微软发一封邮件,要求他们支持当前的语言标准。 (我知道他们已经宣布他们没有计划支持 C99,但无论如何都要去管他们。他们应得的。)

归根结底,如果您想真正安全地使用它,则必须为 MSVC 提供自己的 snprintf()_snprintf()sprintf_s() 的包装以捕获其非标准行为)。

【讨论】:

  • 我建议围绕现有实现使用“合同包装器”(如答案中所述),而不是将任何第三方代码拖到您的项目中(如 pmg 建议的那样)。一个完整的*printf() 实现非常庞大。
  • 实际上一个完整的printf 实现可能非常小。通常我会同意你包装损坏的实现而不是重新实现它们的原则,但是由于 Windows 的 *printf 也有一些你不能轻易包装的损坏的东西(比如对 %s%ls 的反向解释变体),我想知道是否只是替换它可能是最好的方法。
  • 实际上你可以同时修复的另一件事是 MS 的不精确浮点打印。
  • naive *printf() 的实现可能非常小。一个完整的可以变得相当。我自己的实现(诚然以一种不太简洁的风格编写)已经有超过 500 行,支持 %e、%f、%g、宽字符或多字节格式字符串。
【解决方案3】:

最完整的答案(如果你愿意,你可以改进),把它贴在贴纸里

#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

【讨论】:

  • 相当肯定 inline 和可变参数函数是 C99 特性。
  • @Thomas printf() 是一个可变参数函数。它已经存在了一段时间......所以不,这不是 C99 的添加。
【解决方案4】:

如果你小心点,你的建议可能会奏效。问题是这两个函数的行为略有不同,如果这对你来说不是问题,你很好,否则考虑一个包装函数:

MSVC _snprintf 和官方 C99 (gcc,clang) snprintf 之间的区别:

返回值:

  • MSVC:如果缓冲区大小不足以写入所有内容,则返回 -1(不包括终止 null!)
  • GCC:如果缓冲区足够大,则返回将写入的字符数

写入字节数:

  • MSVC:尽量多写,如果没有空间就不要在最后写NULL
  • GCC:尽可能多地写入,始终写入终止 NULL(例外:buffer_size=0)

有趣的%n微妙: 如果您在代码中使用%n,MSVC 将使其未初始化!如果由于缓冲区太小而停止解析,则 GCC 将始终写入如果缓冲区足够大时会写入的字节数。

所以我的建议是使用vsnprintf / _vsnprintf 编写您自己的包装函数mysnprintf,它给出相同的返回值并在两个平台上写入相同的字节(注意:%n 更难修复)。

【讨论】:

  • 在下一层更容易包装。前段时间我不得不这样做,我使用的方法有一个内部函数,它将 alloca() 指定大小的缓冲区,并调用 vsnprintf;如果结果合适,则将其处理到其他存储,并且该函数将返回 OK。如果不是,该函数将返回“使用指定的更大缓冲区大小重试”代码。对于 gcc,'try-again' 大小是从 vsnprintf 返回已知的,对于 MSVC,它会根据一些启发式方法不断增加。
【解决方案5】:

您可以打开 MSVC 的 NUL 特殊文件并写入该文件。它总是会告诉您需要多少字节,并且不会写入任何内容。像这样:

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;
}

然后您应该知道要分配多少字节才能成功使用 sprintf。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2014-11-03
    相关资源
    最近更新 更多