[英]Is there a format processor to write my own printf-like function and keep the %d style arguments, without using sprintf?
我正在为 MCU 编写串行接口,我想知道如何创建printf
类似 function 来写入串行 UART。 我可以写入 UART,但为了节省 memory 和堆栈空间,并避免临时字符串缓冲区,我宁愿直接写入而不是sprintf()
写入字符串,然后通过串行写入字符串。 没有 kernel 也没有文件处理,所以像fprintf()
那样的FILE*
写入将不起作用(但sprintf()
可以)。
是否有一些东西可以处理每个字符的格式化字符串,所以我可以在解析格式字符串时逐个打印字符,并应用相关的 arguments?
标准 C printf
系列函数没有“打印到字符回调”类型的功能。 大多数嵌入式平台也不支持fprintf
。
首先尝试为您的平台挖掘 C 运行时,它可能有一个内置的解决方案。 例如,ESP-IDF 有ets_install_putc1()
,它本质上为printf
安装了回调(尽管它的ets_printf
已经打印到 UART0)。
如果做不到这一点,还有专门为嵌入式应用程序设计的替代printf
实现,您可以根据自己的需要进行调整。
例如mpaland/printf有一个 function 将字符打印机回调作为第一个参数:
int fctprintf(void (*out)(char character, void* arg), void* arg, const char* format, ...);
另请参阅此相关问题: sprintf 或 printf 的最小实现。
你曾说过[在你的顶级评论中]你有 GNU,所以fopencookie
用于钩子 [我以前成功地使用过它]。
附加到stdout
可能很棘手,但可行。
请注意,我们有: FILE *stdout;
(即它[只是]一个指针)。 因此,只需将其设置为 [新] 打开的 stream 即可。
所以,我认为你可以做,或者(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);
),这可以正常工作。
即使在那种情况下,也可能有办法。
更新:
你知道 FILE*stdout 是否是一个常量吗? 如果是这样,我可能需要做一些疯狂的事情,比如 FILE **p = &stdout 然后 *p = fopencookie(...)
你的担心是对的,但并不是你想的那样。 继续阅读...
stdout
是可写的,但是......
在发布之前,我检查了stdio.h
,它有:
extern FILE *stdout; /* Standard output stream. */
如果您考虑一下, stdout
必须是可写的。
否则,我们永远无法做到:
fprintf(stdout,"hello world\n");
fflush(stdout);
此外,如果我们做了一个fork
,那么 [in the child] 如果我们想将stdout
设置为 go 到日志文件,我们需要能够做到:
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;
}
程序 output:
编译后,运行:
./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
function [显然]绕过stdout
并使用 glibc 的内部版本: _IO_stdout
。
似乎 glibc 的puts
是_IO_puts
的弱别名,并且使用_IO_stdout
。
_IO_*
符号不可直接访问。 它们是 glibc 所称的“隐藏”符号——仅对glibc.so
本身可用。
真正的修复:
经过大量的黑客攻击后,我发现了这一点。 这些尝试/修复在下面的附录中。
事实证明, glibc
将(例如) stdout
定义为:
FILE *stdout = (FILE *) &_IO_2_1_stdout_;
在内部, glibc
使用该内部名称。 因此,如果我们更改stdout
指向的内容,它就会破坏这种关联。
实际上,只有_IO_stdout
是隐藏的。 版本化符号是全局的,但我们必须从readelf
output 或使用一些__GLIBC_*
宏(即有点混乱)知道名称。
因此,我们需要修改保存/恢复代码以不更改stdout
中的值,而是将memcpy
更改为/从stdout
指向的内容。
所以,在某种程度上,你是对的。 它是[有效地] const
[只读]。
所以,对于上面的示例/测试程序,当我们想要设置一个新的stdout
时,我们想要:
FILE *fc = fopencookie(...);
FILE saved_stdout = *stdout;
*stdout = *fc;
当我们要恢复原状时:
*fc = *stdout;
fclose(fc);
*stdout = saved_stdout;
所以,问题真的不是gcc
。 我们开发的原始保存/恢复不正确。 但是,它是潜伏的。 只有当gcc
调用puts
时,错误才会显现出来。
个人说明:啊哈,现在我得到了这段代码。 它似乎奇怪地熟悉。 我有似曾相识的经历。 我很确定我过去也必须这样做。 但是,那是很久以前的事了,我已经完全忘记了。
半有效但更复杂的解决方法/修复:
注意:如前所述,这些解决方法只是为了展示我在找到上述简单修复之前尝试过的内容。
一种解决方法是禁用gcc
从printf
到puts
的转换。
最简单的方法可能是 [如前所述] 使用clang
编译。 但是,一些 web 页面说gcc
与clang
做同样的事情。 它没有对我的clang
[for x86_64
] 版本进行puts
优化:7.0.1 -- YMMV
对于gcc
...
一种简单的方法是使用-fno-builtins
进行编译。 这修复了printf->puts
问题,但禁用了memcpy
等的 [desirable] 优化。它也没有记录[AFAICT]
另一种方法是强制我们自己调用fputs/fputc
的puts
版本。 我们将其放入(例如) puts.c
并构建和链接它:
#include <stdio.h>
int
puts(const char *str)
{
fputs(str,stdout);
fputc('\n',stdout);
}
当我们刚刚这样做时: stdout = fc;
我们有点欺骗了glibc
[实际上,glibc 有点欺骗了我们],现在又回来困扰我们了。
“干净”的方法是做freopen
。 但是,AFAICT,没有类似的 function 可用于 cookie stream。 可能有,但我没找到。
因此,其中一种“肮脏”的方法可能是唯一的方法。 我认为使用上面的“自定义” puts
function 方法是最好的选择。
编辑:在我重读上面的“欺骗”句子之后,我找到了简单的解决方案(即它让我更深入地研究了glibc
源代码)。
根据您的标准库实现,您需要编写自己的fputc
或_write
函数版本。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.