祝大家🐵🎉
这篇文章大概介绍了下 Unicode 带来的一些问题、相关的库和 Ruby 的一些实现,最后是我对这个问题的 2 个疑问。
以下内容有较多英文,力图表达最准确的意思。Unicode 相关的中文翻译有一些陌生和奇异。
Ruby 有完整的 Unicode 支持,但是几乎不提供与 Unicode 相关的功能。2.2 的标准库里才添加了 Normalize(把字符分解成 Unicode 的基础组成方式,有 4 种方法,最常见的是 nfc)相关的功能。作为 CJK 的主要用户之一,很多情况下,我们可能会使用中文作为用户名或者名字。这不可避免地会有排序(transliteration,音译
)和比较(confusables)的问题。
ActiveSupport::Inflector.transliterate
即是这个功能。不过中文进去,?
问号出来。с
等来替代英文字符 c
,所以之后浏览器会在某些情况下显示 xxx-xxx.com
这样的 Punycode。中文也有这样的 Unicode,如 ㍯ → 23点
、勇 → 勇
(Chrome 已经用 ICU 处理过这些字符了,所以你是可以搜索匹配到两个的)。同样的情况,在名字这些重要要件中,这也是应该避免的东西,比如 @
自动补全功能可能会让用户对错误的人执行无法预期的操作。(利用 unf 做的玩具库)以上问题都是 Unicode 的冰山一角,我们尽量做我们能做的。
其他 Gem 就几乎没有怎么更新了,比如:
这叫要说到一个处理 Unicode 的集大成者 ICU 了。ICU 是 IBM 专门拿来解决 Unicode 问题的库,C/C++ 和 Java 两种,及其成熟,16+ 年历史了。有挺多语言都有自制的包装库。除此之外,我还能想到 iOS 的 Foundation 里有很多字符相关的功能是用它支持的。
确实有很多软件依赖于它,甚至系统上也很容易找到库文件,OS X 上就有 /usr/lib/libicucore.A.dylib
。
但是只有 Perl 和 PHP 把 ICU 的支持放在标准库里。JRuby 很幸运地直接用 icu4r 就好了...Swift 倒是对 Unicode 有一个很不错的语言级别的实现。
因为是 ICU 的包装,所以这个包装会碰到很多关于 Ruby 底层实现的问题。Ruby 底层用 byte array (char *) 存储 String,字符串本身可以是任意字符(Unicode!)。但这个 byte array 是有不同编码的(UTF-8、UTF-16)。
在 Ruby 1.9 后对于 m17n 的办法是 CSI Model,所以 Ruby 的 String 对象有 encoding 的信息,可以任意转换 encoding,但是在各种情况下都可以作为 String 使用。这就意味着 String#[]
得到的结果一定是一个单一的字符,不论编码。
所以很明显地,这是和 UCS(Universal Coded Character Set,通用字符集)分道扬镳的一种方式。使用 UCS 模型的语言的底层编码类型应该是 UTF-8、UTF-16 和 UTF-32。而大部分语言的实现字符串的方式是 UCS 而不是 CSI。
只是 Ruby 内部实现默认使用了 UTF-8 作为编码方式。
MRI Ruby 没有用 ICU 的原因是要考虑多平台兼容性。
ICU 选定了 UChar(uint16_t) 作为它的内部字符串的实现,Unicode 叫这个叫Plane 平面。在这个级别上是没有大小端序的问题的。Unicode 字符是可以用 2^22 的点集表示的,所以一个 Unicode code point 是可以用 1、2 个(2 个的叫 surrogates,极少出现的情况)UChar 表示。UChar byte array 不一定是良好的 UTF-16 编码就是因为 surrogate 了。
少部分的功能是有直接的 UTF-8 和 UTF-32 支持的。ICU 也支持 UTF-8 字符串到 UChar byte array 的相互转换。
作为一个包装库,重要工作之一是把 Ruby String byte array 和 UChar byte array 对接起来。
String#unpack(U*)
转换到的是 Unicode Code Point,这可以转换到 UTF-32。那么就可以开始考虑如何实现这个包装库了。
目标:
那么不外乎选择:
FFI 包装了 libffi,虽然说 libffi 都有好几年没更新了,但是这是一个很简单的创建包装库的办法。