glibc 知:手册06:字符集处理

1. 前言

The GNU C Library Reference Manual for version 2.35

2. 字符集处理

计算的早期使用的字符集每个字符只有 6、7 或 8 位:从来没有使用超过 8 位(一个字节)来表示单个字符的情况。随着越来越多的人处理非罗马字符集,这种方法的局限性变得更加明显,其中并非构成语言字符集的所有字符都可以用 2^8 个选项表示。本章展示了添加到 C 库以支持多个字符集的功能。

2.1. 扩展字符简介

有多种解决方案可用于克服字节和字符之间关系为 1:1 的字符集与比率为 2:1 或 4:1 的字符集之间的差异。本节的其余部分提供了一些示例,以帮助理解在开发 C 库的功能时所做的设计决策。

我们必须立即做出的区分是内部和外部表示之间的区别。内部表示是指程序使用的表示,同时将文本保存在内存中。当文本通过某种通信渠道存储或传输时,使用外部表示。外部表示的示例包括在目录中等待读取和解析的文件。

传统上,这两种表示方式之间没有区别。在内部和外部使用相同的单字节表示同样舒适和有用。这种舒适度会随着更多和更大的字符集而降低。

内部表示要克服的问题之一是处理使用不同字符集进行外部编码的文本。假设一个程序读取两个文本并使用一些度量比较它们。仅当文本在内部以通用格式保存时,比较才能有用地进行。

对于这样一种常见的格式(= 字符集),八位肯定已经不够用了。所以最小的实体必须增长:现在将使用宽字符。不是每个字符一个字节,而是使用两个或四个。(三个在内存中不好寻址,超过四个字节似乎没有必要)。

如本手册的其他部分所示,已经创建了一个全新的函数系列,可以处理内存中的宽字符文本。这种内部宽字符表示最常用的字符集是 Unicode 和 ISO 10646(也称为通用字符集的 UCS)。Unicode 最初计划为 16 位字符集;而 ISO 10646 被设计为 31 位大代码空间。这两个标准实际上是相同的。它们具有相同的字符库和代码表,但 Unicode 指定了附加语义。目前,只分配了前 0x10000 个代码位置(所谓的基本多语言平面,BMP)中的字符,但在这 16 位空间之外的更专业字符的分配已经在进行中。已为 Unicode 和 ISO 10646 字符定义了许多编码:UCS-2 是一个 16 位字,只能表示 BMP 中的字符,UCS-4 是一个 32 位字,不能表示任何 Unicode 和 ISO 10646 字符, UTF-8 是一种 ASCII 兼容编码,其中 ASCII 字符由 ASCII 字节表示,非 ASCII 字符由 2-6 个非 ASCII 字节的序列表示,最后 UTF-16 是 UCS-2 的扩展,其中某些对UCS-2 字可用于编码最大为 0x10ffff 的非 BMP 字符。

char 类型不适合表示宽字符。出于这个原因,ISO C 标准引入了一种新类型,旨在保留宽字符串中的一个字符。为了保持相似性,对于那些采用单个宽字符的函数,还有一个对应于 int 的类型。

数据类型:wchar_t

此数据类型用作宽字符串的基本类型。换句话说,这种类型的对象数组相当于多字节字符串的 char[]。该类型在 stddef.h 中定义。

引入 wchar_t 的 ISO C90 标准没有说明有关表示的任何具体内容。它只要求这种类型能够存储基本字符集的所有元素。因此,将 wchar_t 定义为 char 是合法的,这对嵌入式系统可能有意义。

但在 GNU C 库中,wchar_t 始终为 32 位宽,因此能够表示所有 UCS-4 值,因此涵盖了所有 ISO 10646。一些 Unix 系统将 wchar_t 定义为 16 位类型,因此非常遵循 Unicode严格。这个定义完全符合标准,但这也意味着要表示来自 Unicode 和 ISO 10646 的所有字符,必须使用 UTF-16 代理字符,这实际上是一种多宽字符编码。但是采用多宽字符编码与 wchar_t 类型的目的相矛盾。

数据类型:wint_t

wint_t 是一种用于包含单个宽字符的参数和变量的数据类型。顾名思义,这种类型在使用普通 char 字符串时相当于 int。如果 wchar_t 和 wint_t 类型的大小为 32 位宽,则它们通常具有相同的表示形式,但如果 wchar_t 定义为 char,则由于参数提升,类型 wint_t 必须定义为 int。

此类型在 wchar.h 中定义,并在 ISO C90 修正案 1 中引入。

与 char 数据类型一样,宏可用于指定 wchar_t 类型对象中可表示的最小值和最大值。

宏:wint_t WCHAR_MIN

宏 WCHAR_MIN 求值为 wint_t 类型的对象可表示的最小值。

这个宏是在 ISO C90 的修正案 1 中引入的。

宏:wint_t WCHAR_MAX

宏 WCHAR_MAX 求值为 wint_t 类型的对象可表示的最大值。

这个宏是在 ISO C90 的修正案 1 中引入的。

另一个特殊的宽字符值相当于 EOF。

宏:wint_t WEOF

宏 WEOF 计算为 wint_t 类型的常量表达式,其值不同于扩展字符集的任何成员。

WEOF 不必与 EOF 具有相同的值,并且与 EOF 不同,它也不必为负值。换句话说,草率的代码就像

{
  int c;while ((c = getc (fp)) < 0)}

当使用宽字符时,必须重写以显式使用 WEOF:

{
  wint_t c;while ((c = getwc (fp)) != WEOF)}

这个宏是在 ISO C90 修正案 1 中引入的,并在 wchar.h 中定义。

这些内部表示在存储和传输方面存在问题。因为每个单独的宽字符都包含一个以上的字节,所以它们会受到字节顺序的影响。因此,具有不同字节顺序的机器在访问相同数据时会看到不同的值。这种字节顺序问题也适用于所有基于字节的通信协议,因此要求发送者必须决定将宽字符拆分为字节。最后一点(但并非最不重要)是宽字符通常比自定义的面向字节的字符集需要更多的存储空间。

由于上述所有原因,如果后者是 UCS-2 或 UCS-4,则通常使用与内部编码不同的外部编码。外部编码是基于字节的,可以根据环境和要处理的文本适当地选择。这种外部编码可以使用多种不同的字符集(这里不会详尽地介绍信息——相反,主要组的描述就足够了)。所有基于 ASCII 的字符集都满足一个要求:它们是“文件系统安全的”。这意味着字符’/'在编码中仅用于表示它自己。对于像 EBCDIC(Extended Binary Coded Decimal Interchange Code,IBM 使用的字符集系列)这样的字符集,情况有些不同,但是如果操作系统不能直接理解 EBCDIC,那么无论如何都必须首先转换参数到系统的调用.

  • 最简单的字符集是单字节字符集。最多只能有 256 个字符(对于 8 位字符集),这不足以涵盖所有语言,但可能足以处理特定文本。处理 8 位字符集很简单。这不适用于稍后介绍的其他类型,因此,一个使用的应用程序可能需要使用 8 位字符集。

  • ISO 2022 标准定义了一种扩展字符集的机制,其中一个字符可以由多个字节表示。这是通过将状态与文本相关联来实现的。可用于更改状态的字符可以嵌入到文本中。文本中的每个字节在每种状态下都可能有不同的解释。状态甚至可能会影响给定字节是单独代表一个字符,还是必须与更多字节组合。

    在 ISO 2022 的大多数使用中,定义的字符集不允许状态更改超过下一个字符。这具有很大的优势,即只要可以识别字符的字节序列的开头,就可以正确解释文本。使用此策略的字符集示例包括各种 EUC 字符集(由 Sun 的操作系统、EUC-JP、EUC-KR、EUC-TW 和 EUC-CN 使用)或 Shift_JIS(SJIS,一种日语编码)。

    但 也有字符集使用对多个字符有效且必须由另一个字节序列更改的状态。例如 ISO-2022-JP、ISO-2022-KR 和 ISO-2022-CN。

  • 使用罗马字母为其他语言修复 8 位字符集的早期尝试导致了类似 ISO 6937 的字符集。这里表示像尖音符号这样的字符的字节本身不会产生输出:必须将它们与其他字符组合以获得所需的结果.例如,字节序列 0xc2 0x61(无间距的重音符号,后跟小写的‘a’)得到“小 a 带重音符号”。要单独获得尖音符字符,必须写入 0xc2 0x20(非间距尖音符后跟一个空格)。

    诸如 ISO 6937 之类的字符集用于一些嵌入式系统,例如电传。

  • 无需转换内部使用的 Unicode 或 ISO 10646 文本,通常只需使用不同于 UCS-2/UCS-4 的编码即可。Unicode 和 ISO 10646 标准甚至指定了这样的编码:UTF-8。这种编码能够表示长度为 1 到 6 的字节串中的所有 ISO 10646 31 位。

    还有一些其他尝试对 ISO 10646 进行编码,例如 UTF-7,但 UTF-8 是当今唯一应该使用的编码。事实上,幸运的是,UTF-8 很快就会成为唯一必须支持的外部编码。它被证明是普遍可用的,唯一的缺点是它有利于罗马语言,因为如果为这些脚本使用特定的字符集,它会使其他脚本(西里尔文、希腊语、亚洲脚本)的字节字符串表示比必要的更长。Unicode 压缩方案等方法可以缓解这些问题。

剩下的问题是:如何选择要使用的字符集或编码。答案:你不能自己决定,它是由系统的开发者或大多数用户决定的。由于目标是互操作性,因此必须使用其他人使用的任何东西。如果没有限制,则选择基于预期用户圈的要求。换句话说,如果一个项目预计只在俄罗斯使用,那么使用 KOI8-R 或类似的字符集就可以了。但是,如果同时来自希腊的人参与其中,则应该使用允许所有人协作的字符集。

最广泛有用的解决方案似乎是:使用最通用的字符集,即 ISO 10646。使用 UTF-8 作为外部编码,用户无法充分使用自己的语言的问题已成为过去。

在这一点上,关于宽字符表示的选择的最后一条评论是必要的。我们在上面说过,自然的选择是使用 Unicode 或 ISO 10646。这不是 ISO C 标准所要求的,但至少是鼓励的。该标准至少定义了一个宏 __STDC_ISO_10646__,该宏仅在 wchar_t 类型编码 ISO 10646 字符的系统上定义。如果未定义此符号,则应避免对宽字符表示进行假设。如果程序员只使用 C 库提供的函数来处理宽字符串,那么与其他系统的兼容性应该不会有问题。

2.2. 字符处理函数概述

Overview about Character Handling Functions

一个 Unix C 库包含两个系列中的三组不同的函数来处理字符集转换。ISO C90 标准中指定了函数系列之一(最常用的),因此即使在 Unix 世界之外也是可移植的。不幸的是,这个家庭是最没用的。应尽可能避免使用这些功能,尤其是在开发库(而不是应用程序)时。

第二个函数系列是在早期的 Unix 标准 (XPG2) 中引入的,并且仍然是最新和最伟大的 Unix 标准的一部分:Unix 98。它也是最强大和最有用的函数集。但我们将从 ISO C90 修正案 1 中定义的功能开始。

2.3. 可重启的多字节转换函数

Restartable Multibyte Conversion Functions

ISO C 标准定义了将字符串从多字节表示形式转换为宽字符串的函数。有几个特点:

  • 假定用于多字节编码的字符集未指定为函数的参数。而是使用当前语言环境的 LC_CTYPE 类别指定的字符集;请参阅区域设置类别
  • 一次处理多个字符的函数需要以 NUL 结尾的字符串作为参数(即,除非可以在适当的位置添加 NUL 字节,否则无法转换文本块)。GNU C 库包含一些允许指定大小的标准扩展,但基本上它们也期望终止字符串。

尽管有这些限制,但 ISO C 函数可以在许多情况下使用。例如,在图形用户界面中,如果文本不是简单的 ASCII,具有要求文本以宽字符串显示的功能并不少见。文本本身可能来自带有翻译的文件,用户应该决定当前的语言环境,这决定了翻译,因此也决定了使用的外部编码。在这种情况下(以及许多其他情况),这里描述的功能是完美的。如果在执行转换时需要更多自由,请查看 iconv 函数(请参阅通用字符集转换)。

2.3.1. 选择转换及其属性

Selecting the conversion and its properties

我们在上面已经说过,当前为 LC_CTYPE 类别选择的语言环境决定了我们将要描述的函数执行的转换。每个语言环境都使用自己的字符集(作为 localedef 的参数给出),这是假定为外部多字节编码的字符集。GNU C 库中的宽字符集始终是 UCS-4。

每个多字节字符集的一个特征是表示一个字符可能需要的最大字节数。在编写使用转换函数的代码时,此信息非常重要(如下面的示例所示)。ISO C 标准定义了两个提供此信息的宏。

宏:int MB_LEN_MAX

MB_LEN_MAX 为任何受支持的语言环境中的单个字符指定多字节序列中的最大字节数。它是一个编译时常量,在 limits.h 中定义。

宏:int MB_CUR_MAX

MB_CUR_MAX 扩展为一个正整数表达式,它是当前语言环境中多字节字符的最大字节数。该值永远不会大于 MB_LEN_MAX。与 MB_LEN_MAX 不同,此宏不必是编译时常量,而在 GNU C 库中则不是。

MB_CUR_MAX 在 stdlib.h 中定义。

两个不同的宏是必要的,因为严格的 ISO C90 编译器不允许可变长度数组定义,但仍然希望避免动态分配。这段不完整的代码显示了问题:

{
  char buf[MB_LEN_MAX];
  ssize_t len = 0;

  while (! feof (fp))
    {
      fread (&buf[len], 1, MB_CUR_MAX - len, fp);
      /* … process buf */
      len -= used;
    }
}

预计内部循环中的代码在数组 buf 中总是有足够的字节来转换一个多字节字符。数组 buf 必须静态调整大小,因为许多编译器不允许可变大小。fread 调用确保 MB_CUR_MAX 字节在 buf 中始终可用。请注意,如果 MB_CUR_MAX 不是编译时常量,这不是问题。

2.3.2. 表示转换的状态

在本章的介绍中,有人说某些字符集使用有状态编码。也就是说,编码值在某种程度上取决于文本中的先前字节。

由于转换函数允许在多个步骤中转换文本,因此我们必须有一种方法可以将此信息从函数的一次调用传递到另一个函数调用。

数据类型:mbstate_t

mbstate_t 类型的变量可以包含有关从一个调用转换函数到另一个调用所需的转换状态的所有信息。

mbstate_t 在 wchar.h 中定义。它是在 ISO C90 的修正案 1 中引入的。

要使用 mbstate_t 类型的对象,程序员必须定义此类对象(通常作为堆栈上的局部变量)并将指向该对象的指针传递给转换函数。这样,如果当前多字节字符集是有状态的,转换函数可以更新对象。

没有特定的函数或初始化程序可以将状态对象置于任何特定状态。规则是对象应始终表示第一次使用之前的初始状态,这是通过使用如下代码清除整个变量来实现的:

{
  mbstate_t state;
  memset (&state, '\0', sizeof (state));
  /* from now on state can be used.  */}

在使用转换函数生成输出时,通常需要测试当前状态是否对应于初始状态。这是必要的,例如,决定是否发出转义序列以在某些序列点将状态设置为初始状态。通信协议通常需要这样做。

函数:int mbsinit (const mbstate_t *ps)

Preliminary: | MT-Safe | AS-Safe | AC-Safe | See POSIX Safety Concepts.

mbsinit函数判断ps指向的状态对象是否处于初始状态。如果 ps 是空指针或对象处于初始状态,则返回值非零。否则为零。

mbsinit 在 ISO C90 的修正案 1 中引入,并在 wchar.h 中声明。

使用 mbsinit 的代码通常如下所示:

{
  mbstate_t state;
  memset (&state, '\0', sizeof (state));
  /* Use state.  */if (! mbsinit (&state))
    {
      /* Emit code to return to initial state.  */
      const wchar_t empty[] = L"";
      const wchar_t *srcp = empty;
      wcsrtombs (outbuf, &srcp, outbuflen, &state);
    }}

发出转义序列以返回初始状态的代码很有趣。wcsrtombs 函数可用于确定必要的输出代码(请参阅转换多字节和宽字符串)。请注意,对于 GNU C 库,不需要执行从多字节文本到宽字符文本的转换的额外操作,因为宽字符编码不是有状态的。但是在任何标准中都没有提到禁止使 wchar_t 使用有状态编码。

2.3.3. 转换单个字符

Converting Single Characters

最基本的转换函数是那些处理单个字符的函数。请注意,这并不总是意味着单个字节。但是由于多字节字符集的子集通常由单字节序列组成,因此有一些函数可以帮助转换字节。通常,ASCII 是多字节字符集的子集。在这种情况下,每个 ASCII 字符代表自己,所有其他字符至少有一个超出 0 到 127 范围的第一个字节。

函数:wint_t btowc (int c)

Preliminary: | MT-Safe | AS-Unsafe corrupt heap lock dlopen | AC-Unsafe corrupt lock mem fd | See POSIX Safety Concepts.

btowc 函数(“字节到宽字符”)使用当前选择的 LC_CTYPE 类别的语言环境中的转换规则,将处于初始移位状态的有效单字节字符 c 转换为等效的宽字符。

如果 (unsigned char) c 不是有效的单字节多字节字符或 c 是 EOF,则函数返回 WEOF。

请注意仅在初始班次状态下测试 c 有效性的限制。不使用从中获取状态信息的 mbstate_t 对象,并且该函数也不使用任何静态状态。

btowc 函数是在 ISO C90 修正案 1 中引入的,并在 wchar.h 中声明。

尽管单字节值总是在初始状态下被解释的限制,但这个函数在大多数情况下实际上是有用的。大多数字符要么完全是单字节字符集,要么是 ASCII 的扩展。但是这样写代码是可能的(并不是说这个具体的例子很有用):

wchar_t *
itow (unsigned long int val)
{
  static wchar_t buf[30];
  wchar_t *wcp = &buf[29];
  *wcp = L'\0';
  while (val != 0)
    {
      *--wcp = btowc ('0' + val % 10);
      val /= 10;
    }
  if (wcp == &buf[29])
    *--wcp = L'0';
  return wcp;
}

为什么需要使用如此复杂的实现而不是简单地将 ‘0’ + val % 10 转换为宽字符?答案是不能保证可以对用于 wchar_t 表示的字符集的字符执行这种算术运算。在其他情况下,字节在编译时不是恒定的,因此编译器无法完成工作。在这种情况下,需要使用 btowc。

还有一个用于在另一个方向进行转换的功能。

函数:int wctob (wint_t c)

Preliminary: | MT-Safe | AS-Unsafe corrupt heap lock dlopen | AC-Unsafe corrupt lock mem fd | See POSIX Safety Concepts.

wctob 函数(“宽字符到字节”)将有效的宽字符作为参数。如果这个字符在初始状态的多字节表示正好是一个字节长,那么这个函数的返回值就是这个字符。否则返回值为 EOF。

wctob 在 ISO C90 的修正案 1 中引入,并在 wchar.h 中声明。

有更通用的函数可以将单个字符从多字节表示转换为宽字符,反之亦然。这些函数对多字节表示的长度没有限制,也不需要它处于初始状态。

函数:size_t mbrtowc (wchar_t *restrict pwc, const char *restrict s, size_t n, mbstate_t *restrict ps)

Preliminary: | MT-Unsafe race:mbrtowc/!ps | AS-Unsafe corrupt heap lock dlopen | AC-Unsafe corrupt lock mem fd | See POSIX Safety Concepts.

mbrtowc 函数(“多字节可重启为宽字符”)将 s 指向的字符串中的下一个多字节字符转换为宽字符,并将其存储在 pwc 指向的位置。根据当前为 LC_CTYPE 类别选择的语言环境执行转换。如果语言环境中使用的字符集的转换需要状态,则多字节字符串以 ps 指向的对象表示的状态进行解释。如果 ps 是空指针,则使用仅由 mbrtowc 函数使用的静态内部状态变量。

如果下一个多字节字符对应于空宽字符,则函数的返回值为 0,然后状态对象处于初始状态。如果接下来的 n 个或更少的字节构成正确的多字节字符,则返回值是从 s 开始的构成多字节字符的字节数。转换状态根据转换中消耗的字节进行更新。在这两种情况下,如果 pwc 不为空,宽字符(L’\0’ 或在转换中找到的字符)都存储在 pwc 指向的字符串中。

如果多字节字符串的前 n 个字节可能构成一个有效的多字节字符,但完成它所需的字节数超过 n 个,则函数的返回值为 (size_t) -2 并且没有值存储在 *pwc 中。转换状态已更新,所有 n 个输入字节都已消耗,不应再次提交。请注意,即使 n 的值大于或等于 MB_CUR_MAX 也会发生这种情况,因为输入可能包含冗余移位序列。

如果多字节字符串的前 n 个字节不可能形成有效的多字节字符,则不存储任何值,将全局变量 errno 设置为值 EILSEQ,并且函数返回 (size_t) -1。转换状态之后是未定义的。

如指定的那样,mbrtowc 函数可以处理包含嵌入空字节的多字节序列(这发生在 Unicode 编码中,例如 UTF-16),但 GNU C 库不支持这种多字节编码。当遇到空输入字节时,该函数将返回零,或返回 (size_t) -1) 并报告 EILSEQ 错误。iconv 函数可用于任意编码之间的转换。请参阅通用字符集转换接口

mbrtowc 在 ISO C90 的修正案 1 中引入,并在 wchar.h 中声明。

将多字节字符串复制为宽字符串同时将所有小写字符转换为大写的函数可能如下所示:

wchar_t *
mbstouwcs (const char *s)
{
  /* Include the null terminator in the conversion. */
  size_t len = strlen (s) + 1;
  wchar_t *result = reallocarray (NULL, len, sizeof (wchar_t));
  if (result == NULL)
    return NULL;

  wchar_t *wcp = result;
  mbstate_t state;
  memset (&state, '\0', sizeof (state));

  while (true)
    {
      wchar_t wc;
      size_t nbytes = mbrtowc (&wc, s, len, &state);
      if (nbytes == 0)
        {
          /* Terminate the result string. */
          *wcp = L'\0';
          break;
        }
      else if (nbytes == (size_t) -2)
        {
          /* Truncated input string. */
          errno = EILSEQ;
          free (result);
          return NULL;
        }
      else if (nbytes == (size_t) -1)
        {
          /* Some other error (including EILSEQ). */
          free (result);
          return NULL;
        }
      else
        {
          /* A character was converted. */
          *wcp++ = towupper (wc);
          len -= nbytes;
          s += nbytes;
        }
    }
  return result;
}

在内部循环中,单个宽字符存储在 wc 中,消耗的字节数存储在变量 nbytes 中。如果转换成功,则将宽字符的大写变体存储在结果数组中,并调整指向输入字符串的指针和可用字节数。如果 mbrtowc 函数返回零,则表示空输入字节尚未转换,因此必须将其显式存储在结果中。

上面的代码使用了这样一个事实,即转换结果中的宽字符永远不会超过多字节输入字符串中的字节数。这种方法对结果的大小产生了悲观的猜测,如果必须以这种方式构造许多宽字符串或者如果字符串很长,则由于输入字符串包含多字节字符而需要分配的额外内存可能很重要。分配的内存块可以在返回之前调整为正确的大小,但更好的解决方案可能是立即为结果分配适量的空间。不幸的是,没有直接从多字节字符串计算宽字符串长度的函数。但是,有一个功能可以完成部分工作。

函数:size_t mbrlen (const char *restrict s, size_t n, mbstate_t *ps)

Preliminary: | MT-Unsafe race:mbrlen/!ps | AS-Unsafe corrupt heap lock dlopen | AC-Unsafe corrupt lock mem fd | See POSIX Safety Concepts.

mbrlen 函数(“多字节可重新启动长度”)计算从 s 开始的最多 n 个字节的数量,这些字节构成下一个有效且完整的多字节字符。

如果下一个多字节字符对应 NUL 宽字符,则返回值为 0。如果接下来的 n 个字节构成有效的多字节字符,则返回属于该多字节字符字节序列的字节数。

如果前 n 个字节可能构成一个有效的多字节字符但该字符不完整,则返回值为 (size_t) -2。否则多字节字符序列无效,返回值为(size_t) -1。

多字节序列以 ps 指向的对象所代表的状态进行解释。如果 ps 是空指针,则使用 mbrlen 本地的状态对象。

mbrlen 在 ISO C90 的修正案 1 中引入,并在 wchar.h 中声明。

细心的读者现在会注意到 mbrlen 可以实现为

mbrtowc (NULL, s, n, ps != NULL ? ps : &internal)

这是真的,事实上官方规范中也提到了。如何使用此函数确定从多字节字符串创建的宽字符串的长度?它不能直接使用,但我们可以使用它定义一个函数 mbslen:

size_t
mbslen (const char *s)
{
  mbstate_t state;
  size_t result = 0;
  size_t nbytes;
  memset (&state, '\0', sizeof (state));
  while ((nbytes = mbrlen (s, MB_LEN_MAX, &state)) > 0)
    {
      if (nbytes >= (size_t) -2)
        /* Something is wrong.  */
        return (size_t) -1;
      s += nbytes;
      ++result;
    }
  return result;
}

该函数只为字符串中的每个多字节字符调用 mbrlen 并计算函数调用的次数。请注意,我们在这里使用 MB_LEN_MAX 作为 mbrlen 调用中的大小参数。这是可以接受的,因为 a) 这个值大于最长的多字节字符序列的长度 b) 我们知道字符串 s 以 NUL 字节结尾,它不能是任何其他多字节字符序列的一部分,但代表 NUL 的字符序列宽字符。因此,mbrlen 函数永远不会读取无效内存。

现在这个函数可用了(只是为了说明这一点,这个函数不是 GNU C 库的一部分)我们可以计算存储转换后的多字节字符串 s 所需的宽字符数

wcs_bytes = (mbslen (s) + 1) * sizeof (wchar_t);

请注意,mbslen 函数效率很低。使用 mbslen 实现 mbstouwcs 必须执行两次多字节字符输入字符串的转换,而且这种转换可能非常昂贵。所以有必要在做两次工作之前考虑使用更简单但不精确的方法的后果。

函数:size_t wcrtomb (char *restrict s, wchar_t wc, mbstate_t *restrict ps)

Preliminary: | MT-Unsafe race:wcrtomb/!ps | AS-Unsafe corrupt heap lock dlopen | AC-Unsafe corrupt lock mem fd | See POSIX Safety Concepts.

wcrtomb 函数(“可重新启动为多字节的宽字符”)将单个宽字符转换为对应于该宽字符的多字节字符串。

如果 s 是空指针,则该函数将存储在 ps 指向的对象(或内部 mbstate_t 对象)中的状态重置为初始状态。这也可以通过这样的调用来实现:

wcrtombs (temp_buf, L'\0', ps)

因为,如果 s 是一个空指针,wcrtomb 就像它写入一个内部缓冲区一样执行,该缓冲区保证足够大。

如果 wc 是 NUL 宽字符,则 wcrtomb 会在必要时发出一个移位序列,以使状态 ps 进入初始状态,然后是单个 NUL 字节,该字节存储在字符串 s 中。

否则,将一个字节序列(可能包括移位序列)写入字符串 s。仅当 wc 是有效的宽字符时才会发生这种情况(即,它在 LC_CTYPE 类别的语言环境选择的字符集中具有多字节表示)。如果 wc 不是有效的宽字符,则字符串 s 中不存储任何内容,errno 设置为 EILSEQ,ps 中的转换状态未定义,返回值为 (size_t) -1。

如果没有发生错误,函数返回存储在字符串 s 中的字节数。这包括表示移位序列的所有字节。

关于函数接口的一句话:没有参数指定数组s的长度。相反,该函数假定至少有 MB_CUR_MAX 字节可用,因为这是表示单个字符的任何字节序列的最大长度。所以调用者必须确保有足够的可用空间,否则会发生缓冲区溢出。

wcrtomb 在 ISO C90 的修正案 1 中引入,并在 wchar.h 中声明。

使用 wcrtomb 就像使用 mbrtowc 一样简单。以下示例将宽字符串附加到多字节字符串。同样,代码并不是真正有用(或正确),它只是在这里演示使用和一些问题。

char *
mbscatwcs (char *s, size_t len, const wchar_t *ws)
{
  mbstate_t state;
  /* Find the end of the existing string.  */
  char *wp = strchr (s, '\0');
  len -= wp - s;
  memset (&state, '\0', sizeof (state));
  do
    {
      size_t nbytes;
      if (len < MB_CUR_LEN)
        {
          /* We cannot guarantee that the next
             character fits into the buffer, so
             return an error.  */
          errno = E2BIG;
          return NULL;
        }
      nbytes = wcrtomb (wp, *ws, &state);
      if (nbytes == (size_t) -1)
        /* Error in the conversion.  */
        return NULL;
      len -= nbytes;
      wp += nbytes;
    }
  while (*ws++ != L'\0');
  return s;
}

首先,该函数必须找到当前在数组 s 中的字符串的结尾。strchr 调用非常有效地执行此操作,因为对多字节字符表示的要求是 NUL 字节除了表示自身(在此上下文中为字符串的结尾)外,永远不会被使用。

初始化状态对象后,进入循环,第一个任务是确保数组 s 中有足够的空间。如果至少没有可用的 MB_CUR_LEN 字节,我们将中止。这并不总是最优的,但我们别无选择。我们可用的字节数可能少于 MB_CUR_LEN,但下一个多字节字符也可能只有一个字节长。在 wcrtomb 调用返回时,决定缓冲区是否足够大已经太晚了。如果此解决方案不合适,则有一个非常缓慢但更准确的解决方案。

if (len < MB_CUR_LEN)
    {
      mbstate_t temp_state;
      memcpy (&temp_state, &state, sizeof (state));
      if (wcrtomb (NULL, *ws, &temp_state) > len)
        {
          /* We cannot guarantee that the next
             character fits into the buffer, so
             return an error.  */
          errno = E2BIG;
          return NULL;
        }
    }

在这里,我们执行可能会溢出缓冲区的转换,以便我们之后可以对缓冲区大小做出准确的决定。请注意新 wcrtomb 调用中目标缓冲区的 NULL 参数; 因为此时我们对转换后的文本不感兴趣,所以这是表达这一点的好方法。这段代码最不寻常的地方当然是转换状态对象的重复,但是如果需要更改状态以发出下一个多字节字符,我们希望在实际转换中执行相同的转换状态更改。因此,我们必须保留初始移位状态信息。

对于这个问题,肯定有更多甚至更好的解决方案。此示例仅用于教育目的。

2.3.4. 转换多字节和宽字符串

Converting Multibyte and Wide Character Strings

上一节中描述的函数一次只转换一个字符。在实际程序中执行的大多数操作都包括字符串,因此 ISO C 标准还定义了对整个字符串的转换。但是,定义的功能集非常有限;因此,GNU C 库包含一些可以在某些重要情况下提供帮助的扩展。

函数:size_t mbsrtowcs (wchar_t *restrict dst, const char **restrict src, size_t len, mbstate_t *restrict ps)

Preliminary: | MT-Unsafe race:mbsrtowcs/!ps | AS-Unsafe corrupt heap lock dlopen | AC-Unsafe corrupt lock mem fd | See POSIX Safety Concepts.。

mbsrtowcs 函数(“多字节字符串可重新启动为宽字符串”)将 *src 处的以 NUL 结尾的多字节字符串转换为等效的宽字符串,包括末尾的 NUL 宽字符。如果 ps 是空指针,则使用来自 ps 指向的对象或来自 mbsrtowcs 的内部对象的状态信息开始转换。在返回之前,状态对象被更新以匹配最后一个转换字符之后的状态。如果到达并转换了终止 NUL 字节,则该状态是初始状态。

如果dst不是空指针,则结果存放在dst指向的数组中;否则,转换结果不可用,因为它存储在内部缓冲区中。

如果 len 宽字符在到达输入字符串末尾之前存储在数组 dst 中,则转换停止并返回 len。如果 dst 是空指针,则永远不会检查 len。

函数调用过早返回的另一个原因是输入字符串是否包含无效的多字节序列。在这种情况下,全局变量 errno 设置为 EILSEQ 并且函数返回 (size_t) -1。

在所有其他情况下,该函数返回在此调用期间转换的宽字符数。如果 dst 不为空,则 mbsrtowcs 在 src 指向的指针中存储空指针(如果已到达输入字符串中的 NUL 字节)或最后一个转换的多字节字符之后的字节地址。

与 mbstowcs 一样,dst 参数可能是一个空指针,并且该函数可用于计算所需的宽字符数。

mbsrtowcs 在 ISO C90 的修正案 1 中引入,并在 wchar.h 中声明。

mbsrtowcs 函数的定义有一个重要的限制。如果想要用文本转换缓冲区,则 dst 必须是一个以 NUL 结尾的字符串的要求会带来问题。缓冲区通常不是以 NUL 结尾的字符串的集合,而是由换行符分隔的连续行集合。现在假设需要一个从缓冲区转换一行的函数。由于该行不是 NUL 终止的,源指针不能直接指向未修改的文本缓冲区。这意味着,要么在 mbsrtowcs 函数调用时将 NUL 字节插入适当的位置(这对于只读缓冲区或多线程应用程序是不可行的),要么将行复制到额外的缓冲区中它可以由 NUL 字节终止。请注意,通常不可能通过将参数 len 设置为任何特定值来限制要转换的字符数。由于不知道每个多字节字符序列的长度是多少字节,因此只能猜测。

在换行符之后 NUL 终止一行的方法仍然存在问题,这可能会导致非常奇怪的结果。正如上面对mbsrtowcs函数的描述中所说,在处理输入字符串末尾的NUL字节后,保证转换状态处于初始移位状态。但是这个 NUL 字节实际上并不是文本的一部分(即,原始文本中换行符之后的转换状态可能与初始移位状态不同,因此下一行的第一个字符使用这种状态进行编码)。但是用户永远无法访问有问题的状态,因为转换在 NUL 字节(重置状态)之后停止。今天使用的大多数有状态字符集都要求换行符后的移位状态是初始状态——但这并不是一个严格的保证。因此,简单地以 NUL 终止一段正在运行的文本并不总是一个适当的解决方案,因此永远不应该在一般使用的代码中使用。

通用转换接口(请参阅通用字符集转换)没有此限制(它仅适用于缓冲区,而不是字符串),并且 GNU C 库包含一组函数,这些函数采用附加参数指定从输入字符串。这样,上面 mbsrtowcs 示例的问题可以通过确定行长度并将此长度传递给函数来解决。

函数:size_t wcsrtombs (char *restrict dst, const wchar_t **restrict src, size_t len, mbstate_t *restrict ps)

Preliminary: | MT-Unsafe race:wcsrtombs/!ps | AS-Unsafe corrupt heap lock dlopen | AC-Unsafe corrupt lock mem fd | See POSIX Safety Concepts.

wcsrtombs 函数(“宽字符串可重新启动为多字节字符串”)将 *src 处的以 NUL 结尾的宽字符串转换为等效的多字节字符串,并将结果存储在 dst 指向的数组中。NUL 宽字符也被转换。如果 ps 是空指针,则转换从 ps 指向的对象或 wcsrtombs 本地的状态对象中描述的状态开始。如果 dst 是空指针,则转换照常执行,但结果不可用。如果输入字符串的所有字符都已成功转换并且 dst 不是空指针,则 src 指向的指针被分配一个空指针。

如果输入字符串中的宽字符之一没有等效的有效多字节字符,则转换提前停止,将全局变量 errno 设置为 EILSEQ,并返回 (size_t) -1。

过早停止的另一个原因是,如果 dst 不是空指针,并且下一个转换的字符对数组 dst 总共需要超过 len 个字节。在这种情况下(如果 dst 不是空指针),src 指向的指针在最后一个成功转换后立即被分配一个指向宽字符的值。

除了编码错误的情况,wcsrtombs 函数的返回值是所有多字节字符序列中的字节数,这些字符序列已经或将会(如果 dst 不是空值)存储在 dst 中。在返回之前,ps 所指向的对象(如果 ps 为空指针,则为内部对象)中的状态会被更新以反映上次转换后的状态。在终止 NUL 宽字符被转换的情况下,该状态是初始移位状态。

wcsrtombs 函数是在 ISO C90 修正案 1 中引入的,并在 wchar.h 中声明。

上面提到的 mbsrtowcs 函数的限制也适用于此。无法直接控制输入字符的数量。必须将 NUL 宽字符放在正确的位置,或者通过可用的输出数组大小(len 参数)间接控制消耗的输入。

函数:size_t mbsnrtowcs (wchar_t *restrict dst, const char **restrict src, size_t nmc, size_t len, mbstate_t *restrict ps)

Preliminary: | MT-Unsafe race:mbsnrtowcs/!ps | AS-Unsafe corrupt heap lock dlopen | AC-Unsafe corrupt lock mem fd | See POSIX Safety Concepts.

mbsnrtowcs 函数与 mbsrtowcs 函数非常相似。除了 nmc 是新的之外,所有参数都相同。返回值与 mbsrtowcs 相同。

这个新参数指定了多字节字符串中最多可以使用多少字节。换句话说,多字节字符串 *src 不需要以 NUL 结尾。但是如果在字符串的 nmc 第一个字节中找到 NUL 字节,则转换将停止。

与 mbstowcs 一样,dst 参数可能是一个空指针,并且该函数可用于计算所需的宽字符数。

这个函数是一个 GNU 扩展。它旨在解决上述问题。现在可以逐段转换具有多字节字符文本的缓冲区,而不必关心插入 NUL 字节以及 NUL 字节对转换状态的影响。

将多字节字符串转换为宽字符串并显示它的函数可以这样编写(这不是一个真正有用的示例):

void
showmbs (const char *src, FILE *fp)
{
  mbstate_t state;
  int cnt = 0;
  memset (&state, '\0', sizeof (state));
  while (1)
    {
      wchar_t linebuf[100];
      const char *endp = strchr (src, '\n');
      size_t n;

      /* Exit if there is no more line.  */
      if (endp == NULL)
        break;

      n = mbsnrtowcs (linebuf, &src, endp - src, 99, &state);
      linebuf[n] = L'\0';
      fprintf (fp, "line %d: \"%S\"\n", linebuf);
    }
}

调用 mbsnrtowcs 后状态没有问题。由于我们不会在从一开始就不存在的字符串中插入字符,并且我们仅将状态用于给定缓冲区的转换,因此更改状态没有问题。

函数:size_t wcsnrtombs (char *restrict dst, const wchar_t **restrict src, size_t nwc, size_t len, mbstate_t *restrict ps)

Preliminary: | MT-Unsafe race:wcsnrtombs/!ps | AS-Unsafe corrupt heap lock dlopen | AC-Unsafe corrupt lock mem fd | See POSIX Safety Concepts.

wcsnrtombs 函数实现了从宽字符串到多字节字符串的转换。它类似于 wcsrtombs,但就像 mbsnrtowcs 一样,它需要一个额外的参数,该参数指定输入字符串的长度。

转换的输入字符串 *src 中不超过 nwc 宽字符。如果输入字符串在前 nwc 字符中包含 NUL 宽字符,则转换在此停止。

wcsnrtombs 函数是 GNU 扩展,就像 mbsnrtowcs 在没有 NUL 终止的输入字符串可用的情况下提供帮助一样。

2.3.5. 一个完整的多字节转换示例

A Complete Multibyte Conversion Example

最后几节中给出的示例程序只是简短的,不包含所有的错误检查等。这里提供的是一个完整且有文档的示例。它具有 mbrtowc 功能,但使用其他功能应该很容易派生版本。

int
file_mbsrtowcs (int input, int output)
{
  /* Note the use of MB_LEN_MAX.
     MB_CUR_MAX cannot portably be used here.  */
  char buffer[BUFSIZ + MB_LEN_MAX];
  mbstate_t state;
  int filled = 0;
  int eof = 0;

  /* Initialize the state.  */
  memset (&state, '\0', sizeof (state));

  while (!eof)
    {
      ssize_t nread;
      ssize_t nwrite;
      char *inp = buffer;
      wchar_t outbuf[BUFSIZ];
      wchar_t *outp = outbuf;

      /* Fill up the buffer from the input file.  */
      nread = read (input, buffer + filled, BUFSIZ);
      if (nread < 0)
        {
          perror ("read");
          return 0;
        }
      /* If we reach end of file, make a note to read no more. */
      if (nread == 0)
        eof = 1;

      /* filled is now the number of bytes in buffer. */
      filled += nread;

      /* Convert those bytes to wide characters–as many as we can. */
      while (1)
        {
          size_t thislen = mbrtowc (outp, inp, filled, &state);
          /* Stop converting at invalid character;
             this can mean we have read just the first part
             of a valid character.  */
          if (thislen == (size_t) -1)
            break;
          /* We want to handle embedded NUL bytes
             but the return value is 0.  Correct this.  */
          if (thislen == 0)
            thislen = 1;
          /* Advance past this character. */
          inp += thislen;
          filled -= thislen;
          ++outp;
        }

      /* Write the wide characters we just made.  */
      nwrite = write (output, outbuf,
                      (outp - outbuf) * sizeof (wchar_t));
      if (nwrite < 0)
        {
          perror ("write");
          return 0;
        }

      /* See if we have a real invalid character. */
      if ((eof && filled > 0) || filled >= MB_CUR_MAX)
        {
          error (0, 0, "invalid multibyte character");
          return 0;
        }

      /* If any characters must be carried forward,
         put them at the beginning of buffer. */
      if (filled > 0)
        memmove (buffer, inp, filled);
    }

  return 1;
}

2.4. 不可重入转换函数

Non-reentrant Conversion Function

前一章描述的函数在 ISO C90 的修正案 1 中定义,但原始 ISO C90 标准也包含字符集转换函数。之所以不先描述这些原始功能,是因为它们几乎完全没用。

问题是原始 ISO C90 中描述的所有转换函数都使用本地状态。使用本地状态意味着不能同时进行多个转换(不仅在使用线程时),并且您不能先转换单个字符然后再转换字符串,因为您无法告诉转换函数使用哪个状态。

因此,这些原始功能只能在非常有限的情况下使用。在开始一个新字符串之前必须完成整个字符串的转换,并且每个字符串/文本必须使用相同的函数进行转换(库本身没有问题;保证没有库函数改变任何这些函数的状态)。由于上述原因,强烈要求使用上一节中描述的函数来代替不可重入转换函数。

2.4.1. 单个字符的不可重入转换

Non-reentrant Conversion of Single Characters

函数:int mbtowc (wchar_t *restrict result, const char *restrict string, size_t size)

Preliminary: | MT-Unsafe race | AS-Unsafe corrupt heap lock dlopen | AC-Unsafe corrupt lock mem fd | See POSIX Safety Concepts.

当使用非空字符串调用 mbtowc(“多字节到宽字符”)函数时,将从字符串开始的第一个多字节字符转换为其对应的宽字符代码。它将结果存储在 *result 中。

mbtowc 从不检查超过 size 字节。(这个想法是提供你手头数据字节数的大小。)

带有非空字符串的 mbtowc 区分了三种可能性:字符串的第一个 size 字节以有效的多字节字符开头,它们以无效的字节序列或只是字符的一部分开头,或者字符串指向空字符串(空字符)。

对于有效的多字节字符,mbtowc 将其转换为宽字符并将其存储在 *result 中,并返回该字符中的字节数(始终至少为 1,并且永远不会超过 size)。

对于无效的字节序列,mbtowc 返回 -1。对于空字符串,它返回 0,同时将 ‘\0’ 存储在 *result 中。

如果多字节字符代码使用移位字符,则 mbtowc 在扫描时维护并更新移位状态。如果您使用字符串的空指针调用 mbtowc,则会将移位状态初始化为其标准初始值。如果使用的多字节字符代码实际上具有移位状态,它也会返回非零值。请参阅不可重入函数中的状态。

函数:int wctomb (char *string, wchar_t wchar)

Preliminary: | MT-Unsafe race | AS-Unsafe corrupt heap lock dlopen | AC-Unsafe corrupt lock mem fd | See POSIX Safety Concepts.

wctomb(“宽字符到多字节”)函数将宽字符代码 wchar 转换为其对应的多字节字符序列,并将结果存储在从 string 开始的字节中。最多存储 MB_CUR_MAX 个字符。

具有非空字符串的 wctomb 区分了 wchar 的三种可能性:有效的宽字符代码(可以转换为多字节字符的代码)、无效代码和 L’\0’。

给定一个有效代码,wctomb 将其转换为多字节字符,存储从 string 开始的字节。然后它返回该字符中的字节数(始终至少为 1,并且永远不会超过 MB_CUR_MAX)。

如果 wchar 是无效的宽字符代码,则 wctomb 返回 -1。如果 wchar 为 L’\0’,则返回 0,同时将 ‘\0’ 存储在 *string 中。

如果多字节字符代码使用移位字符,则 wctomb 在扫描时维护并更新移位状态。如果您使用字符串的空指针调用 wctomb,则将移位状态初始化为其标准初始值。如果使用的多字节字符代码实际上具有移位状态,它也会返回非零值。请参阅不可重入函数中的状态。

当 string 不为 null 时使用 wchar 参数为零调用此函数具有重新初始化存储的移位状态以及存储多字节字符 ‘\0’ 并返回 0 的副作用。

与 mbrlen 类似,还有一个计算多字节字符长度的不可重入函数。可以用mbtowc来定义。

函数:int mblen (const char *string, size_t size)

Preliminary: | MT-Unsafe race | AS-Unsafe corrupt heap lock dlopen | AC-Unsafe corrupt lock mem fd | See POSIX Safety Concepts.

带有非空字符串参数的 mblen 函数返回组成从 string 开始的多字节字符的字节数,从不检查超过 size 字节。(这个想法是提供你手头数据字节数的大小。)

mblen 的返回值区分了三种可能性:字符串的第一个 size 字节以有效的多字节字符开头,它们以无效的字节序列或字符的一部分开头,或者字符串指向空字符串(空字符)。

对于有效的多字节字符,mblen 返回该字符中的字节数(始终至少为 1 且永远不会超过 size)。对于无效的字节序列,mblen 返回 -1。对于空字符串,它返回 0。

如果多字节字符代码使用移位字符,则 mblen 在扫描时维护并更新移位状态。如果您使用字符串的空指针调用 mblen,则将移位状态初始化为其标准初始值。如果使用的多字节字符代码实际上具有移位状态,它也会返回一个非零值。请参阅不可重入函数中的状态

函数 mblen 在 stdlib.h 中声明。

2.4.2. 字符串的不可重入转换

Non-reentrant Conversion of Strings

为方便起见,ISO C90 标准还定义了转换整个字符串而不是单个字符的函数。这些函数与 ISO C90 修正案 1 中的可重入函数存在相同的问题;请参阅转换多字节和宽字符串。

函数:size_t mbstowcs (wchar_t *wstring, const char *string, size_t size)

Preliminary: | MT-Safe | AS-Unsafe corrupt heap lock dlopen | AC-Unsafe corrupt lock mem fd | See POSIX Safety Concepts.

mbstowcs(“多字节字符串到宽字符串”)函数将多字节字符串的空终止字符串转换为宽字符代码数组,将不超过 size 的宽字符存储到从 wstring 开始的数组中。终止空字符计入大小,因此如果大小小于由字符串产生的实际宽字符数,则不存储终止空字符。

从字符串到字符的转换从初始移位状态开始。

如果发现无效的多字节字符序列,则 mbstowcs 函数返回值 -1。否则,它返回存储在数组 wstring 中的宽字符数。此数字不包括终止空字符,如果数字小于大小,则会出现该字符。

这是一个示例,展示了如何转换多字节字符串,为结果分配足够的空间。

wchar_t *
mbstowcs_alloc (const char *string)
{
  size_t size = strlen (string) + 1;
  wchar_t *buf = xmalloc (size * sizeof (wchar_t));

  size = mbstowcs (buf, string, size);
  if (size == (size_t) -1)
    return NULL;
  buf = xreallocarray (buf, size + 1, sizeof *buf);
  return buf;
}

如果 wstring 是空指针,则不写入任何输出,转换如上进行,并返回结果。在实践中,这种行为对于计算转换字符串所需的宽字符的确切数量很有用。为 wstring 接受空指针的这种行为是 XPG4.2 扩展,在 ISO C 中未指定,在 POSIX 中是可选的。

函数:size_t wcstombs (char *string, const wchar_t *wstring, size_t size)

Preliminary: | MT-Safe | AS-Unsafe corrupt heap lock dlopen | AC-Unsafe corrupt lock mem fd | See POSIX Safety Concepts.

wcstombs(“宽字符串到多字节字符串”)函数将以空字符结尾的宽字符数组 wstring 转换为包含多字节字符的字符串,存储不超过 size 个字节,从 string 开始,如果有空间,后跟一个空字符结尾。字符的转换从初始移位状态开始。

终止空字符计入大小,因此如果大小小于或等于 wstring 中所需的字节数,则不存储终止空字符。

如果找到与有效多字节字符不对应的代码,则 wcstombs 函数将返回值 -1。否则,返回值是存储在数组字符串中的字节数。此数字不包括终止空字符,如果数字小于大小,则会出现该字符。

2.4.3. 不可重入函数中的状态

States in Non-reentrant Functions

在一些多字节字符代码中,任何特定字节序列的含义是不固定的;这取决于同一字符串中较早出现的其他序列。通常只有几个序列可以改变其他序列的含义;这几个被称为移位序列,我们说它们为随后的其他序列设置了移位状态。

为了说明移位状态和移位序列,假设我们决定序列 0200(只有一个字节)进入日文模式,其中 0240 到 0377 范围内的字节对是单个字符,而 0201 进入 Latin-1 模式,其中从 0240 到 0377 范围内的单个字节是字符,并根据 ISO Latin-1 字符集进行解释。这是一个多字节代码,具有两种可选的移位状态(“日语模式”和“拉丁语模式”),以及指定特定移位状态的两个移位序列。

当使用的多字节字符代码具有移位状态时,mblen、mbtowc 和 wctomb 在扫描字符串时必须维护和更新当前的移位状态。要使其正常工作,您必须遵循以下规则:

  • 在开始扫描字符串之前,使用空指针调用该函数,该指针指向多字节字符地址,例如 mblen (NULL, 0)。这将移位状态初始化为其标准初始值。
  • 按顺序一次扫描一个字符的字符串。不要“备份”和重新扫描已经扫描的字符,不要穿插不同字符串的处理。

以下是遵循以下规则使用 mblen 的示例:

void
scan_string (char *s)
{
  int length = strlen (s);

  /* Initialize shift state.  */
  mblen (NULL, 0);

  while (1)
    {
      int thischar = mblen (s, length);
      /* Deal with end of string and invalid characters.  */
      if (thischar == 0)
        break;
      if (thischar == -1)
        {
          error ("invalid multibyte character");
          break;
        }
      /* Advance past this character.  */
      s += thischar;
      length -= thischar;
    }
}

当使用使用移位状态的多字节代码时,函数 mblen、mbtowc 和 wctomb 是不可重入的。但是,没有其他库函数调用这些函数,因此您不必担心移位状态会被神秘地改变。

2.5. 通用字符集转换

Generic Charset Conversion

本章到目前为止提到的转换函数都有一个共同点,即它们对函数不直接指定的字符集进行操作。使用的多字节编码由当前为 LC_CTYPE 类别选择的语言环境指定。宽字符集由实现固定(在 GNU C 库的情况下,它始终是 UCS-4 编码的 ISO 10646)。

当涉及到一般字符转换时,这当然有几个问题:

  • 对于源字符集和目标字符集都不是 LC_CTYPE 类别的语言环境字符集的每次转换,都必须使用 setlocale 更改 LC_CTYPE 语言环境。

    更改 LC_CTYPE 区域设置会给其余程序带来主要问题,因为更多的函数(例如,字符分类函数,请参阅字符分类)使用 LC_CTYPE 类别。

  • 由于 LC_CTYPE 选择是全局的并且由所有线程共享,因此不可能与不同字符集进行并行转换。

  • 如果源字符集和目标字符集都不是用于 wchar_t 表示的字符集,则使用上述函数转换文本至少需要两个步骤。必须选择源字符集作为多字节编码,将文本转换为 wchar_t 文本,选择目标字符集作为多字节编码,然后将宽字符文本转换为多字节(= 目标)字符集。

    即使这是可能的(不能保证),这也是一项非常累人的工作。此外,由于语言环境的稳定变化,它更受其他两个凸起点的影响。

XPG2 标准定义了一组全新的功能,没有这些限制。它们根本不与选定的语言环境耦合,并且它们对为源和目标选择的字符集没有任何限制。只有一组可用的转换限制了它们。该标准没有规定任何转换都必须可用。这种可用性是实施质量的衡量标准。

在下面的文本中,首先介绍 iconv 的接口,然后介绍转换函数。与其他实现的比较将显示出阻碍可移植应用程序的障碍。最后,对希望扩展转换功能的高级用户感兴趣的实现进行了描述。

2.5.1. 通用字符集转换接口

Generic Character Set Conversion Interface

这组函数遵循使用资源的传统循环:打开-使用-关闭。该接口由三个函数组成,每个函数实现一个步骤。

在描述接口之前,有必要介绍一种数据类型。就像其他 open-use-close 接口一样,这里介绍的函数使用句柄工作,并且 iconv.h 头文件为使用的句柄定义了一个特殊类型。

数据类型:iconv_t

此数据类型是 iconv.h 中定义的抽象类型。用户不得对这种类型的定义做出任何假设;它必须是完全不透明的。

可以使用 iconv 函数为这种类型的对象分配用于转换的句柄。不需要释放对象本身,但必须释放句柄所代表的转换。

第一步是创建句柄的函数。

函数:iconv_t iconv_open (const char *tocode, const char *fromcode)

Preliminary: | MT-Safe locale | AS-Unsafe corrupt heap lock dlopen | AC-Unsafe corrupt lock mem fd | See POSIX Safety Concepts.

在开始转换之前必须使用 iconv_open 函数。此函数采用的两个参数确定转换的源字符集和目标字符集,如果实现有可能执行这种转换,则函数返回一个句柄。

如果所需的转换不可用,则 iconv_open 函数返回 (iconv_t) -1。在这种情况下,全局变量 errno 可以具有以下值:

EMFILE

该进程已经打开了 OPEN_MAX 个文件描述符。

ENFILE

已达到打开文件的系统限制。

ENOMEM

没有足够的内存来执行操作。

EINVAL

不支持从 fromcode 到 tocode 的转换。

在不同的线程中使用相同的描述符来执行独立的转换是不可能的。与描述符关联的数据结构包括有关转换状态的信息。这不能通过在不同的转换中使用它来搞砸。

iconv 描述符就像一个文件描述符,因为每次使用都必须创建一个新的描述符。描述符并不代表从 fromset 到 toset 的所有转换。

iconv_open 的 GNU C 库实现对其他实现有一个重要的扩展。为了简化可用转换集的扩展,该实现允许将必要的文件与数据和代码一起存储在任意数量的目录中。这个扩展必须如何编写将在下面解释(参见 GNU C 库中的 iconv 实现)。这里重要的是要说 GCONV_PATH 环境变量中提到的所有目录只有在它们包含文件 gconv-modules 时才会被考虑。这些目录不一定需要由系统管理员创建。事实上,引入这个扩展是为了帮助用户编写和使用他们自己的新转换。当然,出于安全原因,这在 SUID 二进制文件中不起作用;在这种情况下,只考虑系统目录,通常是 prefix/lib/gconv。GCONV_PATH 环境变量在第一次调用 iconv_open 函数时被检查一次。以后对该变量的修改没有效果。

iconv_open 函数是在 X/Open 可移植性指南第 2 版中引入的。所有商业 Unices 都支持它,因为它是 Unix 品牌所必需的。但是,实施的质量和完整性差异很大。iconv_open 函数在 iconv.h 中声明。

iconv 实现可以将大数据结构与 iconv_open 返回的句柄相关联。因此,一旦执行了所有转换并且不再需要转换,释放所有资源至关重要。

函数:int iconv_close (iconv_t cd)

reliminary: | MT-Safe | AS-Unsafe corrupt heap lock dlopen | AC-Unsafe corrupt lock mem | See POSIX Safety Concepts.

iconv_close 函数释放与句柄 cd 关联的所有资源,这些资源必须由成功调用 iconv_open 函数返回。

如果函数调用成功,则返回值为 0。否则为 -1,并且 errno 设置得当。定义的错误是:

EBADF

转换描述符无效。

iconv_close 函数与 XPG2 中的其余 iconv 函数一起引入,并在 iconv.h 中声明。

该标准只定义了一种实际的转换函数。因此,它具有最通用的接口:它允许从一个缓冲区转换到另一个缓冲区。从文件到缓冲区的转换,反之亦然,甚至文件到文件的转换都可以在它之上实现。

函数:size_t iconv (iconv_t cd, char **inbuf, size_t *inbytesleft, char **outbuf, size_t *outbytesleft)

Preliminary: | MT-Safe race:cd | AS-Safe | AC-Unsafe corrupt | See POSIX Safety Concepts.

iconv 函数根据与描述符 cd 关联的规则转换输入缓冲区中的文本,并将结果存储在输出缓冲区中。可以连续多次为同一文本调用该函数,因为对于有状态字符集,必要的状态信息保存在与描述符关联的数据结构中。

输入缓冲区由 *inbuf 指定,它包含 *inbytesleft 字节。额外的间接对于将使用的输入传回给调用者是必要的(见下文)。需要注意的是,缓冲区指针是 char 类型,即使输入文本以宽字符编码,长度也以字节为单位。

输出缓冲区以类似的方式指定。*outbuf 指向缓冲区的开头,其中至少有 *outbytesleft 字节空间用于结果。缓冲区指针也是 char 类型,长度以字节为单位。如果 outbuf 或 *outbuf 是空指针,则执行转换但没有输出可用。

如果 inbuf 是空指针,iconv 函数会执行必要的操作以将转换状态置于初始状态。这显然是无状态编码的无操作,但如果编码有状态,这样的函数调用可能会将一些字节序列放入输出缓冲区,从而执行必要的状态更改。下一次调用 inbuf 不是空指针然后简单地从初始状态继续。程序员永远不要对转换是否必须处理状态做出任何假设,这一点很重要。即使输入和输出字符集不是有状态的,实现可能仍然必须保持状态。这是由于为 GNU C 库选择的实现,如下所述。因此,如果某些协议要求对输出文本执行此操作,则应始终执行 iconv 调用以重置状态。

转换因以下三个原因之一而停止。第一个是输入缓冲区中的所有字符都被转换。这实际上可能意味着两件事:要么输入缓冲区中的所有字节都被消耗,要么缓冲区末尾有一些字节可能形成一个完整的字符但输入不完整。停止的第二个原因是输出缓冲区已满。第三个原因是输入包含无效字符。

在所有这些情况下,输入和输出缓冲区的最后一次成功转换后的缓冲区指针存储在 inbuf 和 outbuf 中,每个缓冲区中的可用空间存储在 inbytesleft 和 outbytesleft 中。

由于在 iconv_open 调用中选择的字符集几乎可以是任意的,因此可能存在输入缓冲区包含有效字符的情况,这些字符在输出字符集中没有相同的表示。这种情况下的行为是未定义的。在这种情况下,GNU C 库的当前行为是立即返回错误。这当然不是最理想的解决方案;因此,未来的版本将提供更好的版本,但尚未完成。

如果输入缓冲区的所有输入都成功转换并存储在输出缓冲区中,则该函数返回执行的不可逆转换的次数。在所有其他情况下,返回值为 (size_t) -1 并且 errno 已正确设置。在这种情况下,inbytesleft 指向的值是非零的。

EILSEQ

由于输入中的字节序列无效,转换停止。调用后,*inbuf 指向无效字节序列的第一个字节。

E2BIG

转换停止,因为它用完了输出缓冲区中的空间。

EINVAL

由于输入缓冲区末尾的字节序列不完整,转换停止。

EBADF

cd 参数无效。

iconv 函数是在 XPG2 标准中引入的,并在 iconv.h 头文件中声明。

iconv 函数的定义总体来说还是不错的。它提供了相当灵活的功能。唯一的问题在于边界情况,即输入缓冲区末尾的不完整字节序列和无效输入。第三个问题,不是真正的设计问题,是选择转换的方式。该标准没有说明合法名称,即最小的可用转换集。我们将看到这如何对其他实现产生负面影响,如下所示。

2.5.2. 一个完整的iconv示例

A complete iconv example

下面的示例提供了一个常见问题的解决方案。鉴于人们知道系统对 wchar_t 字符串使用的内部编码,因此通常可以从文件中读取文本并将其存储在宽字符缓冲区中。使用 mbsrtowcs 可以做到这一点,但是我们遇到了上面讨论的问题。

int
file2wcs (int fd, const char *charset, wchar_t *outbuf, size_t avail)
{
  char inbuf[BUFSIZ];
  size_t insize = 0;
  char *wrptr = (char *) outbuf;
  int result = 0;
  iconv_t cd;

  cd = iconv_open ("WCHAR_T", charset);
  if (cd == (iconv_t) -1)
    {
      /* Something went wrong.  */
      if (errno == EINVAL)
        error (0, 0, "conversion from '%s' to wchar_t not available",
               charset);
      else
        perror ("iconv_open");

      /* Terminate the output string.  */
      *outbuf = L'\0';

      return -1;
    }

  while (avail > 0)
    {
      size_t nread;
      size_t nconv;
      char *inptr = inbuf;

      /* Read more input.  */
      nread = read (fd, inbuf + insize, sizeof (inbuf) - insize);
      if (nread == 0)
        {
          /* When we come here the file is completely read.
             This still could mean there are some unused
             characters in the inbuf.  Put them back.  */
          if (lseek (fd, -insize, SEEK_CUR) == -1)
            result = -1;

          /* Now write out the byte sequence to get into the
             initial state if this is necessary.  */
          iconv (cd, NULL, NULL, &wrptr, &avail);

          break;
        }
      insize += nread;

      /* Do the conversion.  */
      nconv = iconv (cd, &inptr, &insize, &wrptr, &avail);
      if (nconv == (size_t) -1)
        {
          /* Not everything went right.  It might only be
             an unfinished byte sequence at the end of the
             buffer.  Or it is a real problem.  */
          if (errno == EINVAL)
            /* This is harmless.  Simply move the unused
               bytes to the beginning of the buffer so that
               they can be used in the next round.  */
            memmove (inbuf, inptr, insize);
          else
            {
              /* It is a real problem.  Maybe we ran out of
                 space in the output buffer or we have invalid
                 input.  In any case back the file pointer to
                 the position of the last processed byte.  */
              lseek (fd, -insize, SEEK_CUR);
              result = -1;
              break;
            }
        }
    }

  /* Terminate the output string.  */
  if (avail >= sizeof (wchar_t))
    *((wchar_t *) wrptr) = L'\0';

  if (iconv_close (cd) != 0)
    perror ("iconv_close");

  return (wchar_t *) wrptr - outbuf;
}

此示例显示了使用 iconv 函数的最重要方面。它显示了如何使用对 iconv 的连续调用来转换大量文本。用户不必关心有状态的编码,因为函数会处理所有事情。

有趣的一点是 iconv 返回错误并且 errno 设置为 EINVAL 的情况。这并不是转换中的真正错误。每当输入字符集包含超过一个字节的某些字符的字节序列并且文本没有被单独处理时,就会发生这种情况。在这种情况下,多字节序列有可能被剪切。然后,调用者可以简单地读取剩余的片段,并将有问题的字节与来自输入的新字符一起提供给 iconv 并继续工作。在这样的事件之后,描述符中保存的内部状态不是未指定的,就像来自 ISO C 标准的转换函数的情况一样。

该示例还显示了在 iconv 中使用宽字符串的问题。正如上面对 iconv 函数的描述中所解释的,该函数始终采用指向 char 数组的指针,并且可用空间以字节为单位。在示例中,输出缓冲区是一个宽字符缓冲区;因此,我们使用 char * 类型的局部变量 wrptr,它在 iconv 调用中使用。

这看起来很无辜,但可能会在对对齐有严格限制的平台上导致问题。因此 iconv 的调用者必须确保传递的指针适合访问来自适当字符集的字符。由于在上述情况下,函数的输入参数是 wchar_t 指针,因此情况就是这样(除非用户在计算参数时违反了对齐方式)。但在其他情况下,特别是在编写不知道使用哪种类型的字符集并因此将文本视为字节序列的通用函数时,它可能会变得棘手。

2.5.3. 关于其他iconv实现的一些细节

Some Details about other iconv Implementations

这里不是真正讨论其他系统的 iconv 实现的地方,但有必要了解一下它们以编写可移植的程序。上面提到的 iconv 函数规范的问题可能会导致可移植性问题。

首先要注意的是,由于使用了大量的字符集,直接在 C 库中对转换进行编码肯定是不切实际的。因此,转换信息必须来自 C 库之外的文件。这通常通过以下一种或两种方式完成:

  • C 库包含一组通用转换函数,可以从数据文件中读取所需的转换表和其他信息。必要时会加载这些文件。

    这个解决方案是有问题的,因为它需要付出很大的努力才能应用于所有字符集(可能是无限集)。不同字符集的结构差异如此之大,以至于必须开发许多不同的表格处理功能变体。此外,这些函数的通用特性使它们比专门实现的函数慢。

  • C 库仅包含一个框架,可以动态加载目标文件并执行其中包含的转换函数。

    该解决方案提供了更大的灵活性。C 库本身只包含很少的代码,因此减少了一般的内存占用。此外,通过 C 库和可加载模块之间的文档化接口,第三方可以扩展可用的转换模块集。该解决方案的一个缺点是动态加载必须可用。

商业 Unices 中的一些实现实现了这些可能性的混合;大多数人只实施第二种解决方案。使用可加载模块将代码移出库本身并为扩展和改进敞开大门,但这种设计在某些平台上也受到限制,因为没有多少平台支持静态链接程序中的动态加载。因此,在没有此功能的平台上,无法在静态链接的程序中使用此接口。在 ELF 平台上,GNU C 库在这些情况下动态加载没有问题。因此,这一点没有实际意义。危险在于人们熟悉了这种情况而忘记了对其他系统的限制。

关于其他 iconv 实现的第二件事是可用转换的数量通常非常有限。一些实现在标准版本(不是特殊的国际或开发者版本)中提供最多 100 到 200 种转换可能性。这并不意味着支持 200 种不同的字符集;例如,从一个字符集到其他 10 个字符集的转换可能计为 10 次转换。再加上另一个方向,这使得一个字符集用尽了 20 种转换可能性。可以想象这些平台提供的薄覆盖。一些 Unix 供应商甚至只提供少量转换,这使得它们几乎无法用于所有用途。

这直接导致了第三点,也可能是最成问题的一点。iconv 转换函数在所有已知的 Unix 系统上实现的方式以及从字符集 A 到 B 的转换函数以及从 B 到 C 的转换的可用性并不意味着从 A 到 C 的转换是可用的。

乍一看,这似乎不是不合理和有问题的,但这是一个相当大的问题,因为人们会在击中它后不久注意到它。为了说明这个问题,我们假设编写一个必须从 A 转换为 C 的程序。调用如下

cd = iconv_open ("C", "A");

根据上述假设失败。但是程序现在做什么呢?转换是必要的;因此,简单地放弃不是一种选择。

这是一个麻烦。iconv 函数应该处理这个问题。但是程序应该如何从这里开始呢?如果它尝试转换为字符集 B,首先调用两个 iconv_open

cd1 = iconv_open ("B", "A");

cd2 = iconv_open ("C", "B");

会成功,但如何找到B?

不幸的是,答案是:没有通用的解决方案。在某些系统上,猜测可能会有所帮助。在这些系统上,大多数字符集可以与 UTF-8 编码的 ISO 10646 或 Unicode 文本相互转换。除此之外,只有一些非常特定于系统的方法可以提供帮助。由于转换函数来自可加载模块,并且这些模块必须存储在文件系统中的某个位置,因此可以尝试找到它们并从可用文件中确定哪些转换可用以及是否存在从 A 到 C 的间接路由。

这个例子显示了上面提到的 iconv 的设计错误之一。至少应该可以以编程方式确定可用转换的列表,以便如果 iconv_open 说没有这样的转换,可以确保间接路由也是如此。

2.5.4. GNU C 库中的 iconv 实现

The iconv Implementation in the GNU C Library

在阅读了上一节中有关 iconv 实现的问题之后,一定要注意 GNU C 库中的实现没有上述问题。以下是对上述几点的分步分析。评估基于当前的开发状态(截至 1999 年 1 月)。iconv 功能的开发并不完整,但基本功能已经固化。

GNU C 库的 iconv 实现使用共享的可加载模块来实现转换。库本身内置了非常少量的转换,但这些只是相当微不足道的转换。

可加载模块的所有好处都在 GNU C 库实现中可用。这特别吸引人,因为接口有很好的文档(见下文),因此很容易编写新的转换模块。使用可加载对象的缺点在 GNU C 库中不是问题,至少在 ELF 系统上是这样。由于该库即使在静态链接的二进制文件中也能够加载共享对象,因此不必禁止静态链接以防万一想要使用 iconv。

提到的第二个问题是支持的转换数量。目前,GNU C 库支持超过 150 个字符集。实现的设计方式支持的转换次数大于 22350(150 乘以 149)。如果缺少从或到字符集的任何转换,可以轻松添加。

特别令人印象深刻的是,这个高数字是由于 iconv 的 GNU C 库实现没有上面提到的第三个问题(即,每当从字符集 A 到 B 以及从 B 到C 总是可以直接从 A 转换为 C)。如果 iconv_open 返回错误并将 errno 设置为 EINVAL,则无法直接或间接地执行所需的转换。

通过为每个字符集提供从和到 UCS-4 编码的 ISO 10646 的转换来实现三角剖分。使用 ISO 10646 作为中间表示可以进行三角剖分(即,使用中间表示进行转换)。

没有内在要求为新字符集提供到 ISO 10646 的转换,也可以在源字符集和目标字符集都不是 ISO 10646 的情况下提供其他转换。现有的转换集只是为了涵盖所有转换这可能是有趣的。

所有当前可用的转换都使用上面的三角测量方法,使转换运行不必要地慢。例如,如果有人经常需要从 ISO-2022-JP 到 EUC-JP 的转换,更快的解决方案是在两个字符集之间进行直接转换,首先跳过对 ISO 10646 的输入。与 ISO 10646 相比,这两个感兴趣的字符集彼此相似得多。

在这种情况下,人们可以轻松编写新的转换并将其作为更好的替代方案。如果指定为更高效,GNU C 库 iconv 实现将自动使用实现转换的模块。

2.5.4.1. gconv-modules 文件的格式

Format of gconv-modules files

有关可用转换的所有信息都来自名为 gconv-modules 的文件,该文件可以在 GCONV_PATH 的任何目录中找到。gconv-modules 文件是面向行的文本文件,其中每一行具有以下格式之一:

  • 如果第一个非空白字符是 #,则该行仅包含注释并被忽略。

  • 以 alias 开头的行定义字符集的别名。预计还有两个字。第一个词定义别名,第二个词定义字符集的原始名称。效果是可以在 iconv_open 的 fromset 或 toset 参数中使用别名,并达到与使用真实字符集名称时相同的结果。

    这非常重要,因为字符集通常有许多不同的名称。通常有一个官方名称,但这不必与最流行的名称相对应。除此之外,许多字符集都有以某种方式构造的特殊名称。例如,ISO 指定的所有字符集都具有 ISO-IR-nnn 形式的别名,其中 nnn 是注册号。这允许知道注册号的程序构造字符集名称并在 iconv_open 调用中使用它们。更多关于可用名称和别名的信息如下。

  • 以 module 开头的行介绍了一个可用的转换模块。这些行必须包含三个或四个以上的单词。

    第一个词指定源字符集,第二个词指定在此模块中实现的转换的目标字符集,第三个词是可加载模块的名称。文件名是通过附加通常的共享对象后缀(通常是 .so)来构造的,然后应该在 gconv-modules 文件所在的同一目录中找到该文件。该行的最后一个单词是可选的,是表示转换成本的数值。如果缺少该词,则假定成本为 1。数值本身并不重要。重要的是所有可能的转化路径的成本总和的相对值。下面是对成本值使用的更精确的描述。

回到上面的示例,其中一个人编写了一个模块来直接从 ISO-2022-JP 转换为 EUC-JP 并返回。所要做的就是将新模块(名称为 ISO2022JP-EUCJP.so)放在一个目录中,并在同一目录中添加一个包含以下内容的文件 gconv-modules:

module  ISO-2022-JP//   EUC-JP//        ISO2022JP-EUCJP    1
module  EUC-JP//        ISO-2022-JP//   ISO2022JP-EUCJP    1

要了解为什么这样就足够了,有必要了解 iconv 使用的转换(并在描述符中描述)是如何选择的。解决这个问题的方法非常简单。

在第一次调用 iconv_open 函数时,程序读取所有可用的 gconv-modules 文件并建立两个表:一个包含所有已知别名,另一个包含有关转换的信息以及哪个共享对象实现它们。

2.5.4.2. 在iconv中查找转化路径

Finding the conversion path in iconv

可用转换的集合形成具有加权边的有向图。边上的权重是 gconv-modules 文件中指定的成本。iconv_open 函数使用适合在此类图中搜索最佳路径的算法,因此构造了一个必须连续执行的转换列表,以获取从源字符集到目标字符集的转换。

解释为什么上述 gconv-modules 文件允许 iconv 实现将特定的 ISO-2022-JP 解析为 EUC-JP 转换模块,而不是库本身的转换很简单。由于后一种转换需要两个步骤(从 ISO-2022-JP 到 ISO 10646,然后从 ISO 10646 到 EUC-JP),因此成本是 1+1 = 2。但是,上面的 gconv-modules 文件指定新的转换模块可以执行此转换,成本仅为 1。

关于上面的 gconv-modules 文件(以及 GNU C 库附带的文件)的一个神秘项目是模块行中指定的字符集的名称。为什么几乎所有的名字都以 // 结尾?这还不是全部:名称实际上可以是正则表达式。在这个时间点,这个谜团不应该被揭开,除非你有相关的施法材料:原始 DOS 6.2 启动盘上的灰烬,被烧成的人像,受过圣艾玛克斯 (St. Emacs) 祝福的十字架,来自中美洲的各种草药根,沙子来自宿务等。对不起!使用它的实现部分尚未完成。现在请简单地遵循现有的例子。一旦它变得更清晰。–drepper

关于 gconv 模块的最后一点是关于不以 // 结尾的名称。经常提到一个名为 INTERNAL 的字符集。从上面的讨论和选择的名称应该已经很清楚,这是三角测量中间步骤中使用的表示的名称。我们说过这是 UCS-4,但实际上这并不完全正确。UCS-4 规范还包括使用的字节排序规范。由于 UCS-4 值由四个字节组成,因此存储的值受字节顺序的影响。如果处理器(或至少正在运行的进程)的字节顺序与 UCS-4 所需的字节顺序不同,则内部表示与 UCS-4 不同。这样做是出于性能原因,因为如果对实际查看 UCS-4 中的结果不感兴趣,则不想执行不必要的字节交换操作。为了避免字节序问题,内部表示始终命名为 INTERNAL,即使在表示相同的大端系统上也是如此。

2.5.4.3. iconv模块数据结构

iconv module data structures

到目前为止,本节已经描述了如何定位和考虑如何使用模块。剩下要描述的是模块的接口,以便人们可以编写新的。本节描述了 1999 年 1 月使用的接口。接口将来会发生一些变化,但幸运的是,只是以向上兼容的方式。

编写新模块所需的定义在非标准头文件 gconv.h 中公开可用。因此,以下文本描述了此头文件中的定义。然而,首先有必要获得一个概览。

从 iconv 用户的角度来看,该接口非常简单:iconv_open 函数返回一个可用于调用 iconv 的句柄,最后通过调用 iconv_close 释放该句柄。问题是句柄必须能够表示可能很长的转换步骤序列以及每次转换的状态,因为句柄是传递给 iconv 函数的全部内容。因此,数据结构确实是理解实现所必需的元素。

我们需要两种不同的数据结构。第一个描述转换,第二个描述状态等。在 gconv.h 中确实有两种类型定义。

数据类型:struct __gconv_step

该数据结构描述了模块可以执行的一种转换。对于带有转换函数的已加载模块中的每个函数,只有一个此类对象。此对象由转换的所有用户共享(即,此对象不包含与实际转换对应的任何信息;它仅描述转换本身)。

struct __gconv_loaded_object *__shlib_handle
const char *__modname
int __counter

结构的所有这些元素都在 C 库内部用于协调加载和卸载共享对象。不能期望任何其他元素可用或初始化。

const char *__from_name
const char *__to_name

__from_name 和 __to_name 包含源字符集和目标字符集的名称。它们可用于识别要执行的实际转换,因为一个模块可能实现多个字符集和/或方向的转换。

gconv_fct __fct
gconv_init_fct __init_fct
gconv_end_fct __end_fct

这些元素包含指向可加载模块中的函数的指针。下面将解释该接口。

int __min_needed_from
int __max_needed_from
int __min_needed_to
int __max_needed_to;

这些值必须在模块的 init 函数中提供。__min_needed_from 值指定源字符集的字符至少需要多少字节。__max_needed_from 指定还包括可能的移位序列的最大值。

__min_needed_to 和 __max_needed_to 值的用途与 __min_needed_from 和 __max_needed_from 相同,但这次是针对目标字符集。

这些值必须准确,否则转换函数将出现问题或根本不起作用。

int __stateful

此元素还必须由 init 函数初始化。如果源字符集是有状态的,则 int __stateful 为非零。否则为零。

void *__data

该元素可以被模块中的转换函数自由使用。void *__data 可用于将额外信息从一个调用传递到另一个调用。void *__data 如果根本不需要,则不需要初始化。如果 void *__data 元素被分配了一个指向动态分配内存的指针(大概在 init 函数中),则必须确保 end 函数释放内存。否则应用程序会泄漏内存。

需要注意的是,此数据结构由本规范转换的所有用户共享,因此 __data 元素不得包含特定于转换函数的特定用途的数据。

数据类型:struct __gconv_step_data

这是包含特定于每个转换函数使用的信息的数据结构。

char *__outbuf
char *__outbufend

这些元素指定转换步骤的输出缓冲区。__outbuf 元素指向缓冲区的开头,而 __outbufend 指向缓冲区中最后一个字节之后的字节。转换函数不能假设缓冲区的大小,但可以安全地假设输出缓冲区中至少有一个完整字符的空间。

转换完成后,如果转换是最后一步,则必须修改 __outbuf 元素以指向写入缓冲区的最后一个字节,以指示有多少输出可用。如果此转换步骤不是最后一个,则不得修改元素。不得修改 __outbufend 元素。

int __is_last

如果此转换步骤是最后一个,则此元素非零。该信息对于递归是必需的。请参阅下面的转换函数内部描述。不得修改此元素。

int __invocation_counter

转换函数可以使用该元素查看已经发生了多少次转换函数调用。某些字符集在生成输出时需要一定的 prolog,通过将该值与零进行比较,可以确定它是否是第一次调用,因此是否应该发出 prolog。不得修改此元素。

int __internal_use

该元素是另一种很少使用但在某些情况下需要的元素。如果转换函数用于实现 mbsrtowcs 等,则它被分配一个非零值。(即不直接通过iconv接口使用该功能)。

这有时会有所不同,因为预计 iconv 函数用于翻译整个文本,而 mbsrtowcs 函数通常仅用于转换单个字符串,并且可能多次用于转换整个文本。

但是在这种情况下,我们在遵守字符集规范的某些规则时会遇到问题。一些字符集需要一个序言,对于整个文本,它必须只出现一次。如果使用多个 mbsrtowcs 调用来转换文本,则只有第一个调用必须添加 prolog。但是,由于 mbsrtowcs 的不同调用之间没有通信,因此转换函数没有可能发现这一点。iconv 调用序列的情况有所不同,因为句柄允许访问所需的信息。

int __internal_use 元素主要与 __invocation_counter 一起使用,如下所示:

if (!data->__internal_use
     && data->__invocation_counter == 0)
  /* Emit prolog.  */

不得修改此元素。

mbstate_t *__statep

__statep 元素指向 mbstate_t 类型的对象(请参阅表示转换的状态)。有状态字符集的转换必须使用 __statep 指向的对象来存储有关转换状态的信息。__statep 元素本身绝不能被修改。

mbstate_t __state

此元素绝不能直接使用。分配所需空间只是此结构的一部分。

2.5.4.4. iconv模块接口

iconv module interfaces

有了关于数据结构的知识,我们现在可以描述转换函数本身。要了解该接口,需要对 C 库中加载转换对象的功能有一些了解。

经常出现多次使用一次转换的情况(即,在一个程序运行期间,对同一组字符集有多个 iconv_open 调用)。mbsrtowcs 等人。GNU C 库中的函数也使用 iconv 功能,这进一步增加了相同函数的使用次数。

由于这种转换的多次使用,模块不会专门为一次转换加载。相反,一个模块一旦加载,就可以同时被任意数量的 iconv 或 mbsrtowcs 调用使用。转换功能特定信息和转换数据之间的信息拆分使这成为可能。最后一节展示了用于执行此操作的两种数据结构。

这当然也体现在模块必须提供的功能的接口和语义上。三个函数必须具有以下名称:

gconv_init

gconv_init 函数初始化转换函数特定的数据结构。使用此转换的所有转换都共享这个相同的对象,因此,不必将有关转换本身的状态信息存储在此处。如果一个模块实现了不止一次的转换,gconv_init 函数会被调用多次。

gconv_end

gconv_end 函数负责释放 gconv_init 函数分配的所有资源。如果无事可做,则可能缺少此功能。如果模块实现了多个转换并且 gconv_init 函数没有为所有转换分配相同的资源,则必须特别小心。

gconv

这是实际的转换函数。调用它来转换一个文本块。它传递由 gconv_init 初始化的转换步骤信息和转换数据,特定于转换函数的这种使用。

为三个模块接口函数定义了三种数据类型,这些数据类型定义了接口。

数据类型:int (*__gconv_init_fct) (struct __gconv_step *)

这指定了模块的初始化函数的接口。对于模块实现的每次转换,它只调用一次。

正如上面对 struct __gconv_step 数据结构的描述中所解释的,初始化函数必须初始化它的一部分。

__min_needed_from
__max_needed_from
__min_needed_to
__max_needed_to

这些元素必须分别初始化为源字符集中和目标字符集中的一个字符使用的最小和最大字节数的确切数字。如果字符都具有相同的大小,则最小值和最大值相同。

__stateful

如果源字符集是有状态的,则必须将此元素初始化为非零值。否则它必须为零。

如果初始化函数需要将一些信息传递给转换函数,则可以使用 __gconv_step 结构的 __data 元素进行此通信。但由于此数据由所有转换共享,因此转换函数不得对其进行修改。下面的示例显示了如何使用它。

#define MIN_NEEDED_FROM         1
#define MAX_NEEDED_FROM         4
#define MIN_NEEDED_TO           4
#define MAX_NEEDED_TO           4

int
gconv_init (struct __gconv_step *step)
{
  /* Determine which direction.  */
  struct iso2022jp_data *new_data;
  enum direction dir = illegal_dir;
  enum variant var = illegal_var;
  int result;

  if (__strcasecmp (step->__from_name, "ISO-2022-JP//") == 0)
    {
      dir = from_iso2022jp;
      var = iso2022jp;
    }
  else if (__strcasecmp (step->__to_name, "ISO-2022-JP//") == 0)
    {
      dir = to_iso2022jp;
      var = iso2022jp;
    }
  else if (__strcasecmp (step->__from_name, "ISO-2022-JP-2//") == 0)
    {
      dir = from_iso2022jp;
      var = iso2022jp2;
    }
  else if (__strcasecmp (step->__to_name, "ISO-2022-JP-2//") == 0)
    {
      dir = to_iso2022jp;
      var = iso2022jp2;
    }

  result = __GCONV_NOCONV;
  if (dir != illegal_dir)
    {
      new_data = (struct iso2022jp_data *)
        malloc (sizeof (struct iso2022jp_data));

      result = __GCONV_NOMEM;
      if (new_data != NULL)
        {
          new_data->dir = dir;
          new_data->var = var;
          step->__data = new_data;

          if (dir == from_iso2022jp)
            {
              step->__min_needed_from = MIN_NEEDED_FROM;
              step->__max_needed_from = MAX_NEEDED_FROM;
              step->__min_needed_to = MIN_NEEDED_TO;
              step->__max_needed_to = MAX_NEEDED_TO;
            }
          else
            {
              step->__min_needed_from = MIN_NEEDED_TO;
              step->__max_needed_from = MAX_NEEDED_TO;
              step->__min_needed_to = MIN_NEEDED_FROM;
              step->__max_needed_to = MAX_NEEDED_FROM + 2;
            }

          /* Yes, this is a stateful encoding.  */
          step->__stateful = 1;

          result = __GCONV_OK;
        }
    }

  return result;
}

该函数首先检查需要哪种转换。获取此函数的模块实现了四种不同的转换;可以通过比较名称来确定选择了哪一个。应始终在不注意大小写的情况下进行比较。

接下来,分配一个数据结构,其中包含有关选择哪种转换的必要信息。数据结构 struct iso2022jp_data 是本地定义的,因为在模块外部,根本不使用该数据。请注意,如果请求此模块支持的所有四种转换,则有四个数据块。

一件有趣的事情是步骤数据对象的 _min 和 _max 元素的初始化。一个 ISO-2022-JP 字符可以包含一到四个字节。因此 MIN_NEEDED_FROM 和 MAX_NEEDED_FROM 宏就是这样定义的。输出始终是内部字符集(又名 UCS-4),因此每个字符正好由四个字节组成。对于从 INTERNAL 到 ISO-2022-JP 的转换,我们必须考虑转义序列可能是切换字符集所必需的。因此,该方向的 __max_needed_to 元素被分配 MAX_NEEDED_FROM + 2。这考虑了转义序列发出切换信号所需的两个字节。两个方向的最大值不对称很容易解释:在读取 ISO-2022-JP 文本时,可以单独处理转义序列(即,不必处理真实字符,因为转义序列的效果可以记录在状态信息中)。另一个方向的情况则不同。由于通常不知道接下来是哪个字符,因此不能发出转义序列来提前更改状态。这意味着转义序列必须与下一个字符一起发出。因此,一个人需要更多的空间,而不仅仅是角色本身。

初始化函数的可能返回值是:

__GCONV_OK

初始化成功

__GCONV_NOCONV

模块不支持请求的转换。如果 gconv-modules 文件有错误,就会发生这种情况。

__GCONV_NOMEM

无法分配存储附加信息所需的内存。

在卸载模块之前调用的函数要容易得多。它通常无事可做;在这种情况下,可以完全忽略它。

数据类型:void (*__gconv_end_fct) (struct gconv_step *)

该函数的任务是释放初始化函数中分配的所有资源。因此,只有参数指向的对象的 __data 元素是有意义的。继续初始化函数的示例,终结函数如下所示:

void
gconv_end (struct __gconv_step *data)
{
  free (data->__data);
}

最重要的功能是转换功能本身,对于复杂的字符集,它会变得相当复杂。但由于这里不感兴趣,我们将只描述转换函数的可能框架。

数据类型:int (*__gconv_fct) (struct __gconv_step *, struct __gconv_step_data *, const char **, const char *, size_t *, int)

可以出于两个基本原因调用转换函数:转换文本或重置状态。从 iconv 函数的描述中可以看出为什么需要刷新模式。选择何种模式由第六个参数决定,一个整数。该参数为非零表示选择了刷新。

两种模式的共同点是可以找到输出缓冲区。有关此缓冲区的信息存储在转换步骤数据中。指向此信息的指针作为第二个参数传递给此函数。struct __gconv_step_data 结构的描述有更多关于转换步骤数据的信息。

刷新必须做什么取决于源字符集。如果源字符集不是有状态的,则无需执行任何操作。否则,该函数必须发出一个字节序列以使状态对象进入初始状态。一旦这一切发生,转换链中的其他转换模块必须获得相同的机会。可以根据第一个参数指向的step数据结构的__is_last元素判断是否有下一个step。

更有趣的模式是必须转换实际文本。在这种情况下,第一步是从输入缓冲区转换尽可能多的文本并将结果存储在输出缓冲区中。输入缓冲区的开始由第三个参数确定,它是指向引用缓冲区开始的指针变量的指针。第四个参数是指向缓冲区中最后一个字节之后的字节的指针。

如果字符集是有状态的,则必须根据当前状态执行转换。状态存储在步骤数据(第二个参数)的 __statep 元素指向的对象中。一旦输入缓冲区为空或输出缓冲区已满,转换就会停止。此时,第三个参数引用的指针变量必须指向最后处理的字节之后的字节(即,如果所有输入都被消耗,则该指针和第四个参数具有相同的值)。

现在发生的事情取决于这一步是否是最后一步。如果是最后一步,唯一要做的就是更新 step 数据结构的 __outbuf 元素,使其指向最后写入的字节之后。此更新为调用者提供了有关输出缓冲区中有多少文本可用的信息。此外,第五个参数指向的变量,即 size_t 类型,必须增加以不可逆方式转换的字符数(不是字节数)。然后,函数可以返回。

如果这一步不是最后一步,则后面的转换函数必须有机会完成它们的工作。因此,必须调用适当的转换函数。有关函数的信息存储在转换数据结构中,作为第一个参数传递。此信息和步骤数据存储在数组中,因此两种情况下的下一个元素都可以通过简单的指针运算找到:

int
gconv (struct __gconv_step *step, struct __gconv_step_data *data,
       const char **inbuf, const char *inbufend, size_t *written,
       int do_flush)
{
  struct __gconv_step *next_step = step + 1;
  struct __gconv_step_data *next_data = data + 1;

next_step 指针引用下一步信息,next_data 引用下一个数据记录。因此,下一个函数的调用将类似于以下内容:

  next_step->__fct (next_step, next_data, &outerr, outbuf,
                    written, 0)

但这还不是全部。一旦函数调用返回,转换函数可能还有更多工作要做。如果函数的返回值为 __GCONV_EMPTY_INPUT,则输出缓冲区中有更多可用空间。除非输入缓冲区为空,否则转换函数会重新开始并处理输入缓冲区的其余部分。如果返回值不是 __GCONV_EMPTY_INPUT,则说明出了问题,我们必须从中恢复。

转换函数的要求是输入缓冲区指针(第三个参数)始终指向以转换形式放入输出缓冲区的最后一个字符。在当前步骤中执行转换后,这是微不足道的,但如果更下游的转换函数过早停止,则并非输出缓冲区中的所有字符都被消耗,因此,输入缓冲区指针必须退回到正确的位置。

如果输入和输出字符集对所有字符都具有固定宽度,则更正输入缓冲区很容易。在这种情况下,我们可以计算输出缓冲区中剩余的字符数,因此可以通过类似的计算适当地纠正输入缓冲区指针。如果任何一个字符集都有用可变长度字节序列表示的字符,事情就会变得棘手,如果转换必须处理状态,事情就会变得更加复杂。在这些情况下,必须从初始转换之前的已知状态再次执行转换(即,如果需要,必须重置转换状态并且必须再次执行转换循环)。现在的不同之处在于它知道必须创建多少输入,并且转换可以在转换第一个未使用的字符之前停止。一旦完成,输入缓冲区指针必须再次更新并且函数可以返回。

应该提到最后一件事。如果转换需要知道它是否是第一次调用(如果必须发出序言),则转换函数应在返回调用者之前增加 step 数据结构的 __invocation_counter 元素。有关如何使用它的更多信息,请参阅上面对 struct __gconv_step_data 结构的描述。

返回值必须是以下值之一:

__GCONV_EMPTY_INPUT

所有输入都被消耗,输出缓冲区中还有空间。

__GCONV_FULL_OUTPUT

输出缓冲区中没有更多空间。如果这不是最后一步,则该值将从链中下一个转换函数的调用向下传播。

__GCONV_INCOMPLETE_INPUT

输入缓冲区并非完全为空,因为它包含不完整的字符序列。

以下示例提供了转换函数的框架。如果必须编写新的转换,则必须填补此实现中的漏洞,仅此而已。

int
gconv (struct __gconv_step *step, struct __gconv_step_data *data,
       const char **inbuf, const char *inbufend, size_t *written,
       int do_flush)
{
  struct __gconv_step *next_step = step + 1;
  struct __gconv_step_data *next_data = data + 1;
  gconv_fct fct = next_step->__fct;
  int status;

  /* If the function is called with no input this means we have
     to reset to the initial state.  The possibly partly
     converted input is dropped.  */
  if (do_flush)
    {
      status = __GCONV_OK;

      /* Possible emit a byte sequence which put the state object
         into the initial state.  */

      /* Call the steps down the chain if there are any but only
         if we successfully emitted the escape sequence.  */
      if (status == __GCONV_OK && ! data->__is_last)
        status = fct (next_step, next_data, NULL, NULL,
                      written, 1);
    }
  else
    {
      /* We preserve the initial values of the pointer variables.  */
      const char *inptr = *inbuf;
      char *outbuf = data->__outbuf;
      char *outend = data->__outbufend;
      char *outptr;

      do
        {
          /* Remember the start value for this round.  */
          inptr = *inbuf;
          /* The outbuf buffer is empty.  */
          outptr = outbuf;

          /* For stateful encodings the state must be safe here.  */

          /* Run the conversion loop.  status is set
             appropriately afterwards.  */

          /* If this is the last step, leave the loop.  There is
             nothing we can do.  */
          if (data->__is_last)
            {
              /* Store information about how many bytes are
                 available.  */
              data->__outbuf = outbuf;

             /* If any non-reversible conversions were performed,
                add the number to *written.  */

             break;
           }

          /* Write out all output that was produced.  */
          if (outbuf > outptr)
            {
              const char *outerr = data->__outbuf;
              int result;

              result = fct (next_step, next_data, &outerr,
                            outbuf, written, 0);

              if (result != __GCONV_EMPTY_INPUT)
                {
                  if (outerr != outbuf)
                    {
                      /* Reset the input buffer pointer.  We
                         document here the complex case.  */
                      size_t nstatus;

                      /* Reload the pointers.  */
                      *inbuf = inptr;
                      outbuf = outptr;

                      /* Possibly reset the state.  */

                      /* Redo the conversion, but this time
                         the end of the output buffer is at
                         outerr.  */
                    }

                  /* Change the status.  */
                  status = result;
                }
              else
                /* All the output is consumed, we can make
                    another run if everything was ok.  */
                if (status == __GCONV_FULL_OUTPUT)
                  status = __GCONV_OK;
           }
        }
      while (status == __GCONV_OK);

      /* We finished one use of this step.  */
      ++data->__invocation_counter;
    }

  return status;
}

这些信息应该足以编写新模块。任何这样做的人也应该看看 GNU C 库源代码中的可用源代码。它包含许多工作和优化模块的示例。

3. 参考

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值