時隔 15.6 個月,終於發佈了一個新版本 v1.1.0。 新版本除了包含了這些日子收集到的無數的小改進及 bug fixes,也有一些新功能。本文嘗試從使用者的角度,簡單介紹一下這些功能和沿由。 ...
時隔 15.6 個月,終於發佈了一個新版本 v1.1.0。
新版本除了包含了這些日子收集到的無數的小改進及 bug fixes,也有一些新功能。本文嘗試從使用者的角度,簡單介紹一下這些功能和沿由。
Photo by Ian Schneider
JSON Pointer
也許 RapidJSON 一直最為人垢病的地方,是它奇怪的 API 設計。例如,對 DOM 加添數據要給於 allocator
參數:
#include "rapidjson/document.h"
using namespace rapidjson;
// ...
Document d(kObjectType);
Value a(kArrayType);
for (int i = 1; i <= 4; i++)
a.PushBack(i, d.GetAllocator());
d.AddMember("a", a, d.GetAllocator());
// { a : [1, 2, 3, 4] }
這是由於 RapidJSON 的 DOM 使用局部的分配器,以避免全局分配器的問題。而為了節省記憶體,每個 Value
不會存儲分配器的指針,所以必須從外部提供。
此設計也導致另一種問題。我們看看一個例子,使用 DOM API 訪問以下這個 JSON:
{
"widget": {
"window": {
"title": "Sample Konfabulator Widget"
}
}
}
要訪問 title
,最直覺想到的應該是這樣:
Document d;
d.Parse(json);
std::cout << d["widget"]["window"]["title"].GetString();
如果 widget
、window
或 title
不存在呢?以標準庫 std::map::operator[]
的做法來說,當找不到鍵,它會自動加入一個鍵值對,並返回該值(所以 map::operator[]
必須是 non-const 函數)。然而,RapidJSON 創建值的時候需要 allocator
,所以不可能自動加入鍵值對。因此,RapidJSON 規定以 operator[]
訪問時,必須確保鍵存在(找不到時直接斷言失敗)。若不能確保,應先用 HasMember()
判斷,或更好的是使用 FindMember()
,因為它可以告之鍵是否存在的同時,能通過迭代器取得該值。可是,使用 FindMember()
去訪問多層對象,代碼非常冗長:
Value::ConstMemberIterator itr1 = d.FindMember("widget");
if (itr1 != d.MemberEnd()) {
const Value& widget = itr1->value;
if (widget.IsObject()) {
Value::ConstMemberIterator itr2 = widget.FindMember("window");
if (itr2 != widget.MemberEnd()) {
const Value& window = itr2->value;
if (window.IsObject()) {
Value::ConstMemberIterator itr3 = window.FindMember("title");
if (itr3 != window.MemberEnd()) {
const Value& title = itr3->value;
if (title.IsString()) {
std::cout << title.GetString();
}
}
}
}
}
}
這坨代碼也許是最快最直接的方式。但一般業務代碼寫成這樣,可讀性太低,也容易出錯。
大家都可以寫一些輔助函數來解決這個問題。而我選擇了實現 RFC6901 ── JSON Pointer。先看看使用後的結果:
#include "rapidjson/pointer.h"
// ...
if (const Value* title = GetValueByPointer(d, "/widget/window/title")) {
if (title->IsString()) {
std::cout << title->GetString();
}
}
這個版本簡單得多吧,"/widget/window/title"
是一個 JSON Pointer 的字元串形式,然後 GetValueByPointer()
在 d
上解引用,如果失敗就返回空指針。
在邏輯上是和上面的冗長版本是一模一樣的,只是增加了一些解析 JSON Pointer 的運行時間及空間成本。對大多數人來說,應該更會接受這個版本。
有時候,業務邏輯還會是這樣的:「如果鍵不存在,就使用預設值。」RapidJSON 的 JSON Pointer 也提供此功能:
Value& title = GetValueByPointerWithDefault(
d, "/widget/window/title", "untitled");
當解引用失敗時,它會創建整個路徑,並把預設值複製成新值,並返回該值。由於它總能返回一個值,此函數的返回類型為引用而不是指針。
在此也簡單介紹一下 JSON Pointer 的語法。它以 '/' 分隔多個 token,而每個 token 可以是 JSON object 的鍵,也可以是 JSON array 的下標。還有一種特殊 token 是負號 -
,它可以指 JSON array 最後元素的下一個元素。使用這種特性能實現 PushBack()
的效果:
Document d;
CreateValueByPointer(d, "/a").SetArray();
for (int i = 1; i <= 4; i++)
SetValueByPointer(d, "/a/-", i);
// { a : [1, 2, 3, 4] }
使用 JSON Pointer 的另一優點在於,它本身也是一個字元串,可以放置在 JSON 或其他文本格式之中。那麼,我們便有一個標準方式去引用 JSON 中的值。
希望 JSON Pointer 能減輕使用者的負擔,同時也提供一種數據驅動的彈性。新功能 JSON Pointer 簡單介紹至此,更多信息可參考 RapidJSON 使用手冊:Pointer。
JSON Schema
上面我們也談到一個問題,JSON 里的組織方式、類型可能和預期的不同,我們可能要寫很多代碼去校驗一個 JSON 的格式是否乎合預期。特別是後臺伺服器可能接收到不正常的JSON,甚至是惡意編寫的 JSON 以圖攻擊。
在 XML 的世界中,可使用 XML DTD 或 XML Schema 去描述 XML 的結構。在 JSON 的世界中,已經有相關草案,稱為 JSON Schema。
RapidJSON 實現了 JSON Schema v4 draft,並正式納入了 v1.1.0。先看看用法:
#include "rapidjson/schema.h"
// ...
Document sd;
if (!sd.Parse(schemaJson).HasParseError()) {
// 此 schema 不是合法的 JSON
}
SchemaDocument schema(sd); // 把一個 Document 編譯至 SchemaDocument
// 之後不再需要 sd
Document d;
if (!d.Parse(inputJson).HasParseError()) {
// 輸入不是一個合法的 JSON
}
SchemaValidator validator(schema);
if (!d.Accept(validator)) {
// 輸入的 JSON 不合乎 schema
}
以我所知,現時所有 JSON Schema 實現都是校驗一個 DOM 是否合乎 Schema。RapidJSON 做了一個創新的嘗試,以事件流(SAX 風格)的方式去做校驗。上面的例子利用 Document::Accept()
產生事件流,然後送交 SchemaValidator
校驗。也許讀者會問:「這也是在校驗一個 DOM 是否合乎 Schema,有什麼特別嗎?」
這實際意味著,RapidJSON 的 JSON Schema 校驗器除了可以校驗 DOM,也可以校驗更底層的 SAX。例如,我們可以用 SAX 解析 JSON 時,同時進行 JSON Schema 校驗。如果中途不合乎 JSON Schema,就能直接中止解析。
SchemaValidator validator(schema);
Reader reader;
if (!reader.Parse(stream, validator)) {
if (!validator.IsValid()) {
// 輸入的 JSON 不合乎 schema
}
}
也可以同時把事件轉發至一個自定義 handler:
MyHandler handler;
GenericSchemaValidator<SchemaDocument, MyHandler> validator(schema, handler);
Reader reader;
if (!reader.Parse(stream, validator)) {
if (!validator.IsValid()) {
// 輸入的 JSON 不合乎 schema
}
}
由於 DOM 解析 JSON 時,底層也是使用 SAX,所以也可以同時做 Schema 校驗。其實除瞭解析,在生成時也可以進行校驗,以確保輸出的 JSON 也是乎合 Schema 的。這些用法都可參考 RapidJSON 使用手冊:Schema。要學習 JSON Schema 的寫法,筆者推薦 Understanding JSON Schema 這個英文網站。
C++11 範圍 for 迴圈
此版本還加入了 Array
和 Object
輔助類型(包裹類),可分別通過 Value::GetArray()
、Value::GetObject()
獲取。這兩個輔助類型提供該 JSON 類型專門的介面,例如 Array::PushBack()
、Object::AddMember()
等。更重要的是,為了令 C++11 用戶使用得更順手,它們可做範圍 for
迴圈(range-based for loop):
// C++03
for (Value::ConstValueIterator itr = a.Begin(); itr != a.End(); ++itr)
printf("%d ", itr->GetInt());
// C++11
for (auto& v : a.GetArray())
printf("%d ", v.GetInt());
// C++03
for (Value::ConstMemberIterator itr = document.MemberBegin();
itr != document.MemberEnd(); ++itr)
{
printf("Type of member %s is %s\n",
itr->name.GetString(), kTypeNames[itr->value.GetType()]);
}
// C++11
for (auto& m : document.GetObject())
printf("Type of member %s is %s\n",
m.name.GetString(), kTypeNames[m.value.GetType()]);
其他相關詳情可參閱 RapidJSON 使用手冊:教程。
結語
這個 RapidJSON 版本對我而言是一個挑戰。
JSON Schema 實際上也需要 JSON Pointer,所以 JSON Pointer 可算是一舉兩得的新功能。但實現 JSON Schema 時有兩個難點。一個是 JSON Schema 需要正則引擎,在 C++11 下能直接使用 std::regex
;而為了 C++03,我還實現了一個 500 行代碼的 Thompson NFA 正則引擎。另一個難點在於,事件流的校驗不容易實現 allOf
、anyOf
、oneOf
、not
等關鍵字,需要多個校驗器同時檢驗事件流。
新功能 JSON Schema 和 JSON Pointer 都是附加功能,完全不影響 v1.0.x 的 API。
除新功能外,此版本含有一個重要的記憶體優化。在 x86-64 架構下,64 位指針只使用到 48 位,我重新設計了 Value
的排布,使每個值的記憶體開銷從 24 位元組縮減至 16 位元組。雖然存儲指針時會有時間開銷,但因大量縮減記憶體,更好的緩存一致性應該可以釐補損失,甚至能進一步提升整體性能。
屈指一算,RapidJSON 已快近 5 個年頭了,最近一年我轉部門後,更少機會在工作上使用 RapidJSON,所以我可能較少機會發現問題和新需求。雖然是這樣,我仍然會繼續維護這個項目,也要靠大家去發現問題和新需求,希望能得到大家的意見。
P.S. 可能大家會關心性能,我會儘快更新 nativejson-benchmark。