組み込みでも使える?シリアライザ 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. スキーマを介して複数デバイス間でデータ構造を共有できる
  2. データの前方・後方互換性を(頑張れば)保てる

利点の1番目は明らかですね。Akerun Proは様々なデバイスと直接通信しています。また間接的ですがゲートウェイバイスを通じてクラウド上のサーバーとも通信しています。

FlatBuffersスキーマを作成し、一元的にこれらの多数のデバイス間通信データのデータ構造を自動生成できるとすれば、開発側に大きなメリットです。また、FlatBuffersは複数のプログラミング言語に対応しており、特定の言語に縛られることがないためDX的にプラスです。公式サイトによると FlatBuffersC++, 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を使います。仲間外れです。

f:id:photosynth-inc:20191213194908p:plain

必要なものは次の通り

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言語バージョンをみて勉強してみましょう〜

パフォーマンス測定

やりたかったのですが厳密な測定は時間が足りなかったので今回はパス。時間ができればやります。公式でのベンチマークはこちら

ざっくり感想↓

  • データサイズ
    • 当たり前ですがC言語の構造体のサイズと比べて+αされる
    • けどJSONとかと比べると十分小さい
    • 小さいデータ構造であれば(+前方互換性無視すれば)BLEの20バイトパケットでも意外と実用的
  • シリアライズ速度
    • デコードは速いのでコマンドパースとかには向いている
    • エンコードも不揮発データ保存用とかには十分速い
    • 内部のステートマシンになげるイベント生成にも使ってみたら流石に遅かった

所感

正直なところ、もう少し使い込まないと結論出せませんが十分使える感触を得ています。スキーマに日本語コメントでコンパイル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言語から使うのはあまり見かけないので参考になったら幸いです!

・・・

株式会社フォトシンスでは、一緒にプロダクトを成長させる様々なレイヤのエンジニアを募集しています。 recruit.jobcan.jp

Akerun Proの購入はこちらから akerun.com