xml パーサを書いたのでそれについて

ちょっと特殊な環境下で xml をパースする必要があって、いろいろと xml パースライブラリを探したのですが、どれもちょっとづつ求めるものと違うところがありうまく行かず、自前で xml パーサを書く必要がありました。
その時のことを書いてみようかと思います。

開発言語

開発言語は C++ です。ですが、以下の様な制約がありました。

  • 言語はほぼ C++03。
  • 例外が使用できない。
  • RTTI が使用できない。
  • boost は使用できない。(標準ライブラリはOK.)

boostはヘッダのみで使用できるものであれば導入することは可能ではあるのですが、プロジェクトのメンバーを説得することができませんでした。

実行環境の特性

実行環境はちょっとした組み込み機器です。
メモリは潤沢にあるのですが、その割り当て(malloc)が無視できないレベルで遅いため、細切れの new(malloc) をできるだけ避ける必要がありました。malloc 自体にも手を入れることは可能ではあるのですが、スレッド同期コスト自体がバカにならないため、そもそもの new(malloc) の回数を減らす必要がありました。

プロジェクト的な背景

汎用的なものを作成する体力はなかったので、プロジェクトで不要なxml的機能を諦めて仕様をシンプルにしようと考えました。
プロジェクト的に xml の読み込みには以下のような背景がありました。

  • 読み込む xml はそれほど大きなものではない。大きくて 数百KB。
  • 読み込む xml の種類が多い。また、頻繁に仕様の変更が予想される。
  • xml を作ることはない。編集することもない。
  • パースの失敗は本来はありえない。失敗は『失敗した』ということだけがわかればよく、詳細は必要ない。
  • 汎用的な木構造を持つデータの伝達のみに使用する。xml namespace などの機能は一切必要ない。
  • 文字コードutf-8 決め打ちで良い。

おおまかな設計

まず、sax系はナシです。スキーマの変更への追従のコストが高すぎます。
dom ツリーを作って、ルートからたどっていってお目当てのデータにたどり着くコードをプログラマがちゃちゃっと書ける環境を作らなくてはいけません。

で、dom 系のほうがいいのですが、一般に dom系パーサは大きなメモリを喰います。このパーサーを作る前は tinyxml(http://www.grinninglizard.com/tinyxml/) を使用してコードを書いていたのですが、実際メモリ消費量と確保の回数的に許容できるものではありませんでした。
なので、なんとか考えなくてはいけないのですが、tinyxml が行う new はそのほとんどが string のインスタンスのためだったりします。
これは tinyxml では dom の操作が可能なためです。dom を操作するからには各ノードのオブジェクトが自分のタグ名やアトリビュートの key-value を個別に文字列として持っておく必要があります。
まず、これを諦めます。

プロジェクト的に dom の編集はありえず、元となるドキュメントの文字列も変更されることはありません。なので、各ノードが持つ文字列は元のドキュメント内の位置を覚えておけば良いことになります。
xml のパースに最適化をするならば、全体として文字列のポインタ一つ、各ノードはその中のインデックスと長さのみを持てば良いのでしょうが、ある程度プロジェクト全体で使用できる道具を作りたいという要求もあったため、『別途寿命管理された文字列の部分文字列を示す、内部には const char* で開始位置、int で文字列長を持つクラス』を作ることとします。

次にノードですが、ノードは木構造を作らないといけないためどうしても動的に確保を行う必要があります。ですが、これを new するのは確保の回数的に許容できません。
幸いメモリは潤沢にあるため、多少確保しすぎることはそれほど大きな問題ではありません。なので、メモリチャンクを作ることにします。これによって同期コストも減らすことができます。xmlのパースに最適化したいわけではないので、ある程度はほかでも使用できる固定長メモリチャンクを作ることとします。実装は Loki の SmallObject を参考にすることとしましょう。


続くかも。