[百度空間] [原]跨平台編程注意事項(三): window 到 android 的 移植

來源:互聯網
上載者:User

標籤:

大的問題

先記錄一下跨平台時需要注意的大方向.

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 的 移植

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.