ccl 是 emacs 中用来转换文件编码的语言。emacs 中内嵌了 ccl 的解释器。 ccl 的代码实际上是一个整数数组,大概类似这样:

;;	|----------------- integer (28-bit) ------------------|
;;	|------------ 20-bit ------------|- 3-bit --|- 5-bit -|
;;	|------------- DATA -------------|-- REG ---|-- OP ---|
;; If REG2 is specified, embed a code in the following format.
;;	|------- 17-bit ------|- 3-bit --|- 3-bit --|- 5-bit -|
;;	|-------- DATA -------|-- REG2 --|-- REG ---|-- OP ---|

直接写 ccl 代码应该是不可能的事情,emacs 提供了更高级的 ccl 语言,并把它编译成 ccl 解释器能使用的代码。程序的语法应该是比较简单的,但是相关的文档很少,函数 define-ccl-program 中给出了 ccl 的 BNF 语法描述,但是对一些函数功能和概念的说明还是太少。

http://www.xemacs.org/Documentation/21.5/html/lispref_64.html 中给出了一个相对比较全面的说明,而且还给出一个例子,但是,emacs 22 上使用的ccl 应该有进行一定的扩展,比如 read-multibyte-character、 write-multibyte-character、lookup-integer 和 translate-character 在这里没有给出,但是这是很有用的命令。我在大概(实在是有的看不懂:))看了几遍 utf-8, utf-16, code-pages 的源码后,结合一些例子对这几个函数有了一定的了解。

首先,一个 ccl 程序是用 define-ccl-program 来定义的。它的语法形式是:

	(BUFFER_MAGNIFICATION
	 CCL_MAIN_CODE
	 [ CCL_EOF_CODE ])

其中 BUFFER_MAGNIFICATION 是输出缓冲和输入缓冲大小的比值。如果设置为 0 就不能用 read 和 write 命令了。

测试一个 ccl 程序可以用 ccl-execute-on-string 函数。比如,写一个最简单的 ccl 程序:

(define-ccl-program ccl-ywb-test
  '(2
    ((write #x41))))
(ccl-execute-on-string ccl-ywb-test
                          (make-vector 9 nil)
                          "")

可以看到这个程序确实产生了一个输出 "A"(即 0x41 对应的 ASCII 字符)。

ccl 的运算符、read、write 和循环控制命令在 define-ccl-program 文档和前面给出的文档中都有很多说明,也很容易明白。只有 read-multibyte-character、 write-multibyte-character、lookup-integer 和 translate-character 这四个函数我觉得是比较难懂的。

下面我用一些例子说明。第一个是解码 unicode 的一个例子。"中"字的 unicode 是 0x4e2d,用这个程序可以把这个 unicode 转换成 emacs 的字符:

(define-ccl-program ccl-ywb-test
  `(2
    ((r0 = #x4e2d)
     (lookup-integer utf-subst-table-for-decode r0 r1)
     (write-multibyte-character r0 r1))))
(ccl-execute-on-string ccl-ywb-test
                          (make-vector 9 nil)
                          "")

它是怎样得到这个汉字的呢?首先看看文档中给出的说明:

 (lookup-integer SYMBOL REG(integer))
         ;; SYMBOL refers to a table defined by
         ;; `define-translation-hash-table'.

utf-subst-table-for-decode 是一个 symbol,它有 translation-hash-table 这个属性:

(get 'utf-subst-table-for-decode 'translation-hash-table)

如果用 #x4e2d 来获取散列表中的值会得到什么呢:

(char-to-string
 (gethash #x4e2d (get 'utf-subst-table-for-decode 'translation-hash-table)))

是不是得到一个 "中" 字。所以这个 translation-hash-table 提供了一个从 unicode 到 emacs 内部字符转换的表格。

那 lookup-integer 具体有怎样的作用呢?我们先看看 write-multibyte-character 的文档:

	;; Write a multibyte representation of a character whose
	;; charset ID is REG_0 and code point is REG_1.  If the
	;; dimension of the charset is two, REG_1 should be ((CODE0 <<
	;; 7) | CODE1), where CODE0 is the first code point and CODE1
	;; is the second code point of the character.
	| (write-multibyte-character REG_0 REG_1)

所以用了 lookup-integer 后,r0 中是 charset id,r1 中是 code point 合成的一个数。关于 charset id 和 code point,我做一个简单的说明吧。首先 emacs 把字符分成多个 charset,每个 charset 分配了一个 charset id。一个 charset 中最多只能有 9025(即 95*95) 个字符。一个 charset 如果维数 (charset-dimension)为 2,就能容纳 9025 个字符,每个字符对应一个 code point,这个 code point 由两个字节构成,每个字节的范围都从 0x21-0x7f(刚好 95 个)。可以用 list-charset-chars 命令来查看各个 charset 中的字符。由于字符范围是在 0x7f 以下,所以用对一个 code point 的两个字符进行

((code0 << 7) | code)
这样的移位是可以用 (code >> 7) 和 (code & 255) 进行还原的。之所以要进行这样的移位操作,我估计在 emacs 内部可以使用这样得到的数作下标来在 char-table 中查找字符。

总的一句话,lookup-integer 的作用是从 translation-hash-table 中查找出一个整数对应的字符。

如果明白了 lookup-integer,其它函数也就很容易明白了。

(define-ccl-program ccl-ywb-test
  `(2
    (
     (read-multibyte-character r0 r1)
     (lookup-character utf-subst-table-for-encode r0 r1)
     (write (r0 >> 8))
     (write (r0 & 255))
     )))
(ccl-execute-on-string ccl-ywb-test
                          (make-vector 9 nil)
                          "中")

是不是得到 "N-"(即 #x4e #x2d 两个字节)这个字符串?所以 read-multibyte-character 和 write-multibyte-character 刚好对应,它是从 emacs 的输入中得到一个字符的 charset id 和 code point,而 lookup-character 与 lookup-character 作用刚好相反,它是把 emacs 内部的一个字符根据 translation-hash-table 转换成一个整数。

translate-character 的功能是用 translation-table 把 emacs 内部表示的一个字符转换成另一个字符。比如,cp437(在 mule/code-pages.el 中定义)使用了一个转换表 decode-cp437。在这个表中可以把 0-255 之间的字符转换成 unicode 字符。我们用这个做一个测试。首先在 *scratch* 中用 C-x RET f cp437 确保导入这个编码系统,在 *scratch* 用这个可以显示表中的内容:

(let ((table (get 'decode-cp437 'translation-table)))
  (dotimes (i 255)
    (insert (format "%d => %c\n" i (aref table i)))))

比如表中有一个从字符 220 转换成 unicode #x2584 的字符。

(define-ccl-program ccl-ywb-test
  `(4
    ((r0 = 128)
     (r1 = 220)
     (translate-character decode-cp437 r0 r1)
     (write-multibyte-character r0 r1))))
(ccl-execute-on-string ccl-ywb-test
                          (make-vector 9 nil)
                          "")

是不是正确转换了?所以 translate-character 是输入的是一个字符的 charset id 和 code point,然后这两个寄存器会转换成另一个字符的 charset id 和 code point。