ぶていのログでぶログ

思い出したが吉日

mruby-rapidyamlを作った

rfコマンドv1.23.0をリリースしたで書いた通りmruby-rapidyamlを作ったのでそれの紹介記事。

github.com

rapidyamlとは?

github.com

rapidyaml(またはryml)は名前のとおり高速性を謳ったYAMLパーサーである。 rapidyamlのREADMEにはyaml-cppとの速度比較がのっていて、それを見ると30倍近く高速に処理できるとかかれている。 yaml-cppではないが、手元でmruby-yaml(libyaml)を使ったrfとmruby-rapidyamlを使ったrfで比較したときに高速に処理していた*1

rapidyamlはC++11で書かれているためコンパイルにはC++のコンパイラが必要になる。 そのため、mruby-yamlでもビルドにC++のコンパイラが必要になってくるが、mrubyはC++でのビルドにも対応しているので、C++が使えないという環境でなければ問題なく使えるはず。

rapidyamlでYAMLをパースするときにいくつかの注意点がある。 詳しくはREADMEを見てほしいが、例をあげるとMapのキーの区切り : のあとにTABを配置できないという制約がある*2

なぜrapidyamlを選んだか?

C/C++から扱えるYAMLライブラリはいくつかあるのだが、その中でrapidyamlを選んだ理由は

  1. 高速性を売りにしていること
  2. シングルヘッダーで完結していること

の2つが挙げられる。 1はmruby-yyjsonで高速なJSONライブラリを作ったので、rfでYAMLも高速にパースしたいという思いがあった。 2については、リポジトリに追加しやすく、かつ、mrubyのビルドシステムで扱いやすいというのが理由。 ヘッダファイルだけもってきて、メインのcppファイルからincludeすれば、あとはrakeするだけで使えるようになるのはお手軽で良い。 ただし、1ファイルがかなりデカくなるのとデバッグしづらくなることとのトレードオフではある…。

これらの理由でrapidyamlを選んだ。

rapidyamlを使うには?

rapidyamlのリポジトリのUsing ryml in your projectにライブラリの組み込み方が記載されている。 mruby-rapidyamlのようにシングルヘッダーで使用するには、rapidyamlのリポジトリに含まれるtools/amalgamate.pyを使ってシングルヘッダーを作成する必要がある。 詳しくは該当の項目を参照のこと。 で、実際にrapidyamlを使う場合はQuick startsamplesが参考になる。

…のだが、事前に知っておかないとよくわからんっとなったり、sample以外のことをやろうとするとなんもわからんとなるので、この記事で補足したいとおもう。 なお、C++11の知識についてはここでは省略する。

c4core

rapidyamlではc4で始まる関数やクラスが多く出てくる。 これはc4coreというライブラリでrapidyamlの作者が作ったライブラリである。 STLにくらべて高速で文字列を処理できるようだ。 rapidyamlではこのc4coreに強く依存しているので、このc4coreの使い方を知っておくと良い。 よくつかうのはc4::csubstrとc4::substrあたり。 それぞれ、文字列を扱うクラスで、cがついている方がconstでRead-only(なはず)。

rapidyamlでの利用例としては、YAMLのkeyやvalueを設定や取得するときに使うことが多い。 以下は実際のコード例。

// const char*からc4::csubstrに変換
auto str = c4::csubstr("hello world");

// mruby Stringからc4::csubstrに変換
// lengthを指定しないとlength以上の文字列が取得されるので注意
auto str = c4::csubstr(RSTRING_PTR(mrb_str), RSTRING_LEN(mrb_str));

// c4::csubstrからmruby Stringに変換
// mrb_str_new_cstrを使いたいところだが、lenを指定しないとlength以上の文字列が取得されるので注意
auto mrb_str = mrb_str_new(mrb, str.str, str.len);

Node

rapidyamlではYAMLのパースや書き込みをするときに、キーや値をNodeという概念で扱う。 基本的にはryml::TreeからNodeのポインタを取得して、そのポインタに対して操作を行う。

// YAMLのパース(Quick startから引用)
char yml_buf[] = "{foo: 1, bar: [2, 3], john: doe}";
ryml::Tree tree = ryml::parse_in_place(yml_buf);

// YAMLの書き込み時には変数を定義するだけでよさそう
ryml::Tree tree;

// Nodeの取得
ryml::ConstNodeRef node = tree.rootref();

このNodeにはtype(NodeType)があり、map, seq, scalarの3つがある。 mapとseqはそれぞれ is_map()is_seq() で判定できる。 その2つがfalseの場合はscalarとなる。 Nodeの値は val() で取得できるが、直接変数に代入することもできる。 たとえば、 c4::csubstr str = node.val(); とすることもできる。 mapの場合はキーが取得でき、 key() で取得できる。

新しくNodeを作成する場合は、append_child()を使用する。 scalarの場合は、append_child()で追加されたNodeに対して直接値を設定すればよい。 seqの場合は、ryml::SEQ NodeTypeを設定したうえでappend_child()し、mapの場合はryml::MAP NodeTypeを設定したうえで、append_child()する。 ややこしいので、以下にコード例を示す。

// scalarのNodeを作成する例
ryml::NodeRef node = tree.rootref();
node = "hello world";

// この例ではプレインテキストとして設定している。他にもVAL_LITERALなどある。
// 詳しくはhttps://github.com/biojppm/rapidyaml/blob/master/src/c4/yml/node_type.cpp#L6
node |= ryml::VAL | ryml::VAL_PLAIN;

// seqのNodeを作成する例
ryml::NodeRef node = tree.rootref();
node |= ryml::SEQ;
n = node.append_child();
n = "seq1";
n |= ryml::VAL | ryml::VAL_PLAIN;

n = node.append_child();
n = "seq1";
n |= ryml::VAL | ryml::VAL_PLAIN;

// mapのNodeを作成する例
ryml::NodeRef node = tree.rootref();
node |= ryml::MAP;
n = node.append_child();
// keyを設定
n << ryml::key("key1");
n |= ryml::KEY_PLAIN;
n = "value1";
n |= ryml::VAL | ryml::VAL_PLAIN;

カスタムパーサー

rymlに標準のパーサーが定義さているが、カスタムパーサー(ドキュメントだとevent-based pase engine)を作成することもできる。 標準パーサーではmapのkeyにmapを設定できないという制約があるため、mruby-rapidyamlではカスタムパーサーを作成している。 カスタムパーサーについてのドキュメントはあまりないのでソースコードを読むしかない。。 基本的には、c4::yaml::ParserStateを継承したstructの定義と、c4::yml::EventHandlerStackを継承したクラスを作成すればよい。 ただし、これらを継承しただけでは動かずいくつかのメンバ関数を実装する必要がある。 それらについても書こうと思った…が、力尽きたのでmruby-rapidyamlのソースコードを参照してほしい。 https://github.com/buty4649/mruby-rapidyaml/blob/main/src/event_handler.hpp

おわりに

rapidyamlを使ったmruby-rapidyamlを作成した。 このmrbgemを使うことで高速にYAMLをパースできるようになるはず。 rapidyamlについてもしりすぼみだが…エッセンスについては書いたので、これを参考にしてもらえればと思う。

*1:前回の記事を参照 https://tech.buty4649.net/entry/2024/08/19/113941

*2:コンパイルオプションを設定することでTABを許可することもできるが、READMEによると10%程おそくなるらしい