17
17

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

FlatBuffersをJavaで使ってみる

Last updated at Posted at 2014-09-16

ProtocolBuffersの後継ライブラリ、FlatBuffersをJavaで使ってみる。

前提

  • 2014/9/17時点のFlatBuffers。
  • 開発環境はMac。
  • サンプルコードの中でguava-librariesのListsとIntsを使用。

FlatBuffersとは

FlatBuffersはGoogleが2014/6から公開しているシリアライズライブラリ。

ゲーム開発向けに作られていて、

  • データアクセスに必要なメモリはバッファのメモリのみで省メモリ
  • パース不要なのでオーバーヘッドが少なく高速
  • 定義ファイルから生成するため型安全
  • クロスプラットフォーム

などが特徴。

FlatBuffers自体まだ開発途中だが、特にJava対応はまだまだ未完成。
enumで定義したものが普通の定数で作成されたり、Json形式が未対応だったりする。

詳しくはこちら

準備

FlatBuffersはまだパッケージが用意されていないので、自分でコンパイルする必要がある。

MacはXcodeを使用する。WindowsはVisualStudioでコンパイルできるはず。

  • 任意のディレクトリにGitHubからclone。
git clone https://github.com/google/flatbuffers.git
  • Xcodeでプロジェクトを開く。
open flatbuffers/build/Xcode/FlatBuffers.xcodeproj
  • メニューのProduct-Scheme-flatcを選択。

screenshot1a.png

  • メニューのProduct-Buildをクリックしてコンパイル。

screenshot2a.png

  • flatbuffers/flatcが作られるので、任意のパスが通ってる所にリンクを作る。
ln -s flatbuffers/flatc /usr/local/bin/flatc
chmod 755 /usr/local/bin/flatc

クラスファイル生成

まず定義ファイルを作る。
よくあるユーザ情報と、そのリストを定義。

user.fbs
namespace entity;

table FUser {
  name: string;
  lv: short;
  lastLogin: long;
  message: string;
}

table FUsers {
  user: [FUser];
}

この定義ファイルを使ってflatcを実行する。

flatc -j -o src/main/java user.fbs

実行するとsrc/main/java/entity以下にFUser.javaとFUsers.javaが生成される。

生成したクラスはFlatBuffersのクラスに依存しているが、現在jarが用意されていないので、FlatBuffersのプロジェクトからコピーしてくる。

ファイル構成は以下のようになる。

project
 |
 ---- user.fbs (定義ファイル)
 |
 ---- src/main/java
     |
     ---- User.java (サンプルコードで使用するDTO)
     |
     ---- entity (flatcで生成したクラスファイル)
     |    |
     |    ---- FUser.java
     |    |
     |    ---- FUsers.java
     |
     ---- com.google.flatbuffers (FlatBuffersからコピーしてきたクラスファイル)
         |
         ---- Constants.java
         |
         ---- FlatBufferBuilder.java
         |
         ---- Struct.java
         |
         ---- Table.java

シリアライズ

シリアライズはFlatBufferBuilderとflatcで生成したクラスを使用して行う。

FUser

FUser
User user = new User("hoge", (short)1, System.currentTimeMillis(), "hello");

FlatBufferBuilder fbb = new FlatBufferBuilder(0);

// StringはあらかじめFlatBufferBuilderに入れてoffsetを取得しておく
int nameOffset = fbb.createString(user.name());
int messageOffset = fbb.createString(user.message());

// FUser開始
FUser.startFUser(fbb);
// 文字列はoffset、その他は値をセット
FUser.addName(fbb, nameOffset);
FUser.addLv(fbb, user.lv());
FUser.addLastLogin(fbb, user.lastLogin());
FUser.addMessage(fbb, messageOffset);

// FUser終了
int rootTable = FUser.endFUser(fbb);
// rootのoffsetを渡してFlatBufferBuilderを終了
fbb.finish(rootTable);

// 開始位置を0に修正したバイナリを取得
byte[] binary = fbb.sizedByteArray();

パッキングせずにバイナリを組み立てているだけなので省メモリかつ高速。

バイナリ取得はdataBufferではなくsizedByteArrayを使用する。
前者だと、デシリアライズ時にByteBufferのpositionの値が必要になってくるので使いにくい。

FUsers

FUsers
List<User> users = Lists.newArrayList();
users.add(new User("hoge", (short)1, System.currentTimeMillis(), "hello"));
users.add(new User("fuga", (short)5, System.currentTimeMillis(), "hi"));

FlatBufferBuilder fbb = new FlatBufferBuilder(0);

// 各FUserのoffsetを保持するリストを用意
List<Integer> userOffsets = Lists.newArrayList();

for (User user : users) {
    // StringはあらかじめFlatBufferBuilderに入れてoffsetを取得しておく
    int nameOffset = fbb.createString(user.name());
    int messageOffset = fbb.createString(user.message());

    // FUser開始
    FUser.startFUser(fbb);
    // 文字列はoffset、その他は値をセット
    FUser.addName(fbb, nameOffset);
    FUser.addLv(fbb, user.lv());
    FUser.addLastLogin(fbb, user.lastLogin());
    FUser.addMessage(fbb, messageOffset);
    // FUser終了、rootのoffsetをリストに追加
    userOffsets.add(FUser.endFUser(fbb));
}
// offsetのリストを反転する
userOffsets = Lists.reverse(userOffsets);

// UserVector開始
FUsers.startUserVector(fbb, userOffsets.size());
for (int userOffset : userOffsets) {
    // FUserのoffsetをセット
    fbb.addOffset(userOffset);
}
// UserVector終了
int usersOffset = fbb.endVector();

// FUsers開始
FUsers.startFUsers(fbb);
// UserVectorのoffsetをセット
FUsers.addUser(fbb, usersOffset);
// FUsers終了
int rootTable = FUsers.endFUsers(fbb);
// rootのoffsetを渡してFlatBufferBuilderを終了
fbb.finish(rootTable);

// 開始位置を0に修正したバイナリを取得
byte[] binary = fbb.sizedByteArray();

リストのシリアライズはかなり複雑になる。

まず、FlatBuffersBuilderはstartとendは1つずつ順に行う必要がある。
何かをstartしている間に別のものをstartしてしまうと、生成されるバイナリがおかしくなってしまうようだ。

startFUsersをしてからstartUserVectorし、startFUserをして構築したバイナリは、デシリアライズできなかった。

また、FUserのoffsetを入れたリストを反転することも重要。
反転しないと、デシリアライズ時にリストの順番が逆になってしまう。

※startUserVectorではなくcreateUserVectorを使用すると自分で反転しなくて済む。

FUsers
int usersOffset = FUsers.createUserVector(fbb, Ints.toArray(userOffsets));

デシリアライズ

シリアライズは複雑だが、デシリアライズは簡単。
flatcで生成したクラスだけ使用する。

FUser

FUser
ByteBuffer bb = ByteBuffer.wrap(binary);
FUser user = FUser.getRootAsFUser(bb);

String name = user.name();
short lv = user.lv();
long lastLogin = user.lastLogin();
String message = user.message();

シリアライズしたバイナリをByteBufferに入れ、FUserに渡すだけでOK。
アンパッキングしていないので省メモリで高速。

シリアライズ時にdataBufferを使用してバイナリを取得した場合、ByteBufferのpositionにシリアライズ時の値をセットする必要がある。

FUsers

FUsers
ByteBuffer bb = ByteBuffer.wrap(binary);
FUsers f = FUsers.getRootAsFUser(bb);

int length = f.userLength();
for (int i = 0; i < length; i++) {
    FUser user = f.user(i);

    String name = user.name();
    short lv = user.lv();
    long lastLogin = user.lastLogin();
    String message = user.message();
}

リストのデシリアライズも、シリアライズしたバイナリをByteBufferに入れ、FUsersに渡すだけでOK。

シリアライズ時にFUserのoffsetを入れたリストを反転していないと、最後に入れたFUserから取得することになる。

また、Iteratorの取得はできないので、拡張for文は使えない。

感想

  • 現状ではjarがなかったり機能が少なすぎたり、色々と不足している。
  • シリアライズが特殊で複雑。慣れるまで大変。
  • DTOでラップしたり、変換Utilクラスを作れば利用しやすい。
  • 省メモリで高速なのは魅力的。JacksonやMessagePackより速い。
17
17
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
17
17

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?