是否有格式处理器可以编写我自己的类似 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
等)
我一直在寻找类似于 freopen
或 fdopen
的东西来满足您的情况,但我没有找到任何东西,所以 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
中的值而是memcpy
to/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但更复杂的解决方法/修复:
注意: 如前所述,这些变通办法只是为了展示我在 找到上面的简单修复之前所做的尝试。
一种解决方法是禁用 gcc
从 printf
到 puts
的转换。
最简单的方法可能是 [如上所述] 编译 w第 clang
。但是,有些网页说 clang
和 gcc
做同样的事情。它 而不是 对我的 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
来源) .
我正在为 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
等)
我一直在寻找类似于 freopen
或 fdopen
的东西来满足您的情况,但我没有找到任何东西,所以 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
中的值而是memcpy
to/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但更复杂的解决方法/修复:
注意: 如前所述,这些变通办法只是为了展示我在 找到上面的简单修复之前所做的尝试。
一种解决方法是禁用 gcc
从 printf
到 puts
的转换。
最简单的方法可能是 [如上所述] 编译 w第 clang
。但是,有些网页说 clang
和 gcc
做同样的事情。它 而不是 对我的 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
来源) .