【问题标题】:How multibyte string is converted to wide-character string in fxprintf.c in glibc?glibc中fxprintf.c中的多字节字符串如何转换为宽字符串?
【发布时间】:2017-02-18 13:30:21
【问题描述】:

目前glibc source of perror中的逻辑是这样的:

如果stderr 是定向的,则按原样使用它,否则dup() 它并在dup()'ed fd 上使用perror()

如果stderr 是面向宽的,则使用stdio-common/fxprintf.c 中的以下逻辑:

size_t len = strlen (fmt) + 1;
wchar_t wfmt[len];
for (size_t i = 0; i < len; ++i)
  {
    assert (isascii (fmt[i]));
    wfmt[i] = fmt[i];
  }
res = __vfwprintf (fp, wfmt, ap);

格式字符串通过以下代码转换为宽字符形式,我看不懂:

wfmt[i] = fmt[i];

另外,它使用isascii断言:

assert (isascii(fmt[i]));

但是在宽字符程序中格式字符串并不总是 ascii,因为我们可能使用 UTF-8 格式字符串,它可以包含非 7 位值。 为什么我们运行以下代码时没有断言警告(假设是UTF-8语言环境和UTF-8编译器编码)?

#include <stdio.h>
#include <errno.h>
#include <wchar.h>
#include <locale.h>
int main(void)
{
  setlocale(LC_CTYPE, "en_US.UTF-8");
  fwide(stderr, 1);
  errno = EINVAL;
  perror("привет мир");  /* note, that the string is multibyte */
  return 0;
}
$ ./a.out 
привет мир: Invalid argument

我们可以在面向宽的stderr 上使用dup() 使其不是面向宽的吗?在这种情况下,可以在不使用这种神秘转换的情况下重写代码,考虑到 perror() 只接受多字节字符串 (const char *s) 并且语言环境消息无论如何都是多字节的事实。

事实证明我们可以。下面的代码演示了这一点:

#include <stdio.h>
#include <wchar.h>
#include <unistd.h>
int main(void)
{
  fwide(stdout,1);
  FILE *fp;
  int fd = -1;
  if ((fd = fileno (stdout)) == -1) return 1;
  if ((fd = dup (fd)) == -1) return 1;
  if ((fp = fdopen (fd, "w+")) == NULL) return 1;
  wprintf(L"stdout: %d, dup: %d\n", fwide(stdout, 0), fwide(fp, 0));
  return 0;
}
$ ./a.out 
stdout: 1, dup: 0

顺便说一句,是否值得向 glibc 开发人员发布有关此改进的问题?


注意

使用dup() 在缓冲方面受到限制。我想知道在glibc中perror()的实现中是否考虑过。以下示例演示了此问题。 输出不是按照写入流的顺序完成的,而是按照缓冲区中数据被注销的顺序完成的。 注意,输出中的值顺序与程序中的顺序不一样,因为 fprintf 的输出是先注销的(因为“\n”),而 fwprintf 的输出在程序退出时会被注销。

#include <wchar.h>
#include <stdio.h>
#include <unistd.h>
int main(void)
{
  wint_t wc = L'b';
  fwprintf(stdout, L"%lc", wc);

  /* --- */

  FILE *fp;
  int fd = -1;
  if ((fd = fileno (stdout)) == -1) return 1;
  if ((fd = dup (fd)) == -1) return 1;
  if ((fp = fdopen (fd, "w+")) == NULL) return 1;

  char c = 'h';
  fprintf(fp, "%c\n", c);
  return 0;
}
$ ./a.out 
h
b

但是如果我们在fwprintf中使用\n,输出和程序中是一样的:

$ ./a.out 
b
h

perror() 设法侥幸逃脱,因为在 GNU libc 中 stderr 是无缓冲的。但它会在手动将stderr 设置为缓冲模式的程序中安全运行吗?


这是我建议给 glibc 开发者的补丁:

diff -urN glibc-2.24.orig/stdio-common/perror.c glibc-2.24/stdio-common/perror.c
--- glibc-2.24.orig/stdio-common/perror.c   2016-08-02 09:01:36.000000000 +0700
+++ glibc-2.24/stdio-common/perror.c    2016-10-10 16:46:03.814756394 +0700
@@ -36,7 +36,7 @@

   errstring = __strerror_r (errnum, buf, sizeof buf);

-  (void) __fxprintf (fp, "%s%s%s\n", s, colon, errstring);
+  (void) _IO_fprintf (fp, "%s%s%s\n", s, colon, errstring);
 }


@@ -55,7 +55,7 @@
      of the stream.  What is supposed to happen when the stream isn't
      oriented yet?  In this case we'll create a new stream which is
      using the same underlying file descriptor.  */
-  if (__builtin_expect (_IO_fwide (stderr, 0) != 0, 1)
+  if (__builtin_expect (_IO_fwide (stderr, 0) < 0, 1)
       || (fd = __fileno (stderr)) == -1
       || (fd = __dup (fd)) == -1
       || (fp = fdopen (fd, "w+")) == NULL)

【问题讨论】:

  • 请注意,wchar_t 与 UTF-8 中的字符串无关,因为它是一个 字符,能够将大于char 的编码空间表示为单一值。似乎假设perror() 的参数是全ASCII,不知道为什么。
  • @unwind wchar_t 本身使用内部编码(glibc中的UCS-4),但也有编译器编码(UTF-8在我的例子),它用于多字节字符串常量。但是fxprintf.c 中的代码以某种方式将格式字符串从 UTF-8 转换为 UCS-4(将其传递给__vfwprintf)。我完全不明白它是如何成功的。但最重要的是我想知道为什么会这样做。
  • 嗯...有趣,这种区别很少使用。我同意,for 循环不可能进行任何类型的转换,而不是“扔掉一些位”。

标签: c file-io io wchar-t widechar


【解决方案1】:

注意:在这篇文章中找到具体问题并不容易;总的来说,这篇文章似乎是在尝试讨论 glibc 的实现细节,在我看来,最好将其指向一个专门针对该库开发的论坛,例如 libc-alpha mailing list。 (或者参见https://www.gnu.org/software/libc/development.html 了解其他选项。)这种讨论对于StackOverflow 来说并不是一个很好的匹配,恕我直言。尽管如此,我还是试图回答我能找到的问题。

  1. wfmt[i] = fmt[i];如何从多字节转换为宽字符?

    其实代码是:

    assert(isascii(fmt[i]));
    wfmt[i] = fmt[i];
    

    这是基于 ascii 字符的数值与wchar_t 相同的事实。严格来说,情况不一定如此。 C标准规定:

    如果实现未定义__STDC_MB_MIGHT_NEQ_WC__,则在用作整数字符常量中的唯一字符时,基本字符集的每个成员都应具有与其值相等的代码值。 (§7.19/2)

    (gcc 没有定义那个符号。)

    但是,这仅适用于基本集中的字符,不适用于isascii 识别的所有字符。基本字符集包含 91 个可打印的 ascii 字符以及空格、换行符、水平制表符、垂直制表符和换页符。所以理论上有可能剩余的控制字符之一不会被正确转换。但是,在调用__fxprintf 时使用的实际格式字符串只包含基本字符集中的字符,因此在实践中这个迂腐的细节并不重要。

  2. 为什么我们执行perror("привет мир");时没有assert警告?

    因为只转换格式字符串,而格式字符串(即"%s%s%s\n")只包含ascii 字符。由于格式字符串包含%s(而不是%ls),因此在窄字符和宽字符方向上,参数都应该是char*(而不是wchar_t*)。

  3. 我们可以在面向宽的 stderr 上使用 dup() 使其不是面向宽的吗?

    这不是一个好主意。首先,如果流有方向,它也可能有一个非空的内部缓冲区。由于该缓冲区是 stdio 库的一部分,而不是底层 Posix fd 的一部分,因此它不会与重复的 fd 共享。因此 perror 打印的消息可能会插入到一些现有输出的中间。此外,多字节编码可能具有移位状态,并且输出流当前不处于初始移位状态。在这种情况下,输出 ascii 序列可能会导致输出乱码。

    在实际实现中,dup只对没有方向的流进行;这些流从未有任何输出指向它们,因此它们肯定仍处于初始移位状态,缓冲区为空(如果流被缓冲)。

  4. 是否值得向 glibc 开发人员发布有关此改进的问题?

    这取决于你,但不要在这里做。这样做的正常方法是提交错误。没有理由相信 glibc 开发人员会阅读 SO 问题,即使他们阅读了,也必须有人将问题复制到错误中,并复制任何提议的补丁。

【讨论】:

  • 关于 1. - 我相信 C11 中的 §6.3.1.3 在这里适用,并且必须正确转换 all ascii 代码。在此处查看 C11 的引用 stackoverflow.com/a/25080246/1487773
  • 特别是,这段代码能正常工作吗? wchar_t c=... if (c&gt;' ' &amp;&amp; c!=0177) { /* visible ASCII and all non-ASCII codes */ ...
  • @IgorLiferenko §6.3.1.3 处理将整数从一种整数类型转换为另一种整数类型,将char 中存储的整数7 转换为wchar_t 中存储的7 没有问题。但是,§6.3.1.3 不保证更多;特别是,它不保证 7 是警报的 wchar_t 代码 (\a)。这就是我的回答。 §7.19/2 保证' '\176 之间的所有值具有charwchar_t 的共同语义,但wchar_t 理论上可以将非ascii 字符放在小于32 的代码中(0 和空格除外) )。
  • 这只有在 wchar_t 使用任意字符集(不是 Unicode)时才有可能。但在这种情况下,' '\176 之间的值也不需要对应。为什么会有这种矛盾? UTF-8 是 Unicode 的有效编码。 UTF-8 的要求是它匹配\000-\177 范围内的ASCII。根据 UTF-8 的结构,所有的 ascii 码都是 Unicode 值,反之亦然。换言之,以下转换始终有效:wc = (wchar_t)c;c = (char)wc; 其中c 的类型为charwc 的类型为wchar_tc 包含ascii 代码。是真的吗?
  • @igor:不保证 char 值将被解释为 Ascii,多字节字符是 UTF-8,wchar_t 是任何 Unicode 编码,或者 charwchar_t是兼容的。如果您假设所有这些事情,则可以使用更广泛的兼容代码,但该代码并非唯一可移植的。
【解决方案2】:

它使用 isascii 断言。

没关系。你不应该调用这个函数。它是一个 glibc 内部的。注意名称前面的两个下划线。从 perror 调用时,有问题的参数是 "%s%s%s\n",它完全是 ASCII。

但格式字符串在宽字符程序中并不总是ascii,因为我们可能使用UTF-8

首先,UTF-8 与宽字符无关。其次,格式字符串始终为 ASCII,因为该函数仅由其他知道自己在做什么的 glibc 函数调用。

perror("привет мир");

这不是格式字符串,这是与实际格式字符串中的%s 之一对应的参数之一。

我们可以在面向宽的 stderr 上使用 dup()

您不能在 FILE* 上使用 dup,它在 POSIX 上运行 没有方向的文件描述符。

这是我建议给 glibc 开发者的补丁:

为什么?什么不工作?

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2017-04-16
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2019-10-10
    • 2011-10-05
    相关资源
    最近更新 更多