組み込みでも使える?シリアライザ FlatBuffers を使ってみた
この記事はAkerun Advent Calendar 16日目の記事です。
今回はファームウェアエンジニア いとう が担当です。今回は FlatBuffers
と呼ばれるGoogleが開発したシリアライザを使ってみたので書いていこうと思います。以下「組み込み」のワードが出てきますが想定するのはプア目なマイコン(Coretex-M系とか)で組み込みLinuxとかの富豪環境ではないので悪しからず。
シリアライゼーションについて
組み込みエンジニア(リソースプア勢)のみなさん、何らかのシリアライザって使ったことありますか?ここでいうシリアライザはオブジェクトデータを通信や永続化に使えるようにするやつです。Webの世界でシリアライズフォーマットとして一番よく使われるのはJSON
でしょうか?最近はProtocol Buffers
もよく聞きます。
Akerun Proの組み込みファームウェアの中でも色々な場面でデータのシリアライズを行なっています。
- FlashROMに格納する不揮発データの永続化
- BLE通信データのペイロード
- ・・・(そんなになかった)
ですが、Akerun Pro本体のファームウェアでは今現在、特定のシリアライズのライブラリを使用してはいません。組み込みの世界で、シリアライズのライブラリがあまり使われない理由としては色々あると思います。
- データ内容によってサイズが異なると困る
JSON
は数値データでも文字列で表現されているため桁が増えるとデータサイズも増えます- 「FlashROMに履歴情報は**件保持すること」を保証するためにはデータサイズは固定出ないといけません
- リソースがシビアなのでリッチなシリアライズフォーマットが取り扱えない
- 非力なCPUでは処理速度も重要
- メモリを動的確保されると辛い
- あんまりC言語用のライブラリがない
#pragam pack(1)
の構造体でよくね?
個人的に組み込みでも使えるシリアライズフォーマットを探していたのですが、いまいちしっくり来るものがありませんでした。そんな中2014年にGoogleからリリースされたFlatBuffers
はその謳い文句から組み込みで使えるんじゃないかと注目しており、今回機会があったので使ってみました。FlatBuffers
の謳い文句次の通りです。
- Access to serialized data without parsing/unpacking(シリアライズされたデータへのパースなしでのアクセス)
- Memory efficiency and speed(高メモリ効率&速度)
- Flexible(柔軟)
- Tiny code footprint (小さいコードフットプリント)
- Strongly typed(厳密な型)
- Convenient to use (使いやすい)
- Cross platform code with no dependencies(クロスプラットフォーム)
FlatBuffers
のメリット
そもそも、なぜわざわざ特定のシリアライズフォーマットを採用するのでしょうか?私の認識では次のようなメリットがあります。
利点の1番目は明らかですね。Akerun Proは様々なデバイスと直接通信しています。また間接的ですがゲートウェイデバイスを通じてクラウド上のサーバーとも通信しています。
FlatBuffers
のスキーマを作成し、一元的にこれらの多数のデバイス間通信データのデータ構造を自動生成できるとすれば、開発側に大きなメリットです。また、FlatBuffers
は複数のプログラミング言語に対応しており、特定の言語に縛られることがないためDX的にプラスです。公式サイトによると FlatBuffers
は C++, C#, C, Go, Java, JavaScript, Lobster, Lua, TypeScript, PHP, Python, Rustに対応しているそうです。
2番目の利点についてはうまく説明できないのですが・・・
例えば設定情報をFlashROMに保存しているデバイスで、ファームウェアバージョンアップに伴い保存するデータフィールドを増やしたとしましょう。その状態で下記のようなケースに対応できるコードを問題なく書けるでしょうか?
FlatBuffers
ではProtocol Buffers
よりは柔軟性にかけますが少し頑張ればデータ構造の前方&後方互換性を保ったままデータフォーマットを拡張できます。
FlatBuffers
を Zephyr RTOS で使ってみた
早速、FlatBuffers
を使ってみましょう。今回は Zephyr RTOSのプロジェクトに組み込む方法を説明します。 FlatBuffers
は通常はflatc
というスキーマコンパイラを使いますが、PureなC言語だけはFlatCC
を使います。仲間外れです。
必要なものは次の通り
- ホスト環境で動作するスキーマコンパイラ
FlatCC
FlatCC
リポジトリにあるruntime/*
をデバイス向けにビルドしてできるライブラリ- スキーマ(
*.fbs
)をコンパイルした時にできるヘッダーファイル
1. FlatCCのビルド
スキーマコンパイラ自体をビルドします。これはビルドするホスト環境でのビルドです。必要なツール類のインストールが終わっていれば公式Readmeの通りにすれば問題なくビルドできると思います。こちらではZephyr RTOSのビルド環境コンテナに組み込ん使用してみました。
ENV FLATCC_ARCHIVE_BASE_URL "https://github.com/dvidelabs/flatcc/archive/" ENV FLATCC_ARCHIVE_TAG "master" RUN cd /tmp && wget -O flatcc.tar.gz "${FLATCC_ARCHIVE_BASE_URL}/${FLATCC_ARCHIVE_TAG}.tar.gz" \ && tar xzf flatcc.tar.gz \ && mv flatcc-* flatcc \ && cd flatcc \ && ./scripts/initbuild.sh make \ && ./scripts/build.sh \ && cp ./bin/flatcc /usr/local/bin/ \ && rm -rf /tmp/* /var/tmp/*
2. ランタイムライブラリのビルド
Zephyrの外部ライブラリとしてビルドさせるため、flatccのディレクトリをZEPHYR_MODULES
で指定されている場所に展開し、次のようなファイルを作成しましょう。flatcもCMakeを使っているので相性はバッチリです。
flatcc/zephyr/CMakeLists.txt
flatcc/zephyr/Kconfig
flatcc/zephyr/module.yml
CMakeLists.txt
option(FLATCC_TEST "enable tests" OFF) option(FLATCC_PORTABLE "include extra headers for compilers that do not support certain C11 features" OFF) option(FLATCC_GNU_POSIX_MEMALIGN "use posix_memalign on gnu systems also when C11 is configured" ON) option(FLATCC_RTONLY "enable build of runtime library only" ON) option(FLATCC_INSTALL "enable build of runtime library only" OFF) option(FLATCC_COVERAGE "enable coverage" OFF) option(FLATCC_DEBUG_VERIFY "assert on verify failure in runtime lib" OFF) option(FLATCC_TRACE_VERIFY "assert on verify failure in runtime lib" OFF) option(FLATCC_REFLECTION "generation of binary flatbuffer schema files" OFF) option(FLATCC_NATIVE_OPTIM "use machine native optimizations like SSE 4.2" OFF) option(FLATCC_FAST_DOUBLE "faster but slightly incorrect floating point parser (json)" OFF) option(FLATCC_ALLOW_WERROR "allow -Werror to be configured" ON) option(FLATCC_IGNORE_CONST_COND "silence const condition warnings" OFF) if(CONFIG_ENABLE_FLATCC) zephyr_include_directories(../include) zephyr_sources( ../src/runtime/builder.c ../src/runtime/emitter.c ../src/runtime/refmap.c ../src/runtime/verifier.c ../src/runtime/json_parser.c ../src/runtime/json_printer.c ) endif()
Kconfig
config ENABLE_FLATCC bool "Enable the flatbuffer runtime lib" select REQUIRES_FULL_LIBC
LIBCは確かmath.h
のマクロ定義か何かのためだけに必要だったのでどうにかしたら外せるかも・・・
module.yml
build: cmake: zephyr/
3. スキーマコンパイル
次にスキーマコンパイルです。Zephyr RTOSプロジェクトの中のCMakeLists.txt
からinclude()
する形で作成しています。
set(file_gen "_builder.h" "_reader.h" ) list(TRANSFORM file_gen PREPEND flatbuffers_common) list(TRANSFORM file_gen PREPEND ${PROJECT_BINARY_DIR}/include/generated/) list(APPEND output_file_list ${file_gen}) add_custom_command( OUTPUT ${file_gen} COMMAND flatcc -a -o ${PROJECT_BINARY_DIR}/include/generated/ COMMENT "Generate ${flatbuffers_common}" ) list(TRANSFORM flatbuffer_include_dirs PREPEND "-I") foreach(fbs_name IN LISTS flatbuffer_schema_list) get_filename_component(output_file_name ${fbs_name} NAME_WE) set(file_gen "_builder.h" "_json_parser.h" "_json_printer.h" "_reader.h" "_verifier.h") list(TRANSFORM file_gen PREPEND ${output_file_name}) list(TRANSFORM file_gen PREPEND ${PROJECT_BINARY_DIR}/include/generated/) set(dep_file_name "${fbs_name}.d" ) set(dep_file ${PROJECT_BINARY_DIR}/include/generated/${output_file_name}) add_custom_command( OUTPUT ${file_gen} COMMAND flatcc -wvr --json -o ${PROJECT_BINARY_DIR}/include/generated/ ${flatbuffer_include_dirs} ${fbs_name} DEPENDS ${flatbuffer_schema_list} COMMENT "Generate from ${fbs_name}" ) list(APPEND output_file_list ${file_gen}) endforeach(fbs_name) add_custom_target( flatcc_target DEPENDS ${output_file_list} ) add_dependencies(app flatcc_target) list(APPEND app_module_include_dirs ${PROJECT_BINARY_DIR}/include/)
これで下のように flatbuffer_schema_list
という名前のリストにスキーマを追加していけばビルド時に./zephyr/include/generated/
の下にヘッダーファイルが生成されます。
list(APPEND flatbuffer_schema_list ${CMAKE_CURRENT_SOURCE_DIR}/monster.fbs )
あとは公式サイトのチュートリアルのC言語バージョンをみて勉強してみましょう〜
パフォーマンス測定
やりたかったのですが厳密な測定は時間が足りなかったので今回はパス。時間ができればやります。公式でのベンチマークはこちら。
ざっくり感想↓
- データサイズ
- シリアライズ速度
- デコードは速いのでコマンドパースとかには向いている
- エンコードも不揮発データ保存用とかには十分速い
- 内部のステートマシンになげるイベント生成にも使ってみたら流石に遅かった
所感
正直なところ、もう少し使い込まないと結論出せませんが十分使える感触を得ています。スキーマに日本語コメントでコンパイルNGなるのでそこはどうにかならないかと検討してます。
意外といいなと思ったのはスキーマで定義したenumとかは自動的に次のようなメンバーの名前を返す関数が定義されルためprintfデバッグが捗りました。スキーマ定義しておけばCの構造体→JSON化の機能もあるので文字列化のためだけでも使ってもいいかな〜
namespace Thumbturn; enum Direction : ubyte { cw = 0, ccw = 1, }
- 自動生成されたコード
static inline const char *Thumbturn_Direction_name(Thumbturn_Direction_enum_t value) { switch (value) { case Thumbturn_Direction_cw: return "cw"; case Thumbturn_Direction_ccw: return "ccw"; default: return ""; } } static inline int Thumbturn_Direction_is_known_value(Thumbturn_Direction_enum_t value) { switch (value) { case Thumbturn_Direction_cw: return 1; case Thumbturn_Direction_ccw: return 1; default: return 0; } }
おわり
FlatBuffers
の記事は色々ありますがPureなC言語から使うのはあまり見かけないので参考になったら幸いです!
・・・
株式会社フォトシンスでは、一緒にプロダクトを成長させる様々なレイヤのエンジニアを募集しています。 hrmos.co
Akerun Proの購入はこちらから akerun.com