当前位置:Linux教程 - Linux - 在XFree86窗口系统中实现对GB18030的支持(一)

在XFree86窗口系统中实现对GB18030的支持(一)

苏哲 ([email protected])
Turbo Linux公司软件工程师
2001 年 9 月

GB18030 是最新的汉字编码字符集国家标准, 向下兼容 GBK 和 GB2312 标准。本文将向您讲述如何在 XFree86 窗口系统中实现对GB18030 标准的支持。
1. 简介

1.1 GB18030 简介
GB18030 是最新的汉字编码字符集国家标准, 向下兼容 GBK 和 GB2312 标准。 GB18030 编码是一二四字节变长编码。 一字节部分从 0x0~0x7F 与 ASCII 编码兼容。 二字节部分, 首字节从 0x81~0xFE, 尾字节从 0x40~0x7E 以及 0x80~0xFE, 与 GBK标准基本兼容。 四字节部分, 第一字节从 0x81~0xFE, 第二字节从 0x30~0x39, 第三和第四字节的范围和前两个字节分别相同。 四字节部分覆盖了从 0x0080 开始, 除去二字节部分已经覆盖的所有 Unicode 3.1 码位。 也就是说, GB18030 编码在码位空间上做到了与 Unicode 标准一一对应,这一点与 UTF-8 编码类似。

目前最新的 glibc 2.2.x 系列已经全面支持了 GB18030 Locale 和 GB18030 与 UCS-4 之间的编码转换, 也就是说在系统层上 Linux 已经可以支持 GB18030 标准了。 下面问题的关键就是怎样让 XFree86 窗口系统也支持 GB18030 标准。

1.2 XFree86 国际化原理简介。
本文介绍的所有内容都基于 XFree86 4.0.3 及其以后版本, XFree86 早期版本未做测试。

XFree86 窗口系统中涉及到国际化的部分主要有字体管理, 文字绘制, 编码转换以及进程间通讯。 具体的编程接口请参阅相关资料, 本文不作更详细的介绍。

1.2.1 字体管理与文字绘制
早期的 XFree86 由于对国际化问题考虑不足, 在字库接口上仅定义了单字节索引和双字节索引两种字库接口, 分别对应 XDrawString 和 XDrawString16 两组函数。 前一组函数使用8位字模索引序列, 后一组函数使用16位字模索引序列。 函数定义分别为:
XDrawString(display, d, gc, x, y, string, length)
Display *display;
Drawable d;
GC gc;
int x, y;
char *string;
int length;

XDrawString16(display, d, gc, x, y, string, length)
Display *display;
Drawable d;
GC gc;
int x, y;
XChar2b *string;
int length;



注意, 两个函数中的 string 指针指向的并不是编码序列, 而是字模在字库中的索引序列。一般将这个序列使用的编码转化成为字符集(CharSet)编码。 对于 ISO8859-1 等8位编码的西方语言来说, 编码和字符集编码是一样的。 所以可以直接用 XDrawString 显示显示这些编码的字符串。 但对于多字节编码的复杂语言来说就不一样了。 以 GB2312 为例, GB2312 编码(encoding) 的范围为单字节 0x00~0x80, 双字节 0xA1A1~0xFEFE, 而 GB2312 字库的字符集仅覆盖 GB2312 编码的双字节部分, 其编码是将 GB2312 双字节编码中每个字节的第8位置0得来的, 例如, GB2312 编码为 0xA1A1 的字符的字模(glyph)信息在 GB2312 字库中的索引是 0x2121, 而不是 0xA1A1。

早期的 XFree86 系统并不支持 ttf 字体, 对于 XFree86 系统中常用的 BDF/PCF 点阵字体而言, 每种字体使用的字符集编码是固定的, 如中文字库有 GB2312.1980-0, GBK-0, BIG5-0 等字符集, 日文有 JISX0201。1976-0, JISX0208。1983-0 等字符集。 后来人们在 XFree86系统中引入了对 ttf 等矢量化字体的支持, 目前用于 XFree86 4.x 的 ttf 字库模块有 freetype 和 xtt。 由于绝大多数 ttf 字体都直接使用 UCS2-BE 作为字符集编码(字库索引)。 为了和现有 XFree86 系统兼容, 这两个模块都具备编码转换功能, 即可以将传统的字符集编码转换为 UCS2-BE 编码, 从而可以将 ttf 字库虚拟成传统的字库来使用。 为了便于字库管理, XFree86 引入了 XLFD (X Logical Font Discription) 机制来描述字体。 关于 XLFD 的详悉介绍请参阅 XFree86 的相关文档。

为了增强 X 窗口系统对国际化的支持, 在 X11R5 版本中引进了一系列新的字符串绘制函数和字体集的概念。 简单来说, 字体集就是将几种不同字符集的字库组合成一个覆盖多个字符集的新字体。 例如将一个 ISO8859-1 的英文字库和一个 GB2312.1980-0 的中文字库组合成一个字体集, 就可以覆盖 GB2312 编码的所有字符了。

支持国际化的字符串绘制函数主要分为两类 XmbDrawString 和 XwcDrawString。 它们的定义为:
void XmbDrawString(display, d, font_set, gc, x, y, string,
num_bytes)
Display *display;
Drawable d;
XFontSet font_set;
GC gc;
int x, y;
char *string;
int num_bytes;

void XwcDrawString(display, d, font_set, gc, x, y, string,
num_wchars)
Display *display;
Drawable d;
XFontSet font_set;
GC gc;
int x, y;
wchar_t *string;
int num_wchars;




前一类用于绘制多字节编码的字符串, 后一类用于绘制宽字节编码的字符串。 使用这两个函数可以直接绘制当前系统语言编码的多字节编码字符串或者 wchar_t 类型的字符串。 但实际上这两个函数还是将多字节字符串或宽字节字符串拆分并转换成多段不同字符集的编码串, 然后调用 XDrawString 或者 XDrawString16 来显示。 所使用的字库就是字体集中对应的字库。

例如使用 XmbDrawString 绘制 ""ABC大家好"" 这个GB2312编码的多字节字符串, XmbDrawString 首先会把这个字符串拆分成两部分, 一部分是 ""ABC"" 对应 ISO8859-1 字符集, 将用字体集中的 ISO8859-1 英文字库来绘制; 另一部分是 ""大家好"" 对应 GB2312.1980-1 字符集, 将用字体集中的中文字库来绘制。 然后会将这两个字符串分别转换为对应字符集的编码, 转换后的结果为 ""ABC"" (未变) 和 0x73 0x34 0x52 0x3C 0x43 0x3A (即 ""大家好"" 的 GB2312.1980-0 字符集编码, 也就是字库索引)。 然后 XmbDrawString 就会分别调用 XDrawString 和 XDrawString16 来显示这两个字符串。

1.2.2 进程间通讯
X 窗口系统的进程间通讯主要涉及到不同进程间的文字拷贝和粘贴, 不同进程间的数据通讯以及程序的数据输入等方面。 由于 X 窗口系统为 Client/Server 结构的分布式网络窗口系统, 为了在保证进程间通讯的网络透明性和可恢复性的同时, 提供国际化支持, 在 X 窗口系统内部引入了复合文本编码 (Compound Text)。 这个编码其实是以 ISO-2022(一种早期的多语言编码框架标准) 为基础制定的。 它并不是一种真正意义上的编码, 而仅是一个能将多种不同编码的字符流组合成一个字符流的框架协议。 进程间在使用 Compound Text 通讯的时候可以将各种不同编码的字符流汇集在一起然后再传输, 到达目的进程后再按照一定规则解码成多个字符流进行处理。 简单来说, Compound Text 就是利用不同的状态字符串来标志各个编码字符串的起止位置, 利用一种统一的编码规则对字符串进行编码, 以免引起歧义。

Compound Text 标准中为 ISO8859, EUC-CN, EUC-JP 等许多常用编码规定了标准的编码规则和状态字符串, 另外还定义了几种扩展规则, 用于将更新的编码溶入 Compound Text 标准。 Compound Text 标准过于复杂, 而且所能包含的编码数量也很有限。 随着 ISO10646/Unicode 标准的不断发展和完善, X 系统已经开始逐步用 UTF-8 编码替换 Compound Text。 为了保持与旧系统的兼容性, 在 XFree86 系统中 UTF-8 编码的字符串可以用一种特殊的编码规则编码成 Compound Text。

1.2.3 编码转换
前面已经提到了很多种文字编码和标准, 简单总结如下:

多字节/宽字节编码 (Multibytes/Wide Char): 通俗的说, 多字节编码就是外码, 一般为可变长编码, 主要用于信息存储和交换; 宽字节编码就是内码, 为定长码, 通常一个字符对应4个字节, 主要用于信息处理。 在 POSIX 标准中定义多字节编码的数据类型为 char, 宽字节编码的数据类型为 wchar_t。 在目前 linux 常用的 glibc 2.2.x 版本中, 使用 UCS-4 作为宽字节编码, 并遵循 Unicode 标准 (最新的 glibc 2.2.4 已经完全支持 Unicode 3.1 标准)。 常见的多字节编码有 UTF-8, ISO8859 系列, GB2312, GBK, EUC-JP 等。
字符集 (charset): 即一系列字汇(glyphs)的有序组合。 字符集一般使用固定长度的编码。 常见的字符集有 Unicode (它即定义了编码的标准, 又定义了字符集的标准), ISO8859-1, GB2312.1980-0, GBK-0 等。
复合文本编码 (Compound Text): 是一种可以将多个不同编码的字符流汇集在一起的编码标准, 常用于 X 应用程序之间的进程通讯和文字输入。
既然有这么多不同的编码和标准, 那么 X 系统就必须能够正确的处理并转换它们。 在 X 的底层采用了一种模块化的机制来处理所有编码的转换与翻译工作。 这些转换模块被直接编译到了 X11 Library 中 (具体源代码请参阅 XFree86 源码中的 xc/lib/X11/lc* 等文件)。

首先, 在 X11 Library (以下简称 libX11) 中为它所支持的编码标准定义了标准名称 (见 XlcPublic。h 文件):
#define XlcNMultiByte ""multiByte"" /* 多字节编码 */
#define XlcNWideChar ""wideChar"" /* 宽字节编码 */
#define XlcNCompoundText ""compoundText"" /* 复合文本编码 */
#define XlcNString ""string"" /* 传统字符串编码, 即 ASCII 编码 */
#define XlcNUtf8String ""utf8String"" /* UTF-8 编码 */
#define XlcNCharSet ""charSet"" /* 字符集编码 */
#define XlcNCTCharSet ""CTcharSet""
#define XlcNFontCharSet ""FontCharSet"" /* 字库使用的字符集编码 */
#define XlcNChar ""char"" /* 单个字符 */
#define XlcNUcsChar ""UCSchar"" /* UCS-4 编码的字符 */




其中 XlcNCharSet 和 XlcNFontCharSet 在绝大多数情况下是一样的, 目前仅在处理 Unicode 的时候有区别。 在 UTF-8 Locale 下, XlcNCharSet 使用的是 UTF-8 编码, 而 XlcNFontCharSet 使用的是 UCS-2 Big Endian 编码。

在程序初始化的时候, libX11 将调用 _XlcInitLoader() 函数来初始化所有的编码转换模块, 程序段如下:
void
_XlcInitLoader()
{
#ifdef USE_GENERIC_LOADER
_XlcAddLoader(_XlcGenericLoader, XlcHead);
#endif

#ifdef USE_DEFAULT_LOADER
_XlcAddLoader(_XlcDefaultLoader, XlcHead);
#endif

#ifdef USE_UTF8_LOADER
_XlcAddLoader(_XlcUtf8Loader, XlcHead);
#endif

#ifdef USE_EUC_LOADER
_XlcAddLoader(_XlcEucLoader, XlcHead);
#endif

#ifdef USE_SJIS_LOADER
_XlcAddLoader(_XlcSjisLoader, XlcHead);
#endif

#ifdef USE_JIS_LOADER
_XlcAddLoader(_XlcJisLoader, XlcHead);
#endif

#ifdef USE_DYNAMIC_LOADER
_XlcAddLoader(_XlcDynamicLoader, XlcHead);
#endif
}




在以上所有转换模块中, Generic 转换模块 (_XlcGenericLoader) 是最重要的一个模块。 它使用 XLC_LOCALE 描述文件所定义的标准转换方法完成 XlcNWideChar, XlcNCharSet, XlcNCompoundText 等编码之间的相互转换。 Utf8 转换模块 (_XlcUtf8Loader) 则专门用于 UTF-8 Locale 下的各种编码转换。 Dynamic 模块用于动态调入独立的编码转换模块。 但在目前的 XFree86 4.x (X11R6.5) 中, 这个模块是无效的。

下面就以 Utf8 模块 (lcUTF8.c) 为例, 来介绍一下编码转换模块的构造:

虽然常用的编码标准仅有 XlcNCharSet, XlcNMultiByte, XlcNWideChar 等几种, 但经过排列组合, 需要处理的编码转换还是非常多的。 下面仅以 XlcNCharSet 到 XlcNMultiByte 转换为例来说明问题。

首先, 在 lcUTF8.c 中定义了一个负责将 XlcNCharSet 编码的字符串转换为 XlcNMultiByte 的字符串的函数:

(注意, 在 UTF-8 Locale 中, MultiByte 编码即为 UTF-8, 所以 XlcNMultiByte 和 XlcNUtf8String 其实是一样的)


/* from XlcNCharSet to XlcNUtf8String */

static int
cstoutf8(
XlcConv conv,
XPointer *from,
int *from_left,
XPointer *to,
int *to_left,
XPointer *args,
int num_args)
{
....... (略)
}




关于数据结构 XlcConv 的定义请参见 XlcPublic。h 文件。然后定义了一个 XlcConvMethodsRec 对象和一个初始化函数:
static XlcConvMethodsRec methods_cstoutf8 = {
close_converter,
cstoutf8,
NULL
};

static XlcConv
open_cstoutf8(
XLCd from_lcd,
const char *from_type,
XLCd to_lcd,
const char *to_type)
{
lazy_init_all_charsets();
return create_conv(from_lcd, &methods_cstoutf8);
}




最后还要在初始化函数 _XlcAddUtf8Converters() 和 _XlcUtf8Loader() 中分别加上:
_XlcSetConverter(lcd, XlcNCharSet, lcd, XlcNUtf8String, open_cstoutf8);

和:

_XlcSetConverter(lcd, XlcNCharSet, lcd, XlcNMultiByte, open_cstoutf8);




至此, UTF-8 Locale 的 XlcNCharSet 到 XlcNMultiByte 和 XlcNUtf8String 的转换函数就完成了。 当 X 应用软件在 UTF-8 Locale 下需要进行 CharSet 到 MultiByte 或 Utf8String 转换的时候, libX11 会自动调用 函数 cstoutf8 进行转换。

大家读过 lcUTF8.c 后也许会发现里面并没有定义 Compound Text 和其他编码之间的转换方法。 其实在 lcCT.c 中已经定义了 Compound Text 和各种标准 CharSet 以及已在 XLC_LOCALE 中定义的非标准 CharSet 之间的转换函数, libX11 会利用这些函数完成 MultiByte, Utf8String 等编码和 Compound Text 之间的间接转换。

由于 libX11 中所有的编码转换都倚赖于这些编码转换模块, 所以要想让 libX11 支持一种新的编码(即一种新的 Locale), 就必需在 libX11 中加入相应的编码转换模块。 在 X 窗口系统中实现对 GB18030 的支持的难点就在于此了。

未完待续)

作者简介
苏哲,Turbo Linux公司软件工程师。您可以通过电子邮件 [email protected] 和他联系。