使用 Android NDK build Irrlicht + FreeType

1 篇文章 / 0 新
johnson_lin
johnson_lin's 的頭像
使用 Android NDK build Irrlicht + FreeType

先看測試結果:

環境與背景
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-intlmsys-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:

  1. grab all object files of B and C
  2. strip out object files that A doesn't reference
  3. 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 文中提到的五個可能解法:

  1. manually reference the missing functions from A, so that they don't get stripped
  2. figure out how to pass --whole-archive to the linker for the external static libraries
  3. 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)
  4. build an executable instead of a shared library (which won't strip code the same way)
  5. 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 裡要改的東西好多,就先不管了。