Document Object Model(DOM)は、問い合わせや操作を行うための、JSONのメモリー内表現です。DOMの基本的な使い方はチュートリアルで説明しています。この節では、詳しい内容と応用的な使い方を説明します。

テンプレート

チュートリアルではValueとDocumentを使いました。std::stringと同様、これらは実際にはテンプレートクラスのtypedefです。

namespace rapidjson {
template <typename Encoding, typename Allocator = MemoryPoolAllocator<> >
class GenericValue {
// ...
};
template <typename Encoding, typename Allocator = MemoryPoolAllocator<> >
class GenericDocument : public GenericValue<Encoding, Allocator> {
// ...
};
} // namespace rapidjson
JSONテキストをDOMへ解析するための文書。
Definition document.h:2024
JSON値を表します。UTF8エンコーディングと既定のアロケーターにはValueを使います。
Definition document.h:540
RapidJSONの主な名前空間。
Definition rapidjson.h:374
GenericValue< UTF8<> > Value
UTF8エンコーディングのGenericValue。
Definition document.h:2010
GenericDocument< UTF8<> > Document
UTF8エンコーディングのGenericDocument。
Definition document.h:2402

これらのテンプレート引数は、独自に指定できます。

エンコーディング

Encoding引数は、メモリー内のJSON文字列値のエンコーディングを指定します。選択肢はUTF8、UTF16、UTF32です。この3つの型もテンプレートクラスです。UTF8<>はUTF8<char>で、文字の保存にcharを使うことを意味します。詳しくはエンコーディングを参照してください。

Windowsアプリケーションで、JSONファイルに保存されたローカライズ用の文字列を問い合わせる場合を考えます。WindowsのUnicode対応関数は、UTF-16(ワイド文字)のエンコーディングを使います。JSONファイルのエンコーディングにかかわらず、メモリー内の文字列をUTF-16で保存できます。

using namespace rapidjson;
typedef GenericDocument<UTF16<> > WDocument;
typedef GenericValue<UTF16<> > WValue;
FILE* fp = fopen("localization.json", "rb"); // non-Windows use "r"
char readBuffer[256];
FileReadStream bis(fp, readBuffer, sizeof(readBuffer));
AutoUTFInputStream<unsigned, FileReadStream> eis(bis); // wraps bis into eis
WDocument d;
d.ParseStream<0, AutoUTF<unsigned> >(eis);
const WValue locale(L"ja"); // Japanese
MessageBoxW(hWnd, d[locale].GetString(), L"Test", MB_OK);
エンコーディングを動的に指定し、自動検出する入力ストリームのラッパー。
Definition encodedstream.h:135
fread()を使う入力用のファイルバイトストリーム。
Definition filereadstream.h:34
ストリームの実行時指定のUTFエンコーディング型に応じ、エンコーディングを動的に選択します。
Definition encodings.h:615

アロケーター

Allocatorは、Document/Valueのメモリーを確保・解放するときに使うアロケータークラスを定義します。DocumentはAllocatorのインスタンスを所有するか参照します。一方、Valueはメモリー消費を減らすため、そうしません。

GenericDocumentで使う既定のアロケーターはMemoryPoolAllocatorです。このアロケーターは、実際にはメモリーを順番に確保し、個別には解放できません。JSONをDOMツリーへ解析する場合に、とても適しています。

別のアロケーターとしてCrtAllocatorがあります。CRTはC RunTime libraryの略です。このアロケーターは、標準のmalloc()/realloc()/free()を呼び出すだけです。追加・削除が多い場合には、こちらが適している場合があります。ただし、効率はMemoryPoolAllocatorより大幅に低くなります。

解析

Documentは、解析用の関数を複数提供します。以下では(1)が基本となる関数で、他は(1)を呼び出す補助関数です。

using namespace rapidjson;
// (1) Fundamental
template <unsigned parseFlags, typename SourceEncoding, typename InputStream>
// (2) Using the same Encoding for stream
template <unsigned parseFlags, typename InputStream>
// (3) Using default parse flags
template <typename InputStream>
// (4) In situ parsing
template <unsigned parseFlags>
// (5) In situ parsing, using default parse flags
// (6) Normal parsing of a string
template <unsigned parseFlags, typename SourceEncoding>
// (7) Normal parsing of a string, using same Encoding of Document
template <unsigned parseFlags>
// (8) Normal parsing of a string, using default parse flags
GenericDocument & ParseStream(InputStream &is)
入力ストリームからJSONテキストを解析します(エンコーディング変換あり)。
Definition document.h:2155
GenericDocument & Parse(const typename SourceEncoding::Ch *str)
読み取り専用文字列からJSONテキストを解析します(エンコーディング変換あり)。
Definition document.h:2221
GenericDocument & ParseInsitu(Ch *str)
変更可能な文字列からJSONテキストを解析します。
Definition document.h:2198

チュートリアルの例は、通常の文字列解析に(8)を使います。ストリームの例は、最初の3つを使います。In situ解析については、この後で説明します。

parseFlagsは、次のビットフラグの組み合わせです。

解析フラグ意味
kParseNoFlagsフラグを設定しません。
kParseDefaultFlags既定の解析フラグです。マクロRAPIDJSON_PARSE_DEFAULT_FLAGSと同じ値で、このマクロはkParseNoFlagsとして定義されています。
kParseInsituFlagin situ(破壊的)解析。
kParseValidateEncodingFlagJSON文字列のエンコーディングの妥当性を検証します。
kParseIterativeFlag反復型の解析(関数呼び出しスタックの大きさは一定)。
kParseStopWhenDoneFlagストリームから完全なJSONのルートを解析したら、ストリームの残りの処理を停止します。このフラグを使うと、パーサーはkParseErrorDocumentRootNotSingularエラーを発生させません。同じストリームの複数のJSONの解析に使います。
kParseFullPrecisionFlag完全な精度で数値を解析します(低速)。このフラグを設定しない場合は、通常の精度(高速)を使います。通常の精度では、誤差は最大3ULPです。
kParseCommentsFlag単一行の// ...と複数行の/* ... */のコメントを許可します(緩和されたJSON構文)。
kParseNumbersAsStringsFlag数値型の値を文字列として解析します。
kParseTrailingCommasFlagオブジェクトや配列の末尾のカンマを許可します(緩和されたJSON構文)。
kParseNanAndInfFlagNaN、Inf、Infinity、-Inf、-Infinityをdouble値として解析することを許可します(緩和されたJSON構文)。

関数の引数ではなく非型テンプレート引数を使うことで、C++コンパイラーは指定した組み合わせに最適化したコードを生成できます。これにより速度が向上し、単一の特殊化だけを使う場合にはコードサイズも減ります。一方、フラグをコンパイル時に決める必要があります。

SourceEncoding引数は、ストリームのエンコーディングを定義します。これはDocumentのEncodingと異なっていても構いません。詳しくはエンコーディング変換と妥当性検証の節を参照してください。

InputStreamは入力ストリームの型です。

解析エラー

解析に成功すると、Documentに解析結果が入ります。エラーがある場合、元のDOMは変更されません。解析のエラー状態は、bool HasParseError()、ParseErrorCode GetParseError()、size_t GetParseOffset()で取得できます。

解析エラーコード説明
kParseErrorNoneエラーなし。
kParseErrorDocumentEmpty文書が空です。
kParseErrorDocumentRootNotSingular文書のルートの後に、他の値が続いてはいけません。
kParseErrorValueInvalid不正な値です。
kParseErrorObjectMissNameオブジェクトのメンバー名がありません。
kParseErrorObjectMissColonオブジェクトのメンバー名の後に、コロンがありません。
kParseErrorObjectMissCommaOrCurlyBracketオブジェクトのメンバーの後に、カンマか}がありません。
kParseErrorArrayMissCommaOrSquareBracket配列の要素の後に、カンマか]がありません。
kParseErrorStringUnicodeEscapeInvalidHex文字列内の\\uエスケープの後に、不正な16進数字があります。
kParseErrorStringUnicodeSurrogateInvalid文字列内のサロゲートペアが不正です。
kParseErrorStringEscapeInvalid文字列内のエスケープ文字が不正です。
kParseErrorStringMissQuotationMark文字列を閉じる引用符がありません。
kParseErrorStringInvalidEncoding文字列のエンコーディングが不正です。
kParseErrorNumberTooBig数値が大きすぎてdoubleに保存できません。
kParseErrorNumberMissFraction数値の小数部がありません。
kParseErrorNumberMissExponent数値の指数部がありません。

エラーのオフセットは、ストリームの先頭からの文字数として定義されます。現在のRapidJSONは、行番号を追跡しません。

エラーメッセージを取得するために、RapidJSONはrapidjson/error/en.hに英語のメッセージを用意しています。他のロケール向けに変更するか、独自のローカライズシステムを使えます。

解析エラーを処理する例を示します。

#include "rapidjson/error/en.h"
// ...
if (d.Parse(json).HasParseError()) {
fprintf(stderr, "\nError(offset %u): %s\n",
(unsigned)d.GetErrorOffset(),
// ...
}
bool HasParseError() const
直前の解析で解析エラーが発生したかどうか。
Definition document.h:2284
ParseErrorCode GetParseError() const
直前の解析のParseErrorCodeを取得します。
Definition document.h:2287
size_t GetErrorOffset() const
入力内の直前の解析エラーの位置を取得します。エラーがなければ0です。
Definition document.h:2290
const RAPIDJSON_ERROR_CHARTYPE * GetParseError_En(ParseErrorCode parseErrorCode)
解析のエラーコードをエラーメッセージへ対応付けます。
Definition en.h:36

in situ解析

Wikipediaより:

In situは、「現場で」や「その位置で」と直訳されるラテン語の語句です。出来事が起こる場所を表す「局所的に」「現場で」「構内で」「その場で」などの意味で、さまざまな文脈に使われます。……(計算機科学では)実行に必要な追加メモリー量がO(1)、つまり入力がどれほど大きくなっても一定量を超えないアルゴリズムを、in situアルゴリズム、またはin-placeアルゴリズムと呼びます。たとえば、ヒープソートはin situのソートアルゴリズムです。

通常の解析では、JSON文字列の復号と他のバッファーへのコピーに大きなオーバーヘッドがあります。In situ解析は、保存されている場所でJSON文字列を復号します。JSONでこれが可能なのは、復号後の文字列の長さが、元のJSON内の長さ以下に必ずなるためです。ここでのJSON文字列の復号とは、"\n"や"\u1234"などのエスケープを処理し、文字列の末尾にヌル終端文字('\0')を加えることです。

以下の図は、通常の解析とin situ解析を比較します。JSON文字列値は、復号した文字列へのポインターを保持します。

通常の解析

通常の解析では、復号した文字列を新たに確保したバッファーへコピーします。"\\n"(2文字)は"\n"(1文字)に、"\\u0073"(6文字)は"s"(1文字)に復号されます。

in situ解析

In situ解析は、元のJSONを変更するだけです。変更した文字は図中で強調表示しています。"msg"のようにJSON文字列がエスケープ文字を含まない場合、解析処理は末尾の二重引用符をヌル文字へ置き換えるだけです。

in situ解析は入力を変更するため、解析APIはconst char*ではなくchar*を必要とします。

// Read whole file into a buffer
FILE* fp = fopen("test.json", "r");
fseek(fp, 0, SEEK_END);
size_t filesize = (size_t)ftell(fp);
fseek(fp, 0, SEEK_SET);
char* buffer = (char*)malloc(filesize + 1);
size_t readLength = fread(buffer, 1, filesize, fp);
buffer[readLength] = '\0';
fclose(fp);
// In situ parsing the buffer into d, buffer will also be modified
d.ParseInsitu(buffer);
// Query/manipulate the DOM here...
free(buffer);
// Note: At this point, d may have dangling pointers pointed to the deallocated buffer.

JSON文字列はconst-stringとして扱われますが、実際には「不変」とは限りません。その寿命はJSONバッファーに依存します。

in situ解析は、メモリー確保のオーバーヘッドとコピーを最小化します。一般に、これによりキャッシュの一貫性が改善します。これは、現代のコンピューターの性能で重要な要因です。

in situ解析には、いくつかの制限があります。

  1. JSON全体がメモリー内にある必要があります。
  2. ストリームの入力エンコーディングと文書の変換先エンコーディングが同じでなければなりません。
  3. 文書を使わなくなるまで、バッファーを保持する必要があります。
  4. 解析後にDOMを長期間使い、DOM内のJSON文字列が少ない場合には、バッファーを保持することがメモリーの無駄になる場合があります。

In situ解析は主に、一度だけ処理してメモリーから解放する、短期間使うJSONに適しています。実際には、このような場面はよくあります。たとえば、JSONからC++オブジェクトへのデシリアライズや、JSONで表されたウェブリクエストの処理などです。

エンコーディング変換と妥当性検証

RapidJSONは、Unicode形式(正式にはUCS Transformation Format)間の変換を内部で処理します。DOMの解析中、ストリームの入力エンコーディングはDOMのエンコーディングと異なっていても構いません。たとえば、入力ストリームのJSONはUTF-8でも、DOMはUTF-16にできます。EncodedInputStreamにコード例があります。

DOMから出力ストリームへJSONを書き込む場合にも、エンコーディング変換を使えます。EncodedOutputStreamに例があります。

エンコーディング変換では、まず入力文字列をUnicodeコードポイントへ復号し、次にコードポイントを変換先の形式へ符号化します。復号時には、入力文字列のバイト列の妥当性を検証します。不正な列であれば、パーサーはkParseErrorStringInvalidEncodingエラーで停止します。

ストリームの入力エンコーディングがDOMのエンコーディングと同じ場合、既定ではパーサーは列の妥当性検証を行いません。kParseValidateEncodingFlagを使えば、強制的に検証できます。

テクニック

DOM APIを使うためのテクニックを説明します。

SAXイベントの発行元としてのDOM

RapidJSONでWriterを使ってDOMを文字列化する方法は、少し奇妙に見えるかもしれません。

// ...
Writer<StringBuffer> writer(buffer);
d.Accept(writer);
bool Accept(Handler &handler) const
この値に対応するイベントをHandlerへ渡します。
Definition document.h:1768
JSONライター。
Definition writer.h:87

実際には、Value::Accept()が値に関するSAXイベントをハンドラーへ発行します。この設計により、ValueとWriterは切り離されています。ValueはSAXイベントを生成でき、Writerはそれらを処理できます。

DOMを他の形式へ変換するための、独自のハンドラーを作れます。たとえば、DOMをXMLへ変換するハンドラーなどです。

SAXイベントとハンドラーの詳しい説明は、SAXを参照してください。

利用者のバッファー

メモリーの確保を、可能な限り避けたいアプリケーションもあります。

MemoryPoolAllocatorは、利用者がバッファーを指定することで、これに対応します。バッファーは、プログラムのスタック上に置いたり、一時データを保存するために静的に確保した「スクラッチバッファー」(static配列やグローバル配列)にしたりできます。

MemoryPoolAllocatorは利用者のバッファーからメモリーを確保します。そのバッファーを使い切ると、基底アロケーター(既定ではCrtAllocator)からメモリーの塊を確保します。

スタックメモリーを使う例を示します。第1のアロケーターは値の保存用、第2のアロケーターは解析中の一時データの保存用です。

char valueBuffer[4096];
char parseBuffer[1024];
MemoryPoolAllocator<> valueAllocator(valueBuffer, sizeof(valueBuffer));
MemoryPoolAllocator<> parseAllocator(parseBuffer, sizeof(parseBuffer));
DocumentType d(&valueAllocator, sizeof(parseBuffer), &parseAllocator);
d.Parse(json);
パーサーとDOMが使う既定のメモリーアロケーター。
Definition allocators.h:102

解析中に確保する合計量が4096+1024バイト未満なら、このコードはnewやmalloc()によるヒープ上のメモリー確保を一度も行いません。

MemoryPoolAllocator::Size()で、現在のメモリー消費量をバイト単位で問い合わせられます。それにより、利用者のバッファーに適した大きさを決められます。