RapidJSON 代碼剖析(三):Unicode 的編碼與解碼

根據 RFC-7159html

8.1 Character Encodinggit

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-八、UTF-1六、UTF-32編碼。缺省編碼爲UTF-8,並且有大量的實現能讀取以UTF-8編碼的JSON文本,說明UTF-8具互操做性;有許多實現不能讀取其餘編碼(如 UTF-16及UTF-32)github

RapidJSON 但願儘可能支持各類經常使用 UTF 編碼,用四百多行代碼實現了 5 種 Unicode 編碼器/解碼器,另外加上 ASCII 編碼。本文會簡單介紹它的實現方式。正則表達式

(配圖爲老彼得·布呂赫爾筆下的巴別塔json

回顧 Unicode、UTF 與 C++

Unicode 是一個標準,用於處理世界上大部分的文字。在 Unicode 出現以前,每種語言文字會使用不一樣的編碼,例如英文主要用 ASCII、中文主要用 GB 2312 和大五碼、日文主要用 JIS 等等。這樣會形成不少不便,例如一個文本信息很難混合各類語言的文字。api

Unicode 定義了統一字符集(Universal Coded Character Set, UCS),每一個字符映射至一個整數碼點(code point),碼點的範圍是 0 至 0x10FFFF。儲存這些碼點有不一樣方式,這些方式稱爲 Unicode 轉換格式(Uniform Transformation Format, UTF)。現時流行的 UTF 爲 UTF-八、UTF-16 和 UTF-32。每種 UTF 會把一個碼點儲存爲一至多個編碼單元(code unit)。例如 UTF-8 的編碼單元是 8 位的字節、UTF-16 爲 16 位、UTF-32 爲 32 位。除 UTF-32 外,UTF-8 和 UTF-16 都是可變長度編碼。數組

UTF-8 成爲現時互聯網上最流行的格式,有幾個緣由:框架

  1. 它採用字節爲編碼單元,不會有字節序(endianness)的問題。
  2. 每一個 ASCII 字符只需一個字節去儲存。
  3. 若是程序原來是以字節方式儲存字符,理論上不須要特別改動就能處理 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++ 可以使用不一樣類型做爲字符類型,如 charwchar_tchar16_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 的輸入流。

相關文章
相關標籤/搜索