【问题标题】:Resuming [vf]?nprintf after reaching the limit达到限制后恢复 [vf]?nprintf
【发布时间】:2010-12-14 23:06:42
【问题描述】:

我有一个使用 snprintf 和 vsnprintf 将字符串打印到缓冲区的应用程序。目前,如果它检测到溢出,它会将 > 附加到字符串的末尾,作为字符串被截断的标志,并向 stderr 打印警告。我正在尝试找到一种方法让它在另一个缓冲区中恢复字符串 [从它停止的地方]。

如果这是使用 strncpy,那就很容易了;我知道写入了多少字节,所以我可以从 *(p+bytes_written); 开始下一次打印。但是,使用 printf,我有两个问题;首先,格式说明符在最终字符串中可能会占用更多或更少的空间,就像在格式字符串中一样,其次,我的 valist 可能会被部分解析。

有人对此有简单的解决方案吗?

编辑:我可能应该澄清我正在使用内存有限+没有动态分配的嵌入式系统[即,我不想使用动态分配]。我可以打印 255 个字节的消息,但不能再打印了,尽管我可以打印任意数量的消息。但是,我没有在堆栈上分配大量内存的内存,而且我的打印函数需要是线程安全的,所以我不能只分配一个全局/静态数组。

【问题讨论】:

    标签: c printf


    【解决方案1】:

    我不认为你可以做你正在寻找的事情(除了通过将缓冲区重新分配到必要大小并再次执行整个操作的直接方法)。

    您列出的原因是造成此问题的几个原因,但真正的杀手是格式化程序可能在格式化参数时空间不足,并且没有合理的方法来重新启动它。

    例如,假设缓冲区中还剩下 3 个字节,格式化程序开始对值 -1234567 进行“%d”转换。它会将“-1\0”放入缓冲区,然后执行它需要执行的任何其他操作以返回您真正需要的缓冲区大小。

    除了您能够确定格式化程序正在处理的说明符之外,您还需要能够弄清楚,您需要在第二轮传递234567,而不是传递-1234567。我不同意你想出一个合理的方法来做到这一点。

    现在,如果您确实有不想从顶部重新启动操作的原因,您可能可以使用分解格式字符串的内容来包装 snprintf()/vsnprintf() 调用,只发送一个转换说明符一次并将结果连接到输出缓冲区。您必须想出一些方法让包装器在重试时保持某种状态,以便它知道从哪个转换规范中获取。

    因此,从某种意义上说,它可能是可行的,但要避免更简单的“完全重试”方案,似乎确实需要做大量的工作。我可以看到也许(也许)在你没有动态分配更大缓冲区的奢侈的系统上尝试这个(也许是嵌入式系统)。在这种情况下,我可能会争辩说,需要一个更简单/受限制范围的格式化程序,它不具备 printf() 格式化程序的所有灵活性并且可以处理重试(因为它们的范围更有限)。

    但是,伙计,我会非常努力地向那些说这是一项要求的人讲道理。

    编辑:


    实际上,我收回了其中的一部分。如果您愿意使用自定义版本的snprintf()(我们称之为snprintf_ex())我可以看到这是一个相对简单的操作:

    int snprintf_ex( char* s, size_t n, size_t skipChars, const char* fmt, ...);
    

    snprintf_ex()(及其配套函数,如vsnprintf())会将字符串格式化到提供的缓冲区中(像往常一样),但会跳过输出第一个skipChars字符。

    您可以使用编译器库中的源代码(或使用 Holger Weiss' snprintf() 之类的东西)作为起点,非常容易地进行装配。使用它可能看起来像:

    int bufSize = sizeof(buf);
    char* fmt = "some complex format string...";
    
    int needed = snprintf_ex( buf, bufSize, 0, fmt, arg1, arg2, etc, etc2);
    
    if (needed >= bufSize) {
        // dang truncation...
    
        // do whatever you want with the truncated bits (send to a logger or whatever)
    
        // format the rest of the string, skipping the bits we already got
        needed = snprintf_ex( buf, bufSize, bufSize - 1, fmt, arg1, arg2, etc, etc2);
    
        // now the buffer contains the part that was truncated before. Note that 
        //  you'd still need to deal with the possibility that this is truncated yet
        //  again - that's an exercise for the reader, and it's probably trickier to
        //  deal with properly than it might sound...
    }
    

    一个缺点(可能会或可能不会被接受)是格式化程序会从一开始就重新进行所有格式化工作 - 它只会丢弃它出现的第一个 skipChars 字符。如果我必须使用这样的东西,我认为这几乎肯定是可以接受的(当有人使用标准的snprintf() 系列函数处理截断时会发生这种情况)。

    【讨论】:

    • 我真的不介意提前包装,或者如果它位于格式字符串的中间,则稍微备份一下。我怀疑我可能需要稍微重写我的 vsnprintf 函数,以使用 va_list 玩游戏[传递它一个副本,并让它返回使用的数字,加上格式字符串的偏移量]。这不是真正的要求。现在,这个要求让我可以切断字符串,但它会是一个非常好的拥有。
    • 您的编辑是我今天早上开始编码的内容......但我发现了另一个问题。我需要做一个 va_arg() 来忽略已经处理的参数,但我需要格式字符串来知道如何处理它们。不是因为我在乎,而是因为我无法保证它们都将占用堆栈上的相同空间;我不能只弹出一个 4 字节类型并期望它覆盖一个 char 或 64 位指针 [不是我的环境还有这些...]
    • 我不确定我是否遵循您关于“需要执行 va_arg() 以忽略参数”的评论。我在想的是,每次调用 vsnprintf_ex() 函数都会从一开始就处理格式字符串(以及 va_list 中的任何关联 va_args)——只是第一个 skipChars 字符作为“输出”生成到缓冲区将被丢弃。没有理由跳过任何va_args - 它们每个都被再次处理,只是它们产生的东西可能会被丢弃。
    • 我正在考虑返回一个指向格式字符串中间点的指针。它也可能会节省一些时间;无需遍历整个字符串 [现在,每个 vanilla snprintf 需要 ~200-500us]。不过,丢弃可能更简单;我将处理这两种实现并进行比较。
    • 这可能会节省一些处理时间,但代价是编码更加复杂。如果调用者不是传入要跳过的字符数,而是传入一个不透明的上下文结构以供实现的私有使用,那么您甚至可以使它比我的“skipChars”想法更容易使用。这样,调用者就不需要跟踪内容被截断的位置——它可以由实现在内部进行管理。
    【解决方案2】:

    C99 函数 snprintf()vsnprintf() 都返回打印带有所有参数的整个格式字符串所需的字符数。

    如果您的实现符合 C99,您可以为输出字符串创建一个足够大的数组,然后根据需要处理它们。

    int chars_needed = snprintf(NULL, 0, fmt_string, v1, v2, v3, ...);
    char *buf = malloc(chars_needed + 1);
    if (buf) {
      snprintf(buf, chars_needed + 1, fmt_string, v1, v2, v3, ...);
      /* use buf */
      free(buf);
    } else {
      /* no memory */
    }
    

    【讨论】:

    • 如果该功能可用,也可以使用asprintf()进行自动分配;如果您无法使用 MS CRT,请使用 _scprintf() 确定字符串的大小并使用 sprintf() 填充缓冲区
    【解决方案3】:

    如果您使用的是 POSIX 系统(我猜您可能是因为您提到了线程),一个不错的解决方案是:

    首先尝试使用snprintf 将字符串打印到单个缓冲区。如果它没有溢出,你已经为自己节省了很多工作。

    如果这不起作用,创建一个新线程和一个管道(使用pipe() 函数),fdopen 管道的写入端,并使用vfprintf 写入字符串。从管道的读取端获取新线程read,并将输出字符串分解为 255 字节的消息。 vfprintf返回后关闭管道并加入线程。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2021-07-19
      • 1970-01-01
      相关资源
      最近更新 更多