先看測試結果:
環境與背景
Windows 7-64bit,不過我底下的 toolchain 還是都選 32bit 的。跟我實際要跑的專案有關,不想現在換了 64bit toolchain 然後多搞出一堆毛來。
使用的 Irrlicht 是我原本從官方 1.7 branch 出來的 https://github.com/arch-jslin/Irrlicht 再跟現行 Irrlicht SVN (pre-1.9) 的 r4560 ogl-es branch 整合。我的 Irrlicht 1.7 branch 主要是整合了 IrrlichtML 對 TrueType Font 的支援,以及我對 Irrlicht 材質類別的擴充(只針對 es1,es2 要寫 shader 我還沒做,可能也不會做吧 ...)、和一些小調整。因為我沒有打算長期維護這個雙重整合的版本,目前沒有 merge 到任何公開的 repository 中,只能說還能無痛手動 merge(意思就是說我手動把以前有 mark 起來的程式碼一段一段貼進 ogl-es branch 裡,然後幾乎沒有發生任何問題,IME 的部分除外,IME 實作的 compiler flag 我從來沒開過)算是奇蹟了。
FreeType 的部份就很單純,我是直接抓官方 2.4.12 zip 檔下來。
然後我一直忘記之前自己是只裝過 JRE 還是裝過 JDK,總之重裝了一次最新版 Oracle JDK。Android toolchain 的部分,我是裝好 Android SDK 後裝了 apache-ant。Android NDK 我使用 r8e。
環境的 path 大概就這樣:
C:\Android\SDK
C:\Android\NDK
C:\Android\apache-ant
...
反正這次會用上的就放一堆就對了,然後除了 ANDROID_HOME 要指向 SDK root 以外,我是把這些都加到 PATH 了:
path_to_NDK
path_to_apache_ant\bin
path_to_SDK
path_to_SDK\tools
path_to_SDK\platform-tools
然後重點,我有裝 msysgit,沒有裝 Cygwin,因為 Cygwin 很慢,然後很多時候其實都只是需要 Cygwin 裡面少少幾個 POSIX 指令的支援,譬如說 bash、make、basename、dirname 啥啥的,很多時候 msysgit 完全是夠用的。我也有裝 MinGW,是 TDM-GCC bundle 的,某些情況下 mingw32-make 是堪用的,不過蠻常遇到不堪用的情況。
但後面也會講到 msysgit 的 caveat。
測試 Android NDK + Irrlicht pre-1.9 ogl-es branch
單純要直接試 Irrlicht ogl-es branch 的話,直接看官方 r4528 的 commit log。
也可以參考一下 SO 上有人貼的常用指令:http://stackoverflow.com/questions/14848042/running-the-ndk-native-activity-sample-from-a-command-line-ide
Android command-line documentation
ADB documentation
NDK documentation
目前 build 測試的東西時常用順序是這樣:
ndk-build clean all [V=1] [NDEBUG=1] (V=1 是開啟 verbose)
ant debug
adb shell pm uninstall com.aaa.bbb (aaa.bbb 就看 manifest xml 裡怎麼設定)
adb install path_to_built_apk
android 指令主要是透過 command line 來增改 manifest xml ... 好像直接抄別人的比較快?
坑一
Irrlicht 必需和 FreeType 整合,畢竟我 TrueType font 支援是寫在 Irrlicht 裡的,所以當然先開始 build FreeType,而且因為目的地是 Android,所以得用 NDK 上的 cross-compiler toolchain: http://en.wikibooks.org/wiki/OpenGL_Programming/Installation/Android#FreeType
(我也有照 http://stackoverflow.com/questions/16976959/ndk-cross-compiler-freetype 一開始講的,先把之後所需的 env var 設一設,其中 PATH 是 make-standalone-toolchain.sh 執行成功後,cross-compiler toolchain binary 會放的位置)
疑?那個 make-standalone-toolchain.sh 跑起來爛掉啊?一度以為是 msysgit 的 bash 太爛,結果不是,是 expr 這個指令太爛 ... 花了數個小時才發現這件事實,也是某篇 SO 答案上的。一時找不到 link 了。但在我打死不裝 Cygwin 的前提下,惡名召彰的 MSYS (其實也就是 msysgit 裡已經有包部分功能了)可以單獨拆出一部分功能來用。所以我就去裝了含 expr 這指令的 MSYS coreutils,只解開 expr 來用。但 coreutils 都相依 msys-intl 跟 msys-iconv,雖然實際上 msysgit 裡也有 libiconv 與 libintl,不過功能與新舊版本實在差太多,不過還好名字也不一樣,所以都放一起也沒差。
然後 make-standalone-toolchain.sh 應該就會幫你把一個完整個 cross-compiling toolchain 擺到一個統一的位置了,包括 bin, lib, include 那些,不然它們原本在 NDK 資料夾裡面被各種不同版本路徑,與針對不同 architecture 的資料夾分開存放,根本很難用。--install-dir 參數記得設自己選的路徑,譬如說我放到 C:/Android/ndk-toolchain。(同理,下面的 FreeType ./configure --prefix 那個 prefix,最好選擇一個固定易找的擺放位置,我是放到 C:/Android/ndk-modules/freetype)
FreeType 2.4.12 不用先加那個 -std=gnu99 來 build。可是 configure 順利跑完了,make 瞬間就死了 ... 想當然爾是 mingw32-make 不夠力。一樣再去找了 MSYS-make 來用,MinGW 的 make 有特別改過名,然後 msysgit 裡本來沒有 make,所以就丟到 msysgit bin 裡面去。印象中 MSYS-make 也只有相依 msys-intl 跟 msys-iconv,應該不用再抓別的東西。
然後 make && make install,完工!FreeType 裝好放到 ndk-modules 目錄下了。
照 Wiki 上指示在 ndk-modules/freetype 目錄下加一個 Android.mk。那個 PREBUILT_STATIC_LIBRARY 留意一下,等等再提。
坑二
其實理論上這樣幾乎就等於已經大功告成,直接先講結論,到 Irrlicht ogles branch 的 Android 專案目碌,把 Android.mk 加上 freetype 即可,就如同上面 Wikibook 所示一般。(當然,把 freetype 放在了 C:/Android/ndk-modules/freetype 下面了,NDK_MODULE_PATH 記得要設定指向 C:/Android/ndk-modules)
...
LOCAL_STATIC_LIBRARIES := ... freetype
...
$(call import-module,freetype)
至此再回到前面講 Irrlicht 官方 r4528 那個 commit log 時的做法,就已經可以正常 build 出已包含 TrueType font 類別的 Irrlicht 了,完全沒有問題。
但是我昨天犯了一個天大的錯誤 ... 我單純想 LOCAL_STATIC_LIBRARIES := 後面可以空一格再加 library name,那下面 call import-module 大概也就是多一個逗號再加 library name 吧!
$(call import-module,blahblah,freetype)
然後就大杯具,明明 NDK_MODULE_PATH 設好了,這些 makefile directives 也都加了,怎麼會跟我講找不到 FreeType header!然後我就開始到處懷疑,是不是 -I 還是要自己加啦 ... STATIC_LIBRARIES 到底是啥意思啦 ... import-module 到底有沒有去找 NDK_MODULE_PATH 啦 ... PREBUILT_STATIC_LIBRARIES 是不是有鬼拉 ...
還試了一堆旁門左道辦法,譬如說把 freetype 整個搬到 Irrlicht 的 Android jni 底下,經過某些神秘的路徑調整(有一個主 Android.mk,加上每個子目錄都當成是一個 module etc)也確實 compile 會過。可是關鍵就是,這種情況下,我不會去寫到 錯誤的 call import-module ... 所以它才正確的找到所有 Android.mk。
當然,實際上,是要這樣寫的:
LOCAL_STATIC_LIBRARIES := blahblah freetype
...
$(call import-module,blahblah)
$(call import-module,freetype)
..................................
坑三
這才是真正的坑,前面都是我白癡而已。就在我順利地照前面講 Irrlicht 官方 r4528 那個 commit log 做法,可以成功 build 完 libIrrlicht.a,然後能正常去 compile/link 那個 hello mobile example,還是發生了兩個問題:
狀況一:要馬是 example build 到最後他會說 FreeType 相關 function 會 undefined reference.
狀況二:example 也 build 過,link irrlicht 成功,打包好了 apk,但是一在手機上裝好執行,就是 crash。
狀況一對我而言一開始是很奇怪的。因為我認為 Irrlicht build 好時,FreeType 不是早就被參考了嗎,怎麼會在 build final shared object 時跟我講 undefined reference?這是應該單純是我過去一直以來的錯誤認知,所以快速講答案:「static library 只是 object file archive,不會有任何 linking 發生。linking 只發生在 build shared or executable 時」
好,那意思就是說我大概是要在 build final shared object 那個 mk 檔裡加上 freetype 吧?加了,沒用,一樣 undefined reference。
這先擱著。
當時想到,誒,這個 example 他 link Irrlicht 的方式是一樣在 LDFLAGS 上直接加 -lIrrlicht 咧,不是用什麼 LOCAL_STATIC_LIBRARIES 跟 import-module。真有那麼簡單就好了。我加上 -lfreetype,還真的確實 link 過囉!裝好 apk,當掉。(狀況二)
最後我查到了這一篇:
http://stackoverflow.com/questions/13637450/android-ndk-how-to-link-multiple-3rd-party-libraries
看完,開始嗅到腐臭味了。
AFAIK, the way linking works for shared libraries is:
- grab all object files of B and C
- strip out object files that A doesn't reference
- resolve references in B and C
This gives link errors because B and C call each other, specifically they call functions that got stripped out in step 2 because A didn't call them.
所以關鍵有二:
1. 我的 Irrlicht 才有 ref 到 FreeType。
2. 我的 final shared object 沒有 ref 到 FreeType。
狀況一時很明顯就是因為雖然在 LOCAL_STATIC_LIBRARIES 上加了 freetype,可是因為沒有直接 ref 到,所以整個 FreeType 被 strip 幹掉了。狀況二時,用 -lfreetype 硬加雖然 build 過,可以打包成 apk,可是因不明原因,它還是被脫了。然後 apk 裝好當然執行就 crash,因為 FreeType 還是被 strip 掉了。(從實際安裝好後的 app 大小可以看出端倪,我把還未實裝 FreeType 的 Irrlicht ogles build 好安裝到手機,跟那個會當掉的版本來比,app 大小幾乎完全一樣。)
至於從哪邊可以猜出它還是被 strip 掉,我想用 ndk-build V=1 下去 build 可以看出問題:
Install : libHelloWorldMobile.so => libs/armeabi/libHelloWorldMobile.so
copy /b/y ".\obj\local\armeabi\libHelloWorldMobile.so" ".\libs\armeabi\libHelloWorldMobile.so" > NUL
C:/Android/android-ndk-r8e/toolchains/arm-linux-androideabi-4.6/prebuilt/windows/bin/arm-linux-androideabi-strip --strip-unneeded ./libs/armeabi/libHelloWorldMobile.so
Install 那行是 ndk-build 全部執行完才會出現的訊息,結果 strip 出現在 Install 之後 ... 那當然照前文提到的,我 FreeType symbol link 進去 final shared object 後,因為 final shared object 本身其實沒有參考到 FreeType,而是 Irrlicht 才需要這些 FreeType symbol,所以自然就被 strip 掉了。
前面那篇 SO 文中提到的五個可能解法:
- manually reference the missing functions from A, so that they don't get stripped
- figure out how to pass --whole-archive to the linker for the external static libraries
- use the PREBUILT_STATIC_LIBRARY (seen it mentioned, but never used it, and the according to the docs it doesn't sound applicable in this case)
- build an executable instead of a shared library (which won't strip code the same way)
- move/rename external libs to trick the NDK build system into thinking they're mine, so that I can add them to LOCAL_WHOLE_STATIC_LIBRARIES.
其實我也可以說 3. 一定是會失敗了,因為完全就是我這狀況。1. 這做法太鳥了,不爽用。2. 的話看不懂,4. 的 Android 上應該不可能?Android app 一定用 shared object/library 的方式來參考你用 NDK build 出來的東西。
所以後來就是把 freetype 改成加到 LOCAL_WHOLE_STATIC_LIBRARIES directive 去。成功。然後實際上在 app 安裝好後的大小才多了數百 KiB,不痛不癢。
有個做法應該是,把 Irrlicht build 成 shared library,可是這樣在 makefile 裡要改的東西好多,就先不管了。