標籤:
大的問題
先記錄一下跨平台時需要注意的大方向.
1.OS和CPU
同一個作業系統, CPU也可能是不一樣的, 比如windows也有基於arm CPU的版本,而android目前有x86,arm,mips幾種.
即便是同一種CPU架構系列, 細節特性也不一樣.
所以目前個人準備了3個宏開關來判斷目標平台. OS, CPU, CPUbits
OS對應不同的作業系統, CPU對應不同的CPU架構(比如x86和arm), CPU-bits目前是32和64, 比如CPU是x86時, CPUbits是64,則對應x86_64(x64)平台.同樣arm系列也有32位和64位的區別.
32/64位的處理之前已經轉載過一篇文章專門說明,這裡不多說了.
通過這三個宏可以確定最終目標平台.但是為了減少維護代價, 這幾個宏一般情況下只在底層封裝使用, 在上層還是少用為好, 能不用盡量不用, 盡量使用標準C/C++.
SIMD的話, Intel Atom支援SSE, 而ARM有NEON指令集, 具體可以參考ndk的sample.
2.程式資料.
一般來說, 需要根據每一種目標平台, 各產生一份對應的資料.
這樣做的好處是每個平台的資料可以根據目標特性做調整.比如移動平台的情境規模/品質和資料量一般都相對要小.
由於每種CPU的資料處理特性(cache line, alignment, SIMD指令集)可能都不一樣,所以需要做特別的處理.
對於資料的格式,有兩種做法, 第一是每個平台的資料包格式不一樣, 第二是種是使用統一的格式, 也就是說儘管每個目標平台都有不同的一個資料包(因目標規模而定), 但理論上某個平台可以正常載入其他所有平台的資料包.
而第一種則不同, 各個平台的資料格式不一樣, 比如一個struct的成員對齊不同, 那麼序列化後的資料會有大小和位移量的差別.
第一種方式不用特別考慮資料對齊等等問題, 但是要產生和維護不同目標平台下的資料包和工具, 雖然維護成本相對較低,但是如果工具很多的時候也很繁瑣.
第二種方式需要在代碼中考慮對齊等等因素, 保證一個資料包可以被所有的平台載入, 這麼做可能也很繁瑣,需要特別小心,並經過完整的資料測試.
這裡有一個android下x86和arm的結構對齊的處理 (4.1 Forced Memory Alignment):
http://software.intel.com/en-us/articles/android-application-development-and-optimization-on-the-intel-atom-platform
參考http://software.intel.com/en-us/blogs/2011/08/18/understanding-x86-vs-arm-memory-alignment-on-android/
比如上面第一個連結中使用的"-malign-double" GCC參數, 強制8位元組的資料對齊的8位元組邊界.保證同一份資料可以被不同的CPU正常載入. 第二個連結使用的是__attribute__((aligned(n))), 來強制資料成員的對齊.
我所在的公司使用的是第一種方式. 第一種方式的問題在於同一個OS也有不同的target CPU(platform), 這樣會出現對於android平台, x86有一份資料, 而arm有另外一份資料. 所以個人傾向於第二種方式, 除了上文連結中的方式以外, 也可以強制不讀寫struct, 唯讀寫原子資料, 把一個struct的成員逐個單獨讀寫.
--附:
考慮到endian的問題, 一般資料寫入會使用固定的endian, 在讀取的時候根據目標特性做byte swap. 而封裝出原子資料的讀寫以後這一點應該也不難做到.
3.Platform API和庫
不同的OS的Platform API不盡相同, 不過幸運的是, 大部分OS都支援posix API介面規範, 所以一般來說,如果標準C/C++不支援的功能, 能使用posix介面的盡量使用. 個人遇到的最多的是IO, 線程和計時器(timer)這些.
雖然windows也支援posix子集, 但是一部分函數沒有, 這個時候要用OS的宏來做條件分支, 使用Windows API.
比如windows下的findfirst遍曆檔案, 在android/posix下面要改用opendir.
但是為了便於移植和維護,要注意一個原則,盡量不要將platform header暴露出來: public header中不要引用系統標頭檔, 這是一種強制檢錯機制. 因為一旦暴露出來的話,上層代碼很容易誤用.比如遊戲代碼裡面使用了HANDLE,在win32下編譯沒問題,所以沒有發現錯誤,換到其他平台會出錯.如果一定要用的話,用宏分開平台相關的資料.
對於一些跨平台功能, 如果不想自己寫的話, 也有很多現成的跨平台三方庫可以用, 它們已經將常用平台封裝好了,跳過繁瑣的Native API條件編譯, 比如UI系統可以使用Wx/Qt庫等.
線程的話, 大部分OS都支援pthread或者其子集, 由於筆者使用的是Intel TBB, 而最新的TBB4.x已經支援android平台, 前不久把TBB3.0更新到最新版本, 並且編譯可用. 這裡不多說了.
4.編譯器
編譯器首選GCC, 交叉編譯的神器啊. 但由於曆史原因(筆者一開始使用的是MSVC,工程從VC8一直到VC11,目前保留了VC10和VC11), 這裡也介紹下MSVC, 並簡說一下兩者.
對於C++11, GCC支援的更好. 對於C, MSVC基本停止在C89. 如果要考慮儘可能多的編譯器支援的話, 盡量使用C++03和C89.
對於Clang, 沒有使用過, 暫不討論.
而不同的編譯器有不同的擴充, 而且很多擴充是等價的. 如果擴充格式類似, 那麼可以寫在基礎標頭檔裡面.比如Ogre的alignment,我也拿過來用了:
| 1234567891011 |
#if defined(_MSC_VER)# define BLADE_COMPILER BLADE_COMPILER_MSVC# define BLADE_ALIGNED(n) __declspec(align(n))# define BLADE_FUNCTION __FUNCTION__#elif defined(__GNUC__)# define BLADE_ALIGNED(n) __attribute__((aligned(n)))# define BLADE_COMPILER BLADE_COMPILER_GNUC# define BLADE_FUNCTION __PRETTY_FUNCTION__#else# error "Compiler not supported yet."#endif
|
這樣對於一個需要對齊的結構, 可以使用 struct BLADE_ALIGNED(8) YOURSTRUCT {}; 來指定對齊, calling convension 也類似, 不過我這裡只有定義,沒有實際用到.
對于格式不一樣的擴充(某些compiler intrinsics), 可以在最後的具體使用時根據編譯器開關來條件編譯, 也可以將準系統封裝成基礎類, 在實現裡同樣使用條件編譯, 這樣上層的使用代碼比較統一.
順便說下我所在公司的tool chain, 他有自己的make系統, 比如XMAKE, 有自己的編譯器前端collection, 比如XCC, 使用非標準的C語言.
通過一個XC Compiler, 把自訂文法和擴充的的C語言轉換成本地編譯器(VC/GCC)支援的原始碼, 然後再使用本地編譯器編譯.
這樣做的優點是跨平台/編譯器的東西可以全部放在自己的compiler extension裡面, 上層代碼很乾淨, 只要按文法來寫, 那麼寫出來的代碼就是跨平台(除了Platform API 需要單獨封裝)的, 基本不用考慮編譯器和CPU相關的的東西, 而且這是強制性的, 很少出現差錯.
缺點就是學習成本, 一個標準C/C++的程式員需要一定時間適應他的文法.
當然他基本不支援C++(除了類/動態綁定以外, 模板和STL都不支援), 所以有點噁心, 但是可以直接調用本地編譯器,編譯標準C++代碼, 並且與其大部分ABI相容, 可以連結. 所以可以編譯如MFC這些代碼並且連結, 但是有一些限制.
這裡面也有曆史的原因, 因為這套工具很早就有了, 那個時候可能第一個C標準還沒有出來.所以才會費很大力搞這套東西, 不過到現在已經很穩定了. 而且他很早支援的某些擴充, 目前的C++11才剛剛有. 總的來說是一個不錯的東西, 但是如果是換到現在的話,有成熟的編譯工具鏈和語言標準, 估計沒有人會再去這麼搞了.
細節
然後記錄下移植過程中遇到的細節問題, 主要是MSVC到GCC過程中遇到的問題.
問題主要在於MSVC對標準支援不好, 允許很多標準不允許的方言.
記錄下這些細節, 以後養成好的編程習慣.
還有一部分android OS API 和bionic libc的問題.
1.模板
嵌套模板時, 標準不允許<<>>這種方式, 因為它與operator>>衝突.VC卻沒有這個問題..所以正確的方法是要用空格 隔開, 這個很早就做了, 只有部分沒有注意的這次改過來了.
| 12 |
typedef std::vector<std::vector<int>> intdvector; //errortypedef std::vector< std::vector<int> > intdvector; |
同樣在cast的時候,最好也用空格將<>隔開, 避免編譯錯誤,
| 12 |
::RGBQUAD& quad = reinterpret_cast<::RGBQUAD&>( q ); //error::RGBQUAD& quad = reinterpret_cast< ::RGBQUAD& >( q ); |
2.組合關鍵字的類型轉換
如果是單個關鍵字則都是合法的.但MSVC允許
| 1 |
unsigned short s = unsigned short(1.0f); |
這樣的轉換, 但是GCC比如要把組合關鍵字用括弧擴起來 (至於哪個是標準, 沒有去查, 但是猜想應該是GCC的)
| 1 |
unsigned short s = (unsigned short)(1.0f); |
3.mutable reference
| 12345 |
struct A {...};struct B{ mutable A& mARef; //error}; |
GCC 不允許使用mutable reference, 因為reference可以在const成員函數裡面修改.
這個也沒有去查標準, 但是應該心照不宣了.
4.純虛析構
MSVC允許將純虛析構(純虛函數的實現)放在類內,但是標準不允許.這個很久前查過標準, 而且修改過代碼,不過後來有少量代碼由於習慣的原因又這麼寫了.
| 123 |
virtual ~Class() = 0 {} // OK on MSVC, error on GCCvirtual ~Class() = 0; //OKvirtual ~Class() {} //OK |
5.Function scope functor
| 12345678 |
iterator findSomeThing(){ struct FnFinder { bool operator(const T& val) { ... } }; return std::find_if( v.begin(), v.end(), FnFinder() );} |
這個在GCC下編譯錯誤, 需要把struct FnFinder定義在函數體外面.
6.動態庫的模板匯出
前面的都是小改動, 但這是個最頭疼的問題.
MSVC下的DLL匯出模板很簡單:
DLL_EXPORT_API 是根據當前編譯的代碼, 是本地DLL代碼,還是引用該DLL的客戶代碼所做的__declspec( dllexport )/__declspec( dllimport )
本地代碼就是編譯DLL時的代碼, 客戶代碼指使用DLL(對應的標頭檔)並連結該DLL的模組代碼.
| 12345 |
# ifdef DLL_LOCAL_COMPILE# define DLL_EXPORT_API __declspec( dllexport )# else# define DLL_EXPORT_API __declspec( dllimport )# endif |
然後在公用標頭檔裡面聲明該匯出模板:
| 1 |
template class DLL_EXPORT_API Handle<Class>; |
只要本地DLL內部任何一個編譯單元包含了上面語句(可以重複包含), 就可以在該dll裡面產生一份唯一的類代碼.
但是GCC下面的機制有所不同: 由於沒有特別的匯出關鍵字, 只是通過visibility來控制可見度:(要設定為預設時所有符號都不可見)
| 12 |
#DLL_EXPORT_API __attribute__ ((visibility("default")))template class DLL_EXPORT_API Handle<Class>; |
這句顯式執行個體化語句, 會在客戶代碼的每一個編譯單元裡產生一份執行個體化代碼.而不像MSVC那樣根據__declspec( dllimport )產生對外部連結的引用.
如果是純程式碼的類, 最多就是產生冗餘的代碼, 目標檔案過大. 這或許勉強可以忽略.
但問題在於class 內部的static變數或者成員函數內部的static變數也同樣存在多個, 導致singleton等等某些類的單例指標存在多份執行個體, 從而導致libA.so 的singleton執行個體與libB.so的singleton執行個體不是同一個執行個體.
GCC下的模板支援聲明外部模板.
| 1 |
extern template class Handle<Class>; |
這樣在客戶代碼使用該聲明的時候, 就可以阻止模板執行個體化, 使他具有對外部連結(.so匯出符號)的引用.
而真正的顯示執行個體化要放在本地.so檔案的一個編譯單元裡, 而且不能重複聲明(重複以後導致多次執行個體化, 產生重複符號, 連結錯誤).
這樣最終的版本(GCC & MSVC都可行)如下:
| 12345678910 |
# if BLADE_COMPILER == BLADE_COMPILER_MSVC# ifdef XXX_DLL_LOCAL_COMPILE# define DLL_EXPORT_API __declspec( dllexport )# else# define DLL_EXPORT_API __declspec( dllimport )# endif# elif BLADE_COMPILER == BLADE_COMPILER_GNUC# define DLL_EXPORT_API __attribute__ ((visibility("default")))# endif extern template class DLL_EXPORT_API Handle<Class>; |
雖然MSVC警告說__declspec( dllexport )和extern不相容(因為dllexport是要求本地產生執行個體化代碼作為匯出符號, 與extern的意義衝突)但是可以關閉這個警告.
同時要在本地庫的某編譯單元裡面加上唯一的一句:
| 1 |
template class Handle<Class>; |
來完成GCC的本地模板執行個體化, 而對於MSVC來說, 有沒有這一句都一樣, 因為上面的 extern template class DLL_EXPORT_API Handle<Class>; 已經完成了匯出模板的執行個體化(extern被忽略), 只要它被本地的任何一個或者多個編譯單元包含就可以.
最終的移植工作量就是, 給所有的匯出模板聲明加上 extern 關鍵字, 並在本地.so工程的某編譯單元添加一個對應的顯示執行個體化聲明.目前感覺這樣做的改動是最小的.
--附
另外需要注意的是, 如果一個模板類A, 整合自另一個模板類B, 那麼extern template class 只對本類有效, 基類沒有extern屬性.幸好我這裡這種情況不多, 類只有一個, 但是需要在每個extern顯式執行個體化時加上基類, 由於有點繁瑣, 根據目前情況,直接在子類overwrite掉父類的函數, 在子類的匯出函數裡調用父類函數, 避免直接調用父類函數導致父類在客戶代碼被執行個體化, 這樣也可以解決.
7.Bionic libc下的libstdc++不支援寬字元
據說之前的版本sizoeof(wchar_t)還是1, 後來支援了,是4, 但是目前的版本(ndk r9)寬字元的庫函數仍然沒有.
比如vswscanf等等, 這個也需要自己黑了.
8.NDK下沒有messagebox
由於把messagebox做成了基礎架構函數,所以必須要使用這個功能.
但是ndk下幾乎沒有任何視窗API, 這個需要用JNI, 在C/C++層調用Java來解決了.
本來使用了ndk內建的NativeActivity想跳過Java coding, 編寫full native code, 目前看來不可行了.
9.dlopen不支援RTLD_NODELETE
由於使用了很多動態庫, 而為了調試記憶體, 把上層庫內部的local buffer指標比如__FILIE__, __PRETTY_FUNCTION__傳到了基礎庫裡面,在memory leak dump的時候需要把這些上層庫關閉(所有對象釋放)以後, 才能夠dump.
但是這些上層庫卸載之後,這些localbuffer已經隨著.so的釋放被釋放掉. 因為這些常量buffer通常是在編譯期直接產生到.so鏡像裡面的常量section. 這導致leak dump時引用了無效的野指標.
windows下也有類似的問題, 不過windows下的GetModuleHandleEx的GET_MODULE_HANDLE_EX_FLAG_PIN可以支援永久不卸載庫,與RTLD_NODELETE等價,在debug模式下需要開啟才能正常leak dump, 否則有泄露的時候可能會崩潰掉.
這個問題已經提交到官方issue裡了: http://code.google.com/p/android/issues/detail?id=64069
10.GLES 2.0
這部分沒有實現, 留著以後補充. 除了GLES以外,其他所有模組都已經移植完畢並且真機測試可以啟動運行.
先簡單總結下渲染部分, 主要有渲染裝置(drawcall/ render state), Texture/RenderTarget, Index/VertexBuffer, Shader, 這些封裝一下, 都是體力活, 還有很多坑. 但移植的基調已經定了, 這些等以後有空了在做, 最好等到GLES3.0普及了再做. 後面主要任務是平台無關的圖形效果, 等效果做的差不多了, 再移植就可以用了.
11.其他
還有很多APK打包上的諸多限制, 比如google play上要求apk最大50M, 超過的資料需要用擴充包, apk打包時候的檔案名稱/目錄結構限制, 動態庫.so載入的限制等等,已經在前一篇筆記中記錄了.
[百度空間] [原]跨平台編程注意事項(三): window 到 android 的 移植