eaglesakuraの技術ブログ

技術的な話題とか、メモとか。

Android NDKでprotocol buffersを利用する

Protocol Buffersとは

Protocol Buffersは2008年の7月7日=鑑純夏さんの誕生日にGoogleが公開したデータシリアライズ・デシリアライズライブラリです。なお、七夕は雨です。

IDLという独自のテキストフォーマットでデータを記述し、複数プラットフォーム間でデータのシリアライズとデシリアライズが行えます。

利用したいと思ったプラットフォームがAndroidiOSであり、Java/C++が公式に両対応され、かつ2008年公開でネット上に十分な情報が既に公式・非公式問わず大量に公開されているため利用しました。

JSONよりも優れている点

Javaだけで利用する場合はJSONXMLで十分でしたが、C++ではデコードや実際に利用するオブジェクトへのデータバインドがメンドウという欠点が有りました。

Protocol BuffersはIDLで記述したデータフォーマットからJava/C++のクラスを吐き出してくれるため、その点で便利です。

速度的に遅いという情報が多々見受けられますが、データサイズが100kb程度であれば速度やメモリ効率よりもデータの扱いやすさを重視したほうが開発側のコストが減るでしょう。2011年発売のMotorola XoomでもRAMのメモリ帯域は2.6GB/s程度あるようですので、その程度の(かつ読み込み時の一時的な)流量を心配するほど現代のモバイル端末のメモリは遅くないようです。

※Xoomのメモリ帯域はOpenGL Insights参照

protocのインストール

IDLからJava/C++のクラスを出力するには、protocコマンドが必要です。protocコマンドはGoogleから最新版のprotocol buffers一式を落としてきてビルドする必要があります。

cd Protocol Buffersのtarを解凍したディレクトリ
./configure
make
sudo make install

正常に完了すると、protocコマンドとincludeファイル一覧がMacにインストールされます。

データの用意

Protocol Buffersでデータをシリアライズ・デシリアライズするにはまずIDLでファイルを記述しなければなりません。

EclipseにはIDLを記述するためのプラグインが用意されているので、Marketで"Protocol Buffers"あたりで検索をかけるとインストール出来ます。

今回はTexture Packerのライセンスを貰うことが出来たので、JSONで出力されたテクスチャアトラスデータのIDLを記述しました。

IDLファイルの拡張子は"*.proto"を利用するのが通例のようです。

// Primitive.proto

package jc_res;

option java_package = "com.eaglesakura.resource.primitive";
// 四角形
message Rectangle {
    required int32 x = 1000;
    required int32 y = 2000;
    required int32 w = 3000;
    required int32 h = 4000;
}
// アトラス化された画像
message Size {
    required int32 w = 1000;
    required int32 h = 2000;
}

// Texture.proto
package jc_res_texture;

option java_package = "com.eaglesakura.resource.texture";

// テクスチャフォーマット
enum PixelFormat {
    RGBA8888 = 0; //
    RGB888 = 1; //
    RGB565 = 2; //
    RGBA4444 = 3; //
    RGB5551 = 4; //
}

// テクスチャの画像フォーマットを定義する
enum TextureFormat {
    PNG = 0;
    JPEG = 1;
    KTX = 2;
}
// TextureAtlasData.proto
package jc_res_atlas;

import "Primitive.proto";
import "Texture.proto";

option java_package = "com.eaglesakura.resource.texture.atlas";


// ATLAS化された各画像を示す
message AtlasTexture {
    // Origin Filename.
    // 拡張子は取り除かれる
    required string filename = 2000;
    // アトラスグループ内のテクスチャ位置
    required jc_res.Rectangle frame = 3000;
    // テクスチャが縦回転させられていたらtrue
    required bool rotated = 4000;
    // トリミング済みであればtrue
    required bool trimmed = 5000;
    //
    optional jc_res.Rectangle spriteSourceSize = 6000;
    // 元画像サイズ
    optional jc_res.Size sourceSize = 7000;
}

// アトラス化した画像の親グループ
message AtlasGroup {
    // 識別子
    required string uniqueId = 1000;
    // 画像配列
    repeated AtlasTexture images = 2000;
    // 画像ファイル名
    // 拡張子は取り除かれる
    required string filename = 3000;
    // 画像の拡大率
    optional float scale = 4000;
    // 画像読み込み時のピクセルフォーマット
    optional jc_res_texture.PixelFormat importPixelFormat = 5000;
}

IDLには外部ファイルをimport、コメント等の機能があるため、かなり簡単に記述できます。

Messageのデータには「必至/オプション」の設定や配列等の通常必要と思われる要素はひと通り使えます。

GradleでJava/C++用クラスを生成する

ここからが本番で、シリアライズ・デシリアライズ用のクラスを出力します。今回はオフラインツール(java)でデータを用意して、実機(NDK/C++)でデコードするという用途のためJavaC++の両方で生成を行わせます。

この頃Gradleにも興味が出てきたので、せっかくだからGradleにてビルドを行わせています。

// build.gradle

task buildIDL {
    doLast {
        // IDL出力先生成
        File cppOutDir = new File("./gen-cpp").getAbsoluteFile();
        cppOutDir.mkdirs();

        File javaOutDir = new File("./gen-java").getAbsoluteFile();
        javaOutDir.mkdirs();

        File srcDir = new File("./idl/").getAbsoluteFile();

        for(File proto : srcDir.listFiles()) {
            if(proto.getName().endsWith(".proto")) {
                // IDL File
                // build
                println("compile :: " + proto.getName());
                ant.exec(executable : 'protoc', dir : ".", output : "protoc.log") {
                    arg(value:String.format("--proto_path=%s", proto.parentFile.absolutePath));
                    arg(value:"--cpp_out");
                    arg(value:cppOutDir.absolutePath);
                    arg(value:"--java_out");
                    arg(value:javaOutDir.absolutePath);
                    arg(value:proto.absolutePath);
                }
            }
        }
    }
}

これで ./idl ディレクトリ配下の*.protoファイルを全てビルドし、gen-cppとgen-java配下にクラスを生成させています。

gradle buildIDL

Javaで利用する

JavaのプロジェクトでProtocol buffersで生成したクラスを利用するにはProtocol Buffersの開発者向けJarを生成しなければなりません。

Jarファイルを生成するにはMaven2が必要です(maven3では動きませんでした)。ただし、私の環境ではメモリが不足してビルドが失敗することがあったので、mvn2 install前にVMの使用メモリを変更しています。

ビルドには"..src/protoc"が必要(おそらくtestの実行に)ですので、最低でもprotocコマンドのmakeまでは完了させなければならないようです。

cd Protocol Buffersのtarを解凍したディレクトリ
cd java

# メモリ利用量を上げる
export MAVEN_OPTS=-Xmx2048M

# Maven2を利用してビルド
mvn2 install

実行すると、protobuf-java-2.5.0.jar(私の場合)が生成されるので、あとはそれをJavaのプロジェクトに加えれば利用が行えます。

arm向けの静的ライブラリを生成する

今回最も頭を悩ませたのが、NDK向けのprotocl buffersのビルドです。ソースレベルで配布されていますが、configure等を通さないとビルドが行えないため以前導入しようと思った時はここでコケてメンドウになって導入を見送っています。

今回はまじめにいろいろググりながら利用できるようにしました。

configureを通すためには環境変数にいろいろ準備をしないといけないようなので、次のようなスクリプトを書いて実行しています。Android 4.0以上で使えればよかったのでandroid-14で通していますが、パスを変更すれば多分他のバージョンでもいけます(未検証)。

#! /bin/sh
# ndk-configure.sh

PREBUILT=${ANDROID_NDK_HOME}/toolchains//arm-linux-androideabi-4.8
PLATFORM=${ANDROID_NDK_HOME}/platforms/android-14/arch-arm

export CC=${PREBUILT}/prebuilt/darwin-x86_64/bin/arm-linux-androideabi-gcc
export CFLAGS=-'fPIC -DANDROID -nostdlib'
export LDFLAGS="-Wl,-rpath-link=${PLATFORM}/usr/lib/ -L${PLATFORM}/usr/lib/"

export CPPFLAGS=-I${PLATFORM}/usr/include

export LIBS=-lc 
./configure --host=arm-eabi

ndk-configureを実行すると、Android NDK用のconfig.hファイルが生成されます。

次にprotocol buffersを解答したディレクトリをjniにリネームして、次のAndroid.mkとApplication.mkを配置します。無駄なものが含まれているかもしれませんが、おそらくリンク時に排除してくれるので多分問題ありません。

# Application.mk
APP_STL := gnustl_static
APP_ABI := armeabi-v7a armeabi
APP_MODULE := libprotobuf
# Copyright (C) 2009 The Android Open Source Project
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#      http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
#

LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)

COMPILER_SRC_FILES :=  \
src/google/protobuf/descriptor.cc \
src/google/protobuf/descriptor.pb.cc \
src/google/protobuf/descriptor_database.cc \
src/google/protobuf/dynamic_message.cc \
src/google/protobuf/extension_set.cc \
src/google/protobuf/extension_set_heavy.cc \
src/google/protobuf/generated_message_reflection.cc \
src/google/protobuf/generated_message_util.cc \
src/google/protobuf/message.cc \
src/google/protobuf/message_lite.cc \
src/google/protobuf/reflection_ops.cc \
src/google/protobuf/repeated_field.cc \
src/google/protobuf/service.cc \
src/google/protobuf/text_format.cc \
src/google/protobuf/unknown_field_set.cc \
src/google/protobuf/wire_format.cc \
src/google/protobuf/wire_format_lite.cc \
src/google/protobuf/io/coded_stream.cc \
src/google/protobuf/io/gzip_stream.cc \
src/google/protobuf/io/printer.cc \
src/google/protobuf/io/tokenizer.cc \
src/google/protobuf/io/zero_copy_stream.cc \
src/google/protobuf/io/zero_copy_stream_impl.cc \
src/google/protobuf/io/zero_copy_stream_impl_lite.cc \
src/google/protobuf/stubs/common.cc \
src/google/protobuf/stubs/once.cc \
src/google/protobuf/stubs/structurally_valid.cc \
src/google/protobuf/stubs/strutil.cc \
src/google/protobuf/stubs/stringprintf.cc \
src/google/protobuf/stubs/substitute.cc

# C++ full library
# =======================================================
#include $(CLEAR_VARS)

LOCAL_MODULE := libprotobuf
LOCAL_MODULE_TAGS := optional

LOCAL_CPP_EXTENSION := .cc

LOCAL_SRC_FILES := $(COMPILER_SRC_FILES) 

LOCAL_C_INCLUDES := $(LOCAL_PATH)/src

LOCAL_C_INCLUDES := \
$(LOCAL_PATH)/android \
bionic \
$(LOCAL_PATH)/src \
$(JNI_H_INCLUDE)

# LOCAL_SHARED_LIBRARIES := libz libcutils libutils
# LOCAL_LDLIBS := -lz

# stlport conflicts with the host stl library
#ifneq ($(TARGET_SIMULATOR),true)
#LOCAL_C_INCLUDES += external/stlport/stlport
#LOCAL_SHARED_LIBRARIES += libstlport
#endif

# Define the header files to be copied
#LOCAL_COPY_HEADERS := \
#    src/google/protobuf/stubs/once.h \
#    src/google/protobuf/stubs/common.h \
#    src/google/protobuf/io/coded_stream.h \
#    src/google/protobuf/generated_message_util.h \
#    src/google/protobuf/repeated_field.h \
#    src/google/protobuf/extension_set.h \
#    src/google/protobuf/wire_format_lite_inl.h
#
#LOCAL_COPY_HEADERS_TO := $(LOCAL_MODULE)
# RTTIを有効
LOCAL_CPPFLAGS   += -frtti
LOCAL_CFLAGS   += -frtti
# LOCAL_CFLAGS := -DGOOGLE_PROTOBUF_NO_RTTI

include $(BUILD_STATIC_LIBRARY)

私の場合はdynamic_cast利用のため実行時型情報が欲しかったので、-frttiを有効にしています。ビルドはNDKのためndk-buildコマンドを利用します。

ビルドするにはAndroidプロジェクトと同等の構成にしないとエラーで弾かれるので、protobuf-2.5.0(最新版をDLした場合)をjniにリネームし、適当なAndroidManifest.xmlを配置すればndk-buildが行えます。

ビルドを行うとobj/local/armeabiにlibprotobuf.aが生成されています。

アプリ用のAndroid.mkで利用する

事前に生成した静的ライブラリは最終的にはアプリ用の*.soにリンクしなければなりません。アプリ側のAndroid.mkで次のように記述し、リンクを行っています。

## Import Protocolbuffer
include $(CLEAR_VARS)
LOCAL_MODULE := protobuf
LOCAL_SRC_FILES := 相対パス/libprotobuf.a
include $(PREBUILT_STATIC_LIBRARY)

#################################################################################
include $(CLEAR_VARS)

## アプリのビルド設定...

## libprotobuf.aをリンクする
LOCAL_STATIC_LIBRARIES += protobuf

## *.soをビルドする
include $(BUILD_SHARED_LIBRARY)

アプリ内で*.pb形式のデシリアライズを行う

オフラインツールで出力したProtocol Buffers形式のファイル(*.pbファイル)はNDKだと次のように記述することでデシリアライズを行えます。

今回は"TextureAtlasData.proto"から生成したAtlasGroupクラスを利用しています。

newでクラスを生成して、生成されるAtlasGroup::MergeFromCodedStream()メソッドを呼び出すことでbyte配列からデシリアライズしています。

#include    "TextureAtlasData.pb.h"

中略...


void Hoge::onAppInitialize() {

    // deserialize
    {
        MFileMapper file = Platform::getFileSystem()->loadFile(Uri::fromAssets("atlas.pb"), NULL);
        jc_sp group(new jc_res_atlas::AtlasGroup());


        google::protobuf::io::CodedInputStream is( (uint8_t*)file->getHead(), (int)file->length() );
        
        jclogf("created image size(%d)", group->images_size());

        group->MergeFromCodedStream(&is);

        jclogf("loaded image size(%d)", group->images_size());
        jclogf("file name(%s)", group->filename().c_str());
    }
}

まとめ

前準備が非常にメンドウですが、一度ビルドしてしまえば後は*.protoを書いてクラス生成 -> includeするだけでJava/C++両対応のファイルが生成できるため、今後は非常にデータ形式の生成が楽になりそうです。

makefile等はgithubでも公開しています。 https://github.com/eaglesakura/jointcoding/tree/develop/apps/protocolbuffers

大量の情報を書き留めてくれていたネット上の先人たちに感謝します。

OpenGL Insights 日本語版 (54名のエンジニアが明かす最先端グラフィックス プログラミング)

OpenGL Insights 日本語版 (54名のエンジニアが明かす最先端グラフィックス プログラミング)

マブラヴ ツインパック figma 鑑純夏 1体 同梱 - Xbox360

マブラヴ ツインパック figma 鑑純夏 1体 同梱 - Xbox360