如何解决是否有格式处理器来编写我自己的类似 printf 的函数并保留 %d 样式参数,而不使用 sprintf?
我正在为 MCU 编写串行接口,我想知道如何创建类似 printf
的函数来写入串行 UART。我可以写入 UART,但为了节省内存和堆栈空间,并避免临时字符串缓冲区,我更愿意直接写入而不是将 sprintf()
写入字符串,然后通过串行写入字符串。没有内核,也没有文件处理,因此 FILE*
像来自 fprintf()
的那些写入将不起作用(但 sprintf()
可以)。
是否有一些东西可以为每个字符处理格式化的字符串,以便我可以在解析格式字符串并应用相关参数时逐个字符地打印?
解决方法
标准 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;
(即它[只是]一个指针)。因此,只需将其设置为 [newly] 打开的流应该可以工作。
所以,我认为你可以做到,要么(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
,那么[在子进程中]如果我们想设置 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,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,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_stdout
。
_IO_*
符号不能直接访问。它们是 glibc 所谓的“隐藏”符号——仅对 glibc.so
本身可用。
真正的解决办法:
经过大量的黑客攻击后,我发现了这一点。这些尝试/修复在下面的附录中。
事实证明,glibc
将(例如)stdout
定义为:
FILE *stdout = (FILE *) &_IO_2_1_stdout_;
在内部,glibc
使用该内部名称。因此,如果我们更改 stdout
指向的内容,它会破坏这种关联。
实际上,只有_IO_stdout
是隐藏的。版本化符号是全局的,但我们必须知道名称,要么从 readelf
输出或使用一些 __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
编译。但是,有些网页说 clang
与 gcc
的作用相同。它不会对我的 puts
版本进行clang
优化 [for x86_64
]: 7.0.1 -- YMMV
对于gcc
...
一个简单的方法是用 -fno-builtins
编译。这修复了 printf->puts
问题,但禁用了 memcpy
等的 [理想] 优化。它也未记录 [AFAICT]
另一种方法是强制我们自己的版本的 puts
调用 fputs/fputc
。我们会将其放入(例如)puts.c
中,然后对其进行构建和链接:
#include <stdio.h>
int
puts(const char *str)
{
fputs(str,stdout);
fputc('\n',stdout);
}
当我们刚刚这样做时:stdout = fc;
我们欺骗 glibc
有点[实际上,glibc 有点欺骗了我们],这已经现在回来困扰我们。
“干净”的方法是做 freopen
。但是,AFAICT,没有 类似的功能适用于 cookie 流。 可能有,但我没找到。
因此,“脏”方法之一可能是唯一的方法。我认为使用上面的“自定义”puts
函数方法是最好的选择。
编辑:在我重读上面的“欺骗”句子后,我找到了简单的解决方案(即它让我更深入地研究了 glibc
源)。
根据您的标准库实现,您需要编写自己的 fputc
或 _write
函数版本。
版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。