是否有格式处理器可以编写我自己的类似 printf 的函数并保留 %d 样式参数,而不使用 sprintf?

Is there a format processor to write my own printf-like function and keep the %d style arguments, without using sprintf?

我正在为 MCU 编写串行接口,我想知道如何创建类似 printf 的函数来写入串行 UART。我可以写入 UART,但为了节省内存和堆栈 space,并避免使用临时字符串缓冲区,我宁愿直接写入而不是对字符串执行 sprintf(),然后通过串行。没有内核,也没有文件处理,所以 FILE*fprintf() 那样的写法将不起作用(但 sprintf() 可以)。

有没有什么东西可以处理每个字符的格式化字符串,这样我就可以在解析格式字符串时逐个字符地打印出来,并应用相关参数?

我们正在使用 newlib as part of the efm32-base project

更新

我想指出,最终我们实现了 _write() 函数,因为这就是所有 newlib 需要点亮 printf 的地方。

根据您的标准库实现,您需要编写自己的 fputc_write 函数版本。

标准 C printf 函数族没有“打印到字符回调”类型的功能。大多数嵌入式平台也不支持 fprintf

首先尝试为您的平台挖掘 C 运行时,它可能有一个内置的解决方案。例如,ESP-IDF 有 ets_install_putc1(),它实际上为 printf 安装了一个回调(尽管它的 ets_printf 已经打印到 UART0)。

否则,还有其他 printf 专为嵌入式应用程序设计的实现,您可以根据自己的需要进行调整。

例如mpaland/printf有一个函数将字符打印机回调作为第一个参数:

int fctprintf(void (*out)(char character, void* arg), void* arg, const char* format, ...);

另请参阅此相关问题:Minimal implementation of sprintf or printf

您曾[在您的热门评论中]说过您拥有 GNU,因此 fopencookie 用于钩子 [我之前成功地使用过它]。

附加到 stdout 可能很棘手,但可行。

请注意我们有:FILE *stdout;(即它[只是]一个指针)。因此,只需将其设置为 [新] 打开的流就可以了。

所以,我认为你可以做到,或者 (1):

FILE *saved_stdout = stdout;

或 (2):

fclose(stdout);

然后,(3):

FILE *fc = fopencookie(...);
setlinebuf(fc);  // and whatever else ...
stdout = fc;

您可以 [可能] 调整顺序以适应(例如先 fclose 等)

我一直在寻找类似于 freopenfdopen 的东西来满足您的情况,但我没有找到任何东西,所以 stdout = ...; 可能是一个选择。

这很好如果没有有任何代码试图直接写入fd 1(例如write(1,"hello\n",6); ).

即使那样,也有办法。


更新:

Do you know if FILE*stdout is a const? If so, I might need to do something crazy like FILE **p = &stdout and then *p = fopencookie(...)

您的担心是对的,但不是您认为的原因。继续阅读...


stdout 是可写的 但是 ...

发帖前,我检查了stdio.h,它有:

extern FILE *stdout;        /* Standard output stream.  */

仔细想想,stdout一定是可写的

否则,我们永远做不到:

fprintf(stdout,"hello world\n");
fflush(stdout);

此外,如果我们执行 fork,然后 [在 child] 中,如果我们想设置 stdout 以转到日志文件,我们需要能够做到:

freopen("child_logfile","w",stdout);

所以,不用担心...


信任但验证 ...

我说过“不用担心”了吗?我可能早产了 ;-)

一个问题。

这是一个示例测试程序:

#define _GNU_SOURCE
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>

#if 1 || DEBUG
#define dbgprt(_fmt...) \
    do { \
        fprintf(stderr,_fmt); \
        fflush(stderr); \
    } while (0)
#else
#define dbgprt(_fmt...) \
    do { } while (0)
#endif

typedef struct {
    int ioport;
} uartio_t;

char *arg = "argument";

ssize_t
my_write(void *cookie,const char *buf,size_t len)
{
    uartio_t *uart = cookie;
    ssize_t err;

    dbgprt("my_write: ENTER ioport=%d buf=%p len=%zu\n",
        uart->ioport,buf,len);

    err = write(uart->ioport,buf,len);

    dbgprt("my_write: EXIT err=%zd\n",err);

    return err;
}

int
my_close(void *cookie)
{
    uartio_t *uart = cookie;

    dbgprt("my_close: ioport=%d\n",uart->ioport);
    int err = close(uart->ioport);
    uart->ioport = -1;

    return err;
}

int
main(void)
{

    cookie_io_functions_t cookie = {
        .write = my_write,
        .close = my_close
    };
    uartio_t uart;

    printf("hello\n");
    fflush(stdout);

    uart.ioport = open("uart",O_WRONLY | O_TRUNC | O_CREAT,0644);
    FILE *fc = fopencookie(&uart,"w",cookie);

    FILE *saved_stdout = stdout;
    stdout = fc;

    printf("uart simple printf\n");
    fprintf(stdout,"uart fprintf\n");
    printf("uart printf with %s\n",arg);

    fclose(fc);
    stdout = saved_stdout;

    printf("world\n");

    return 0;
}

程序输出:

编译后,运行:

./uart >out 2>err

这应该会产生预期的结果。 但是,我们得到(来自head -100 out err uart):

==> out <==
hello
uart simple printf
world

==> err <==
my_write: ENTER ioport=3 buf=0xa90390 len=39
my_write: EXIT err=39
my_close: ioport=3

==> uart <==
uart fprintf
uart printf with argument

哇!发生了什么? out 文件 应该 只是:

hello
world

而且,uart 文件 应该 行而不是 两行 :

uart printf
uart simple printf
uart printf with argument

但是,uart simple printf 行转到了 out 而不是 [预期的] uart 文件。

再次,哇!发生了什么?!?!


解释:

程序是用 gcc 编译的。使用 clang 重新编译会产生 期望的 结果!

事实证明,gcc 试图 提供帮助。编译时,转换为:

printf("uart simple printf\n");

进入:

puts("uart simple printf");

我们看到,如果我们反汇编可执行文件[或使用 -S 编译并查看 .s 文件]。

puts函数[显然]绕过stdout并使用glibc的内部版本:_IO_stdout.

glibc 的 puts 似乎是 _IO_puts 的弱别名 _IO_puts 并且使用 _IO_stdout.

_IO_* 符号不能 直接访问。它们就是 glibc 所说的“隐藏”符号——仅对 glibc.so 本身可用。


真正的修复:

我经过大量的黑客攻击后发现了这一点。那些 attempts/fixes 在下面的附录中。

事实证明,glibc 定义(例如)stdout 为:

FILE *stdout = (FILE *) &_IO_2_1_stdout_;

在内部,glibc 使用那个 内部 名称。因此,如果我们更改 stdout 指向的内容,它 会破坏 该关联。

其实只有_IO_stdout被隐藏了。版本化符号 全局的,但我们必须 知道 来自 readelf 输出或使用一些 __GLIBC_* 宏的名称(即有点乱)。

所以,我们需要将save/restore代码修改为不是改变stdout中的值而是memcpyto/from stdout 指向什么。

所以,在某种程度上,是正确的。 [有效] const [只读]​​.

所以,对于上面的sample/test程序,当我们要设置一个新的stdout时,我们要:

FILE *fc = fopencookie(...);
FILE saved_stdout = *stdout;
*stdout = *fc;

当我们要恢复原状时:

*fc = *stdout;
fclose(fc);
*stdout = saved_stdout;

所以,gcc 确实不是问题所在。原来我们开发的save/restore是不正确的。但是,它是潜伏的。只有当 gcc 调用 puts 时,错误才会显现出来。

个人说明:啊哈!现在我让这段代码开始工作了,它看起来很奇怪。我有一种 似曾相识 的体验。我很确定我过去也不得不这样做。但是,那是很久以前的事了,我完全忘记了。


semi-worked但更复杂的解决方法/修复:

注意: 如前所述,这些变通办法只是为了展示我在 找到上面的简单修复之前所做的尝试。

一种解决方法是禁用 gccprintfputs 的转换。

最简单的方法可能是 [如上所述] 编译 w第 clang。但是,有些网页说 clanggcc 做同样的事情。它 而不是 对我的 clang [for x86_64] 版本进行 puts 优化:7.0.1 -- YMMV

对于gcc ...

一个简单的方法是用-fno-builtins编译。这修复了 printf->puts 问题,但禁用了 memcpy 的 [理想] 优化,等等。它也是 未记录的 [AFAICT]

另一种方法是强制我们 自己的 版本的 puts 调用 fputs/fputc。我们将其放入(例如)puts.c 并针对它构建和 link:

#include <stdio.h>

int
puts(const char *str)
{
    fputs(str,stdout);
    fputc('\n',stdout);
}

当我们刚刚做的时候:stdout = fc;我们欺骗glibc有点[实际上,glibc在欺骗我们 一点] 现在又回来困扰我们了。

“干净”的方法是 freopen。但是,AFAICT,没有 类似的函数可以在 cookie 流上工作。 可能有一个,但我没找到。

因此,“肮脏”的方法之一可能是唯一的方法。我认为使用上面的“自定义”puts 函数方法是最好的选择。

编辑: 在我重读了上面的“欺骗”句子之后,我想到了简单的解决方案(即它让我更深入地研究了 glibc 来源) .