RapidJSON 程式碼剖析(三):Unicode 的編碼與解碼
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
雖然 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 的輸入流。