作者:王守華 本文首先介紹了國家最新頒布的GB 18030-2000標准的結構和編碼規則,對Linux的國際化和本地化機制進行了簡要的概述,然後從Glibc、 Locale兩個方面具體討論了如何在Linux上實現對GB 18030-2000標准的支持,並且對下一步需要進行的工作進行了展望。 一、GB18030-2000編碼標准簡介 GB18030-2000編碼標准是由信息產業部和國家質量技術監督局在2000年 3月17日聯合發布的,並且將作為一項國家標准在明年的1月正式強制執行。 1.概述 GB18030-2000編碼標准是在原來的 GB2312-1980 編碼標准和 GBK 編碼標准的基礎上進行擴充,增加了四字節部分的編碼。它可以完全映射ISO10646的基本平面和所有輔助平面,共有150多萬個碼位。在ISO10646的基本平面內,它在原來的2萬多漢字的基礎上增加了7000 多個漢字的碼位和字型,從而使基本平面的漢字到達 27000多個。它的主要目的是為了解決一些生、偏、難字的問題,以及適應出版、郵政、戶政、金融、地理信息系統等迫切需要的人名、地名用字問題。 GB18030-2000作為GB2311體系的編碼字符標准,規定了信息交換用的圖形字符及其二進制編碼的十六進制表示。它支持 GB 13000.1-1993的全部中日韓(CJK)統一漢字字符和全部中日韓統一漢字Extension A和Extension B的字符。 2.字匯 GB18030-2000編碼標准收錄的字符分別以單字節、雙字節和四字節編碼。下面簡要列舉一下它們各自包括的內容: 1) 單字節部分 單字節部分收錄了GB11383的0x00到0x7E全部 128個字符及單字節編碼的歐元符號。 2) 雙字節部分 雙字節部分收錄的內容如下: GB13000.1的全部CJK統一漢字字符; GB13000.1的CJK兼容區挑選出來的21個漢字; GB13000.1中收錄而GB2312未收錄的我國台灣地區使用的圖形字符139個; GB13000.1收錄的其它字符31個; GB2312中的非漢字符號; GB12345的豎排標點符號19個; Gb2312未收錄的10個小寫羅馬數字; Gb2312未收錄的帶音調的漢語拼音字母5個以及 a和g; 漢字數字“O”; 表意文字描述符13個; 增補漢字和部首/構件80個; 雙字節編碼的歐元符號。 3) 四字節部分 四字節部分收錄了上述雙字節字符之外的,包括 CJK 統一漢字擴充 A 在內的 GB13000.1 中的全部字符。 3.編碼規則 GB18030-2000標准采用單字節、雙字節和四字節三種方式對字符編碼。單字節部分采用GB 11383的編碼結構與規則,使用 0x00至0x80碼位。雙字節部分采用兩個字節表示一個字符,其首字節碼位從 0x81至0xFE,尾字節碼位分別是0x40至0x7E和0x80至0xFE。四字節部分第一、三字節仍為0x81~0xFE,第二、四字節采用GB 11383未使用的0x30到0x39作為對雙字節編碼擴充的後綴,這樣擴充的四字節編碼,其范圍為0x81308130 到 0xFE39FE39。碼位范圍分配如下表所示: 表1 碼位范圍分配 字節數 碼位空間 碼位數 單字節 0x00~0x80 129 個碼位 雙字節 第一字節 第二字節 23940 個碼位 0x81~0xfe 0x40~0x7e, 0x80~0xfe 四字節 第一字節 第二字節 第三字節 第四字節 1587600 個碼位 0x81~0xfe 0x30~0x39 0x81~0xfe 0x30~0x39 四字節字符的編碼自第四個字節開始,編碼碼位為 0x30至0x39;其次是第三個字節,編碼碼位為0x81至0xFE;再次是第二個字節,編碼碼位為0x30至0x39;最後是第一個字節,編碼碼位為0x81至0xFE。即: 0x81308130至0x81308139; 0x81308230至0x81308239; ...... 0x8130FE30至0x8130FE39; 0x81318130至0x81318139; ...... 0x8131FE30至0x8131FE39; ...... 0x82308130至0x82308139; ...... 0xFE308130至0xFE308139; ...... 0xFE39FE30至0xFE39FE39。 二、Linux的國際化和本地化機制 Linux的國際化和本地化機制非常靈活方便,這使得我們在Linux增加一種新的本地化支持非常容易,甚至基本上不需要去編譯應用程序包,而只需要在系統核心的某些地方進行擴充和修改就可以了。 1.NLS簡介 為了更好地對國際化和本地化進行支持,Linux系統 提供了符合Posix標准的 NLS(National Language Support)子系統。該子系統建築在基於ASCII碼的Linux核心上,為世界上不同地域、不同語言環境的應用提供國際化本地化支持。系統中所有支持多國語言的實用程序,包括X Window都是建立在這個基礎上。在此基礎上,可以建立支持各種不同的語言文化的民族特征數據庫(LOCALE)、輸入方法(IM)、字體(FONT)和消息機制(MESSAGE)等。在NLS子系統中,Glibc函數庫中提供的與代碼集相關的多字節字符與寬字符處理函數是Linux實用程序支持國際化本地化的核心,通過這些函數,實用程序把英文與各種本地文字同樣處理;而 Locale 則是本地化工作的一個基石,因為不管是Glibc,還是系統的其它部分,都是通過讀取系統當前的 Locale 設定來識別 當前的本地化區域,從而使用正確的字符映射表和消息函數。 2.Glibc的轉換模式概念 libc對多字節編碼的支持非常靈活和自由,它通過一種"轉換模式"來實現。目前的Glibc采用UTF-8作為處理碼。當接收到外部輸入的多字節字符時,系統首先根據當前的 Locale 確定所使用的字符集,然後查找從當前字符集到UTF-8之間的轉換模式,根據這種轉換模式把輸入的多字節字符轉換為相應的UTF-8字符。相反,當需要輸出字符時,系統再查找從UTF-8到當前字符集之間的轉換模式,按照這種模式把要輸出的UTF-8字符轉換為當前字符集中的多字節字符。在Glibc內部,有一個gconv_modules的轉換模式列表。這個列表作為一個文件的形式存在於/usr/lib/gconv 目錄下。對於每種具體的轉換模式,Glibc把它們的轉換表和轉換函數編譯為一個獨立的共享文件(.so),比如支持GB的轉換模式的共享文件名為libGB.so,支持Big5的共享文件名為 libBIG5.so。當應用程序調用到Glibc中有關編碼轉換部分的函數時,Glibc首先從gconv_modules 這個轉換模式列表中查找所應該采用的轉換模式,然後把相應轉換模式的共享文件調入內存。這種動態裝載的方式,使得添加或者刪除轉換模式非常自由和方便,而且當Glibc 支持越來越多的編碼時,libc.so這個文件本身並不會變得異常龐大。 3.Glibc對多字節字符和寬字符的轉換處理 Glibc中提供了一種新的字符類型——寬字符。寬字符的好處就是它可以把這個字符作為一個單字來處理,而不會導致多字節字符在處理時遇到的刪除半個字符的現象。為了更好地使用寬字節字符, Glibc提供了多字節字符和寬字符之間的轉換函數以及其它的對寬字符處理的函數。前面介紹的當系統要將本地化編碼集中的多字節字符轉化為系統的處理內碼UTF-8 時,使用的就是這類處理函數中的mBTowc()函數。下面就以這個函數的工作流程為例進行詳細介紹:支持國際化本地化的應用程序在需要將一個多字節字符轉換UTF-8處理碼時,調用 mbtowc()函數進行處理。但是,mbtowc()函數只是一個外殼,它主要是調用 mbrtowc()函數。而在mbrtowc函數中,首先調用 update_conversion_ptrs() 函數來更改當前的轉換函數,然後調用 _wcsmbs_gconv_fcts 結構的 towc 成員 的 fct 成員函數來完成實際的轉換。而在 update_conversion_ptrs() 函數中, 主要是通過調用 __wcsmbs_load_conv() 函數來完成的。__wcsmbs_load_conv() 函數函數又調用了getfct() 函數來取得當前轉換模式的結構信息,同時根據轉換模式的結構信息調用相應的共享文件。getfct() 函數調用 gconv_find_transform()函數來查找當前應該采用的轉換函數,並且把轉換函數的名字放在轉換模式的結構中一起返回。 mbtowc()函數實現的具體步驟如圖1所示: 從上面的轉換過程可以看出,要想在Linux上增加對GB18030-2000編碼的支持,需要對Glibc進行一定的擴充。這種擴充重要就是增加新的支持GB18030-2000編碼標准的轉換模式。由於目前Linux源碼的開放性,使得我們無論是了解程序的工作流程,還是進行具體的修改都十分方便。至於對Glibc所做的擴充,在第三部分將會詳細介紹。 4.Locale簡介 Locale是Linux本地化工作的基礎,針對不同的區域, 它在 /usr/share/locales 目錄下面有不同的Locale。 一般來說,Locale是由三個文件創建得到的,它們分別 是Charmap Source 文件、Locale Source文件和Method文件。其中Charmap Source 文件主要用來定義該Locale所支持的字符集中的每個字符; Locale Source文件的主要作用是定義Locale的六個域,它們分別是LC_CTYPE、LC_COLLATE、LC_TIME、 LC_NUMERIC、LC_MONETARY、LC_MESSAGES,在新的Glibc2.2版本中將會支持更多的 Locale域;而所謂Method文件,其實就是針對多字節處理所提供的一些函數。 Glibc 提供了 localedef 函數來生成具體的 Locale, localedef 的用法如下所示: localedef -f charmapsourcefile -i localesourcefile -m methodfile localename 其中-f, -i 和 -m 參數分別表示指定 charmap source 文件, locale source 文件和 method 文件,而最後的一個參數表示所要生成的 locale 的名字。 三、為支持GB18030-2000所做的工作 GB18030-2000標准在Linux上面的實現可以分為三個步驟:內核級支持、基本系統和完全支持。其中內核級支持就是指系統能夠正確識別 GB18030-2000編碼,並且有關多字節字符和寬字節字符處理的函數能夠正常工作;而基本系統就是指在這之上字符界面和X Window平台下能夠正確顯示和輸入 GB18030-2000編碼,同時有一些常用應用程序能夠正確處理GB18030-2000編碼;完全實現就是指除了上述支持之外,其它的所有應用程序、數據庫和網絡都能夠正確支持 GB18030-2000 編碼,另外,所有支持國際化的應用程序都應該能夠不做修改地在該系統上面運行。 我們完成的工作已經達到了內核級的支持,也就是從 Glibc 和Locale兩個對系統進行擴充,下一步的工作就是實現一個基本系統。 1.Glibc方面 在Glibc中,定義了許多編碼和內碼之間的轉換模式,這種 模式的定義在 iconvdata/gcong-modules文件中。如果要增加對 GB18030-2000 編碼標准的支持,首先就需要在這個文件中增加兩個新的轉換模式:從 GB18030-2000 編碼到內碼和從內碼到GB18030-2000編碼。增加的幾行代碼如下所示: # from to module cost alias GB13000// GB18030// module GB18030// INTERNAL GB18030 2 module INTERNAL GB18030// GB18030 2 為了完成具體的轉換,僅僅定義轉換模式還是遠遠不夠的,接下來要做的工作就是實現具體的轉換函數和轉換編碼表。實現GB18030-2000編碼遇到的一個主要困難就在這裡,由於 GB18030-2000 編碼是單字節、雙字節和四字節混合的,而以前的GB2312-1980和GBK編碼都只有單字節和雙字節兩種編碼方式,這樣就給系統的處理帶來了一定的麻煩。唯一值得慶幸的是具體的轉換函數可以自己實現,只需要按照一定的格式就可以了。 Glibc的這套固定格式其實就是一系列宏的定義,只要安裝這種規則來正確定義這些宏,Glibc就能夠正確地進行編碼轉換處理,比如下面是一套從 GB18030-2000 到系統內碼的轉換函數實現輪廓: #define CHARSET_NAME "GB18030//" #define FROM_LOOP from_gb18030 #define TO_LOOP to_gb18030 #define DEFINE_INIT 1 #define DEFINE_FINI 1 #define MIN_NEEDED_FROM 1 #define MAX_NEEDED_FROM 4 #define MIN_NEEDED_TO 4 #define MIN_NEEDED_INPUT MIN_NEEDED_FROM #define MAX_NEEDED_INPUT MAX_NEEDED_FROM #define MIN_NEEDED_OUTPUT MIN_NEEDED_TO #define LOOPFCT FROM_LOOP #define BODY { ...... } 從上面可以看出,這段代碼主要是定義字符集的名字、轉換前後的字符集以及其它的一些轉換信息。其中最主要的工作就是BODY內的轉換函數,它用來定義如何把一個 GB18030 的編碼轉換為系統內碼。下面是完整的代碼段: uint32_t ch = *inptr; if (ch <= 0x7f) ++inptr; else if (ch <= 0x80 ch > 0xfe) { /* This is illegal. */ if (! ignore_errors_p ()) { result = __GCONV_ILLEGAL_INPUT; break; } ++inptr; ++*irreversible; continue; } else { /* Two or more byte character. First test whether the next character is also available. */ uint32_t ch2; unsigned long int idx; if (inptr + 1 >= inend) { /* The second character is not available. Store the intermediate result. */ result = __GCONV_INCOMPLETE_INPUT; break; } ch2 = inptr[1]; /* All second bytes of a multibyte character must be >= 0x30. */ if (ch2 < 0x30) { if (! ignore_errors_p ()) { /* This is an illegal character. */ result = __GCONV_ILLEGAL_INPUT; break; } ++inptr; ++*irreversible; continue; } if (ch2 >= 0x30 && ch2 <= 0x39) { uint32_t ch3; uint32_t ch4; if (inptr + 3 >= inend) { /* Not all characters are available. Store the intermediate result. */ result = __GCONV_INCOMPLETE_INPUT; break; } ch3 = inptr[2]; if (ch3 < 0x81 ch3 > 0xfe) { /* This is an illegal character. */ if (! ignore_errors_p ()) { /* This is an illegal character. */ result = __GCONV_ILLEGAL_INPUT; break; } inptr += 3; ++*irreversible; continue; } ch4 = inptr[3]; if (ch4 < 0x30 ch4 > 0x39) { /* This is an illegal character. */ if (! ignore_errors_p ()) { /* This is an illegal character. */ result = __GCONV_ILLEGAL_INPUT; break; } inptr += 4; ++*irreversible; continue; } idx = ((((ch - 0x81) * 10 + (ch2 - 0x30)) * 126 + (ch3 - 0x81)) * 10 + (ch4 - 0x30)); if (idx >= (sizeof (__gb18030_to_UCs) / sizeof (__gb18030_to_ucs[0])) (ch = __gb18030_to_ucs[idx], ch == 0 && *inptr != '\0')) { /* This is an illegal character. */ if (! ignore_errors_p ()) { /* This is an illegal character. */ result = __GCONV_ILLEGAL_INPUT; break; } inptr += 4; ++*irreversible; continue; } inptr += 4; } else if (__builtin_eXPect (ch2, 0x40) >= 0x40) { idx = (ch - 0x81) * 192 + (ch2 - 0x40); if (idx >= sizeof (__gbk_to_ucs) / sizeof (__gbk_to_ucs[0]) (ch = __gbk_to_ucs[idx], ch == 0 && *inptr != '\0')) { /* This is an illegal character. */ if (! ignore_errors_p ()) { /* This is an illegal character. */ result = __GCONV_ILLEGAL_INPUT; break; } inptr += 2; ++*irreversible; continue; } inptr += 2; } else { /* This is an illegal character. */ if (! ignore_errors_p ()) { /* This is an illegal character. */ result = __GCONV_ILLEGAL_INPUT; break; } inptr += 2; ++*irreversible; continue; } } *((uint32_t *) outptr)++ = ch; 需要注意的是上面的每行語句後面都有一個“\”符號,這個續行符是為了保證整個函數是一個完整的宏而添加的。為了實現這種轉換,還需要相應的編碼轉換表,而轉換函數的具體代碼就是從編碼轉換表中查找到當前GB18030編碼對應的系統內碼,然後返回這個系統內碼。編碼轉換表一般是從編碼的字符映射表生成的,在這個程序中,它應該作為一個或者多個數組的形式而存在。相應地,從系統內碼到GB18030編碼的轉換函數的實現也是類似的,但是有一點區別就是轉換後得到的GB18030編碼是不定長的,它有單字節、雙字節和四字節三種情況,從而會導致這部分的編碼轉換函數相對復雜得多。在編碼轉換表方面,需要按照返回的 GB18030 編碼長度,對編碼轉換表進行分段,然後再根據各個段的情況進行分別處理;在編碼轉換程序方面,需要在返回GB18030編碼的同時返回編碼的長度,根據編碼長度進行相應的處理。這部分的轉換函數代碼比較復雜,就不再附在本文中,如果想了解可以到http://sources.redhat.com/cgi-bin/cvsweb.cgi/libc/iconvdata/gb18030.c?rev= 1.3&content-type=text/x-cvsweb-markup&cvsroot=glibc頁面中得到想要的內容。定義了轉換模塊、具體的轉換函數以及它們使用的轉換表,系統就能夠正確識別相應字符集的編碼了,最後還需要在Makefile中添加GB18030這個編碼模塊,同時指明它有由哪幾個文件編譯得到的。對Glibc的這些修改工作完成之後,還需要重新編譯整個Glibc,就可以得到支持GB18030編碼標准的Glibc庫。 2.Locle方面 相對來說,在Locale方面所做的工作就相對簡單一些。我們只需要編寫一套新的關於GB18030-2000編碼標准的charmap Source文件、Locale Source文件 和 Method 文件就可以了。其中Charmap Source文件可以從有關GB18030-2000編碼標准的 一些文檔中得到;Locale Source文件可以按照原來的GB2312或者GBK的Locale Source文件編寫;Method文件也可以使用GB2312或者GBK所使用的Method文件。這三個文件完成之後,使用前面介紹過的localedef工具,就可以生成一個新的locale,我們不妨把它命名為zh_CN.GB18030。 Glibc所做的擴充結合新的zh_CN.GB18030 Locale,在Linux系統上就可以做到內碼級地支 持 GB18030-2000編碼標准了。現在可以試著把Locale設定為zh_CN.GB18030,然後在程序中調用mbtowc()、wctomb()、mblen()等函數,發現它們均可以正常工作,而且能夠識別諸如 0x81308130之類的四字節編碼。 3.進一步的工作 前面已經介紹過,GB18030-2000標准在Linux上的完全實現絕對不僅僅局限於內核級的實現,而應該包括基本系統的實現和應用程序的完全支持。在這方面還有許多工作要做,比如: * 字符界面下中文平台的顯示和輸入 * X Window下GB18030編碼文字的顯示和輸入 * 系統基本命令的支持 * 應用程序的支持 * 網絡支持 * 數據庫方面的支持 四、小結 Glibc和Locale的擴充綜合起來,就可以在Linux上面實現對 GB18030-2000編碼的內碼級支持,也就是說,Linux就可以正確識別和轉換 GB 18030-2000的編碼,在內核級別上支持GB18030-2000編碼標准。如果想要實現一個完全支持GB18030-2000編碼標准的Linux系統,還需要進一步的工作,本文也對它們進行了規劃和展望。 參考資料 參考文獻 關於本篇文章的參考資料,都是文獻書籍。並列出如下: [1] 《信息技術 信息交換用漢字編碼字符集基本集的擴充》,中國標准出版社,2000,北京 [2] Internationalization of AIX Software - A Programmer’s Guide, IBM 1992, USA [3] Yufang Sun, Jian Wu, Design and Implementation of CJK Character Set on UNIX system, 9th International Unicode Conference, San Jose, CA, USA, 1996.9 [4] 陳季雷、楊裕衡、林守铿,《洞悉UNIX——中文系統篇》,和碩科技文化有限公司,1994年7月,台北 [5] 吳健、孫玉芳、李國華、李祥凱,《“炎黃”中文平台的本地化處理》,已投《中文信息學報》