遥かへのスピードランナー

シリコンバレーでAndroidアプリの開発してます。コンピュータービジョン・3D・アルゴリズム界隈にもたまに出現します。

iPhone・iPad用に手書き文字認識ライブラリZinniaをビルドする

iPhoneiPad向けに手書き文字認識ライブラリ-Zinniaをビルドしてみて、結構苦労したので、そのときのメモです。Zinniaと書いてますが、Fat binary全般についての僕の理解も込みで説明してます。以下iPhoneと書いてますが、iPad用でも同様の内容があてはまります。

Fat binaryについて

まず基本的な話で、iPhone実機、Mac OSが稼働するPC、そしてiPhoneシミュレータの三者は、動作するプログラムのアーキテクチャが違う、ということを頭に置いておく必要があります。iPhoneのCPUはarm、Mac OSが稼働するCPUは(OS10.6では)x86_64、そしてiPhoneシミュレータはi386のCPUをシミュレートするようになっています。Mac OS上で普通にコンパイルするとx86_64向けのバイナリが出来てしまうので、これはiPhone実機やシミュレータでは動かないわけです。

作成されたバイナリがどのアーキテクチャ向けにビルドされているかは、otoolコマンドなどで参照することが出来て、たとえば普通にconfigureしてmakeしたZinniaのバイナリは以下のようになっています。

$ otool -vh .libs/libzinnia.a
Archive : .libs/libzinnia.a
.libs/libzinnia.a(param.o):
Mach header
      magic cputype cpusubtype  caps    filetype ncmds sizeofcmds      flags
MH_MAGIC_64  X86_64        ALL  0x00      OBJECT     3       1136 SUBSECTIONS_VIA_SYMBOLS
...

こんな感じでこのライブラリにリンクされているバイナリがx86_64をターゲットにコンパイルされていることが分かります。
で、こいつをiPhoneで動かしたい、となるとシミュレータのことも考慮してarmとi386の両方で動くようにビルドしてあげる必要があるわけです。こんな風に複数CPUで動くように作られたバイナリをFat binaryとかUniversal binaryとか呼んだりします。

さて、Mac OS上でFat binaryを作るにはここに書いてあるようにいくつか方法がありますが、一番シンプルなのが、それぞれのアーキテクチャ向けに個々にコンパイルしたものを、lipoコマンドで結合してあげるやり方です。

基本的にはg++に-arch [アーキテクチャ名]のオプションが渡るようにしてあげれば、そのアーキテクチャ向けにコンパイルしてくれるはずなのですが、これがなかなか一筋縄ではいきません

armバイナリのビルド

armでビルドするときは、arm向けのライブラリやヘッダファイルなどが、Mac OSのデフォルトパス上のものとは異なるので、それらのライブラリパスやインクルードパスも全部指定し直す必要があります。arm用のファイルは、iOS SDKをインストールしたフォルダ /Developer/Platforms/iPhoneOS.platform/Developer に格納されているので、configure時にこれらを参照するように〜FLAGSなどの環境変数を設定してあげます。

export CPPFLAGS="-I$SDKROOT/usr/lib/gcc/arm-apple-darwin10/4.2.1/include/ -I$SDKROOT/usr/include/ -I$SDKROOT/usr/include/c++/4.2.1 -I$SDKROOT/usr/include/c++/4.
2.1/armv6-apple-darwin10/ -miphoneos-version-min=3.0"
export CFLAGS="$CPPFLAGS -pipe -no-cpp-precomp -isysroot $SDKROOT"
export CPP="$DEVROOT/usr/bin/cpp $CPPFLAGS"
export CXXFLAGS="$CFLAGS"

# Dynamic library location generated by the Unix package
LIBPATH=$LIBFILE.dylib
LIBNAME=`basename $LIBPATH`

export LDFLAGS="-L$SDKROOT/usr/lib -L$SDKROOT/usr/lib/gcc/arm-apple-darwin10/4.2.1/ -Wl,-dylib_install_name,@executable_path/$LIBNAME"

./configure CXX=$DEVROOT/usr/bin/arm-apple-darwin10-g++-4.2.1 CC=$DEVROOT/usr/bin/arm-apple-darwin10-gcc-4.2.1 LD=$DEVROOT/usr/bin/ld --host=arm-apple-darwin

make

これでarm用のバイナリが作られるはずです。

i386バイナリのビルド

Mac OSのライブラリ類はi386互換で作られているはずなので、armのようにインクルードパスやライブラリパスをいちいち指定し直さなくても、g++に-arch i386オプションを指定してあげれば、i386用のバイナリが作られます。

ただ一つ問題なのがZinniaのMakefileです。./configure時にCFLAGS="-arch i386"と指定してもMakefileが作られるときには、なぜかこの指定が消えてしまっているのです。恐らくはZinniaがビルドに使用しているlibtoolの問題ではないかと思うのですが(推測です)、仕方がないので無理矢理Makefileを書き換えてあげます。

./configure

perl -pi -e 's@^CXXFLAGS = @CXXFLAGS = -arch i386 @g' Makefile
perl -pi -e 's@^CFLAGS = @CFLAGS = -arch i386 @g' Makefile

make

これでi386のバイナリもビルドできました。

まとめ

以上をまとめたZinniaのFat binaryビルドファイルです。
Mac OS 10.6.4、iOS SDK 4.0.2、Zinnia 0.0.6で動作確認しています。

build_fat.sh
#!/bin/sh

LIBFILE=.libs/libzinnia

export DEVROOT=/Developer/Platforms/iPhoneOS.platform/Developer
export SDKROOT=$DEVROOT/SDKs/iPhoneOS4.0.sdk

# build arm binary
export CPPFLAGS="-I$SDKROOT/usr/lib/gcc/arm-apple-darwin10/4.2.1/include/ -I$SDKROOT/usr/include/ -I$SDKROOT/usr/include/c++/4.2.1 -I$SDKROOT/usr/include/c++/4.2.1/armv6-apple-darwin10/ -miphoneos-version-min=3.0"
export CFLAGS="$CPPFLAGS -pipe -no-cpp-precomp -isysroot $SDKROOT"
export CPP="$DEVROOT/usr/bin/cpp $CPPFLAGS"
export CXXFLAGS="$CFLAGS"

LIBPATH=$LIBFILE.dylib
LIBNAME=`basename $LIBPATH`

export LDFLAGS="-L$SDKROOT/usr/lib -L$SDKROOT/usr/lib/gcc/arm-apple-darwin10/4.2.1/ -Wl,-dylib_install_name,@executable_path/$LIBNAME"

LIBPATH_static=$LIBFILE.a
LIBNAME_static=`basename $LIBPATH_static`

./configure CXX=$DEVROOT/usr/bin/arm-apple-darwin10-g++-4.2.1 CC=$DEVROOT/usr/bin/arm-apple-darwin10-gcc-4.2.1 LD=$DEVROOT/usr/bin/ld --host=arm-apple-darwin

make

mkdir -p lnsout
cp $LIBPATH_static lnsout/$LIBNAME_static.arm

make distclean

unset CPPFLAGS CFLAGS CPP LDFLAGS CXXFLAGS DEVROOT SDKROOT

# build i386 binary
./configure

perl -pi -e 's@^CXXFLAGS = @CXXFLAGS = -arch i386 @g' Makefile
perl -pi -e 's@^CFLAGS = @CFLAGS = -arch i386 @g' Makefile

make

cp $LIBPATH_static lnsout/$LIBNAME_static.i386

/usr/bin/lipo -arch arm lnsout/$LIBNAME_static.arm -arch i386 lnsout/$LIBNAME_static.i386 -create -output lnsout/$LIBNAME_static

unset CPPFLAGS CFLAGS CPP LDFLAGS CPP CXXFLAGS DEVROOT SDKROOT
結果

fileコマンドで本当にfat binaryができているか確認してみます。

$ file lnsout/libzinnia.a
lnsout/libzinnia.a: Mach-O universal binary with 2 architectures
lnsout/libzinnia.a (for architecture arm):	current ar archive random library
lnsout/libzinnia.a (for architecture i386):	current ar archive random library

ちゃんとFat binaryが出来てますね、これでiPhoneでもiPadで手書き認識し放題です!
Enjoy!