RapidJSON 代码剖析(三):Unicode 的编码与解码

根据 RFC-7159:
8.1 Character Encoding
JSON text SHALL be encoded in UTF-8, UTF-16, or UTF-32. The default encoding is UTF-8, and JSON texts that are encoded in UTF-8 are interoperable in the sense that they will be read successfully by the maximum number of implementations; there are many implementations that cannot successfully read texts in other encodings (such as UTF-16 and UTF-32).
翻译:JSON文本应该以UTF-8、UTF-16、UTF-32编码。缺省编码为UTF-8,而且有大量的实现能读取以UTF-8编码的JSON文本,说明UTF-8具互操作性;有许多实现不能读取其他编码(如 UTF-16及UTF-32)
RapidJSON 希望尽量支持各种常用 UTF 编码,用四百多行代码实现了 5 种 Unicode 编码器/解码器,另外加上 ASCII 编码。本文会简单介绍它的实现方式。
(配图为老彼得·布吕赫尔笔下的巴别塔)
回顾 Unicode、UTF 与 C++
Unicode 是一个标准,用于处理世界上大部分的文字。在 Unicode 出现之前,每种语言文字会使用不同的编码,例如英文主要用 ASCII、中文主要用 GB 2312 和大五码、日文主要用 JIS 等等。这样会造成很多不便,例如一个文本信息很难混合各种语言的文字。
Unicode 定义了统一字符集(Universal Coded Character Set, UCS),每个字符映射至一个整数码点(code point),码点的范围是 0 至 0x10FFFF。储存这些码点有不同方式,这些方式称为 Unicode 转换格式(Uniform Transformation Format, UTF)。现时流行的 UTF 为 UTF-8、UTF-16 和 UTF-32。每种 UTF 会把一个码点储存为一至多个编码单元(code unit)。例如 UTF-8 的编码单元是 8 位的字节、UTF-16 为 16 位、UTF-32 为 32 位。除 UTF-32 外,UTF-8 和 UTF-16 都是可变长度编码。
UTF-8 成为现时互联网上最流行的格式,有几个原因:
- 它采用字节为编码单元,不会有字节序(endianness)的问题。
- 每个 ASCII 字符只需一个字节去储存。
- 如果程序原来是以字节方式储存字符,理论上不需要特别改动就能处理 UTF-8 的数据。
那么,在处理 JSON 时,若使用 UTF-8,我们为何还需要特别处理?这是因为 JSON 的字符串可以包含 \uXXXX 这种转义字符串。例如["\u20AC"]这个JSON是一个数组,里面有一个字符串,转义之后是欧元符号"€"。在 JSON 中,这个转义符使用 UTF-16 编码。JSON 也支持 UTF-16 代理对(surrogate pair),例如高音谱号(U+1D11E)可写成"\uD834\uDD1E"。所以,即使是 UTF-8 的 JSON,我们都需要在解析JSON字符串时做解码/编码工作。
虽然 Unicode 始于上世纪90年代,C++11 才加入较好的支持。RapidJSON 为了支持 C++ 03,需要自行实现一组编码/解码器。
Encoding
RapidJSON 的编码(encoding)的概念是这样的(非C++代码):
concept Encoding {
typename Ch; //! Type of character. A "character" is actually a code unit in unicode's definition.
enum { supportUnicode = 1 }; // or 0 if not supporting unicode
//! \brief Encode a Unicode codepoint to an output stream.
//! \param os Output stream.
//! \param codepoint An unicode codepoint, ranging from 0x0 to 0x10FFFF inclusively.
template<typename OutputStream>
static void Encode(OutputStream& os, unsigned codepoint);
//! \brief Decode a Unicode codepoint from an input stream.
//! \param is Input stream.
//! \param codepoint Output of the unicode codepoint.
//! \return true if a valid codepoint can be decoded from the stream.
template <typename InputStream>
static bool Decode(InputStream& is, unsigned* codepoint);
//! \brief Validate one Unicode codepoint from an encoded stream.
//! \param is Input stream to obtain codepoint.
//! \param os Output for copying one codepoint.
//! \return true if it is valid.
//! \note This function just validating and copying the codepoint without actually decode it.
template <typename InputStream, typename OutputStream>
static bool Validate(InputStream& is, OutputStream& os);
// The following functions are deal with byte streams.
//! Take a character from input byte stream, skip BOM if exist.
template <typename InputByteStream>
static CharType TakeBOM(InputByteStream& is);
//! Take a character from input byte stream.
template <typename InputByteStream>
static Ch Take(InputByteStream& is);
//! Put BOM to output byte stream.
template <typename OutputByteStream>
static void PutBOM(OutputByteStream& os);
//! Put a character to output byte stream.
template <typename OutputByteStream>
static void Put(OutputByteStream& os, Ch c);
};
由于 C++ 可使用不同类型作为字符类型,如 char、wchar_t、char16_t (C++11)、char32_t (C++11)等,实现这个 Encoding 概念的类需要设定一个 Ch 类型。
这当中最种要的函数是 Encode() 和 Decode(),它们分别把码点编码至输出流,以及从输入流解码成码点。Validate()则是只验证编码是否正确,并复制至目标流,不做解码工作。例如 UTF-16 的编码/解码实现是:
template<typename CharType = wchar_t>
struct UTF16 {
typedef CharType Ch;
RAPIDJSON_STATIC_ASSERT(sizeof(Ch) >= 2);
enum { supportUnicode = 1 };
template<typename OutputStream>
static void Encode(OutputStream& os, unsigned codepoint) {
RAPIDJSON_STATIC_ASSERT(sizeof(typename OutputStream::Ch) >= 2);
if (codepoint <= 0xFFFF) {
RAPIDJSON_ASSERT(codepoint < 0xD800 || codepoint > 0xDFFF); // Code point itself cannot be surrogate pair
os.Put(static_cast<typename OutputStream::Ch>(codepoint));
}
else {
RAPIDJSON_ASSERT(codepoint <= 0x10FFFF);
unsigned v = codepoint - 0x10000;
os.Put(static_cast<typename OutputStream::Ch>((v >> 10) | 0xD800));
os.Put((v & 0x3FF) | 0xDC00);
}
}
template <typename InputStream>
static bool Decode(InputStream& is, unsigned* codepoint) {
RAPIDJSON_STATIC_ASSERT(sizeof(typename InputStream::Ch) >= 2);
Ch c = is.Take();
if (c < 0xD800 || c > 0xDFFF) {
*codepoint = c;
return true;
}
else if (c <= 0xDBFF) {
*codepoint = (c & 0x3FF) << 10;
c = is.Take();
*codepoint |= (c & 0x3FF);
*codepoint += 0x10000;
return c >= 0xDC00 && c <= 0xDFFF;
}
return false;
}
// ...
};
转码
RapidJSON 的解析器可以读入某种编码的JSON,并转码为另一种编码。例如我们可以解析一个 UTF-8 JSON文件至 UTF-16 的 DOM。我们可以实现一个类做这样的转码工作:
template<typename SourceEncoding, typename TargetEncoding>
struct Transcoder {
//! Take one Unicode codepoint from source encoding, convert it to target encoding and put it to the output stream.
template<typename InputStream, typename OutputStream>
RAPIDJSON_FORCEINLINE static bool Transcode(InputStream& is, OutputStream& os) {
unsigned codepoint;
if (!SourceEncoding::Decode(is, &codepoint))
return false;
TargetEncoding::Encode(os, codepoint);
return true;
}
// ...
};
这段代码非常简单,就是从输入流解码出一个码点,解码成功就编码并写入输出流。但如果来源的编码和目标的编码都一样,我们不是做了无用功么?但 C++ 的[模板偏特化(partial template specialization)可以这么做:
//! Specialization of Transcoder with same source and target encoding.
template<typename Encoding>
struct Transcoder<Encoding, Encoding> {
template<typename InputStream, typename OutputStream>
RAPIDJSON_FORCEINLINE static bool Transcode(InputStream& is, OutputStream& os) {
os.Put(is.Take()); // Just copy one code unit. This semantic is different from primary template class.
return true;
}
// ...
};
那么,不用转码的时候,就只需复制编码一个单元。零开销!所以,在解析及生成 JSON 时都使用到 Transcoder 去做编码转换。
UTF-8 解码与 DFA
在 UTF-8 中,一个码点可能会编码为1至4个编码单元(字节)。它的解码比较复杂。RapidJSON 参考了 Hoehrmann 的实现,使用确定有限状态自动机(deterministic finite automation, DFA)的方式去解码。UTF-8的解码过程可以表示为以下的DFA:

当中,每个转移(transition)代表在输入流中遇到的编码单元(字节)范围。这幅图忽略了不合法的范围,它们都会转移至一个错误的状态。
原来我希望在本文中详细解析 RapidJSON 实现中的「优化」。但几年前在 Windows 上的测试结果和近日在 Mac 上的测试结果大相迳庭。还是等待之后再分析后再讲。
AutoUTF
有时候,我们不能在编译期决定 JSON 采用了哪种编码。而上述的实现都是在编译期以模板类型做挷定的。所以,后来 RapidJSON 加入了一个运行时做动态挷定的编码类型,称为 AutoUTF。它之所以称为自动,是因为它还有检测字节顺序标记(byte-order mark, BOM)的功能。如果输入流有 BOM,就能自动选择适当的解码器。不过,因为在运行时挷定,就需要多一层间接。RapidJSON采用了函数指针的数组来做这间接层。
ASCII
有一个用家提出希望写入 JSON 时,能把所有非 ASCII 的字符都写成 \uXXXX 转义形式。解决方法就是加入了 ASCII 这个模板类:
template<typename CharType = char>
struct ASCII {
typedef CharType Ch;
enum { supportUnicode = 0 };
// ...
template <typename InputStream>
static bool Decode(InputStream& is, unsigned* codepoint) {
unsigned char c = static_cast<unsigned char>(is.Take());
*codepoint = c;
return c <= 0X7F;
}
// ...
};
通过检测 supportUnicode,写入 JSON 时就可以决定是否做转义。另外,Decode()时也会检查是否超出 ASCII 范围。
总结
RapidJSON 提供内置的 Unicode 支持,包括各种 UTF 格式及转码。这是其他 JSON 库较少做的部分。另外,RapidJSON 是在输入输出流的层面去处理,避免了把整个JSON读入、转码,然后才开始解析。RapidJSON 这么实现节省内存,而且性能应该更优。
最近为了开发 RapidJSON 下一个版本新增的 JSON Schema 功能,实现了一个正则表达式引擎。该引擎也利用了 Encoding 这套框架,轻松地实现了 Unicode 支持,例如可以直接匹配 UTF-8 的输入流。
RapidJSON 代码剖析(三):Unicode 的编码与解码的更多相关文章
- RapidJSON 代码剖析(四):优化 Grisu
我曾经在知乎的一个答案里谈及到 V8 引擎里实现了 Grisu 算法,我先引用该文的内容简单介绍 Grisu.然后,再谈及 RapidJSON 对它做了的几个底层优化. (配图中的<Grisù& ...
- RapidJSON 代码剖析(二):使用 SSE4.2 优化字符串扫描
现在的 CPU 都提供了单指令流多数据流(single instruction multiple data, SIMD)指令集.最常见的是用于大量的浮点数计算,但其实也可以用在文字处理方面. 其中,S ...
- RapidJSON 代码剖析(一):混合任意类型的堆栈
大家好,这个专栏会分析 RapidJSON (中文使用手册)中一些有趣的 C++ 代码,希望对读者有所裨益. C++ 语法解说 我们先来看一行代码(document.h): bool StartArr ...
- unicode的编码与解码
- HDFS集中式的缓存管理原理与代码剖析--转载
原文地址:http://yanbohappy.sinaapp.com/?p=468 Hadoop 2.3.0已经发布了,其中最大的亮点就是集中式的缓存管理(HDFS centralized cache ...
- HDFS集中式的缓存管理原理与代码剖析
转载自:http://www.infoq.com/cn/articles/hdfs-centralized-cache/ HDFS集中式的缓存管理原理与代码剖析 Hadoop 2.3.0已经发布了,其 ...
- Python中GBK, UTF-8和Unicode的编码问题
编码问题,一直是使用python2时的一块心病.几乎所有的控制台输入输出.IO操作和HTTP操作都会涉及如下的编码问题: UnicodeDecodeError:‘ascii’codec can’t d ...
- x264代码剖析(三):主函数main()、解析函数parse()与编码函数encode()
x264代码剖析(三):主函数main().解析函数parse()与编码函数encode() x264的入口函数为main().main()函数首先调用parse()解析输入的參数,然后调用encod ...
- 三种字符编码:ASCII、Unicode和UTF-8
原文:三种字符编码:ASCII.Unicode和UTF-8 什么是字符编码? 计算机只能处理数字,如果要处理文本,就必须先把文本转换为数字才能处理.最早的计算机在设计时采用8个比特(bit)作为一个字 ...
随机推荐
- jquery键盘事件总结
在工作中在发现同事在写输入密码按键的相关js效果时,发现自己对于这块很是不了解,这几天特地了解了一下,进行以下总结: 一.首先要知道键盘事件的几个属性: 1.keydown():在键盘按下时触发. 2 ...
- Spring(5)—— 注解
注解Annotation,是一种类似注释的机制,在代码中添加注解可以在之后某时间使用这些信息.跟注释不同的是,注释是给我们看到,java虚拟机不会编译,注解也是不编译的,但是我们可以通过反射机制来读取 ...
- 一些有用的SAP技术TCODE
Background Processing RZ01 Job Scheduling Monitor SM36 Schedule Background Job SM36WIZ Job definitio ...
- tabbarItem字体及图片颜色设置
tabbarItem设置图片后运行往往与我们原始图片颜色有出入,这是因为在默认情况下,未选中状态图片和字体颜色为灰色,选中状态下图片和字体颜色为蓝色. UIImage 在呈现(render)时会选 ...
- Android自定义控件6--轮播图广告的实现
本文接着实现轮播图广告的监听滚动 本文地址:http://www.cnblogs.com/wuyudong/p/5920757.html,转载请注明源地址. 首先添加布局文件,实现小白点 shape_ ...
- FMDB第三方框架
FMDB是同AFN,SDWebImage同样好用的第三方框架,它以OC的方式封装了SQLite的C语言API,使得开发变得简单方便. 附上github链接https://github.com/ccgu ...
- C# 6.0新特性
因为在博客中给出的代码大多数都使用了C#6.0的新特性,如果各位对C#6.0还不了解,可以简单的看一下这篇随笔.o( ̄▽ ̄)d 先来看一个Point类 public class Point { pub ...
- 从零自学Hadoop(05):Ambari
阅读目录 序 引入背景 Ambari介绍 在线安装 系列索引 本文版权归mephisto和博客园共有,欢迎转载,但须保留此段声明,并给出原文链接,谢谢合作. 文章是哥(mephisto)写的,Sour ...
- 基于Ajax+div的“左边菜单、右边内容”页面效果实现
效果演示: ①默认页面(index.jsp): ②:点击左侧 用户管理 标签下的 用户列表 选项后,右边默认页面内容更新为用户列表页(userList.jsp)的内容 : ③:同理,点击 产品管理.订 ...
- 在Dell R720服务器上安装ESXI5.5时会出现卡在LSI_MR3.V00的解决方法
接近年底,公司各种活动,各种加班,导致没有太多时间写博客,今抽了点时间将前几天搭建虚拟化服务时所出现的一个问题描述下: 服务器配置:CUP E5-2609 内存32G 硬盘5 ...