一、問題的提出
在程序員中有這樣一個說法,若一個程序不能移植到Linux下,那這個程序將看不到未來。
由於Linux操作系統源碼公開是的,開發庫等輔助工具都是源碼公開的,這樣就減少了程序的不可預知性,而且出現錯誤可以大家一起修正、完善,而Windows平臺下所有的操作系統Api就給了個接口,即使出現莫名奇妙的錯誤也只能望着接口興嘆了。再加上Linux操作系統本身和它上面的許多工具軟件是免費的,更是吸引了更多的公司和程序開發人員將程序開發轉向Linux。<?xml:namespace prefix = o />
在程序跨平臺的移植過程中,將存在操作系統API的不同、文件名大小寫識別不同、路徑分隔符不同、不同開發平臺數據類型的不一致等較一般性的問題。對於這些一般性的問題怎樣很好的來解決呢?Linux下的工程都是使用makefile文件來管理的,怎樣編寫出相應的makefile文件呢?這些問題都是本文後面將要闡述的。
二、解決思路
本文撰寫的目的是爲了提供Windows平臺上程序向Linux下移植所碰到的一些典型問題及相應的解決方法,供要進行程序平臺移植的同仁參考之用。
文中還描述了使用pwlib庫時makefile文件的編寫方法,對於使用了pwlib庫進行開發的程序能快捷的建立makefile工程文件,避免了自己手動書寫makefile的繁雜工作。
特別是<<?xml:namespace prefix = st1 />3.2.6可以移植的數據類型>一節中對於不同開發平臺數據類型的不一致提出了一個簡捷通用的解決方法,不用修改源程序中任何代碼即可在Linux下使用Windows開發平臺上的一些數據類型。
三、實踐情況
3.1.Makefile的編寫
Linux下一般都是使用make工具來管理和編譯一個大的開發工程的所有源文件,make命令執行時,需要一個 Makefile 文件,以告訴make命令需要怎麼樣的去編譯和鏈接程序,makefile關係到了整個工程的編譯規則。一個工程中的源文件不計其數,其按類型、功能、模塊分別放在若干個目錄中,makefile定義了一系列的規則來指定,哪些文件需要先編譯,哪些文件需要後編譯,哪些文件需要重新編譯,甚至於進行更復雜的功能操作,因爲makefile就像一個Shell腳本一樣,其中也可以執行操作系統的命令。在Windows的一些IDE如VC中將自動幫你生成相應的makefile,所有這些都是透明的,但在Linux下你就不能不自己寫makefile了,會不會寫makefile,從一個側面說明了一個人是否具備完成大型工程的能力。
make工具採用增量編譯的方式,每次只編譯被改動過確實需要編譯的源文件,每次編譯時make工具將自動判斷那些源文件需要重新編譯,當一個工程很大而又只改動了很少的幾個源文件,這將節省很多時間。
具體makefile文件的編寫規則可以查看make的man 和info文檔(在Linux命令行方式下輸入:man make 或info make)。makefile文件的編寫規則很多,重要的是怎樣使用最簡單的方式寫出我們自己需要的makefile文件。
網上也有很多介紹資料,網上有一篇很好的介紹makefile文件編寫的文章:
<?xml:namespace prefix = v />
下文將着重介紹使用pwlib開發庫的工程的makefile的編寫,但對於其它工程只需將common.mak文件中對pwlib庫進行編譯的腳本去掉也可適用。
3.1.1使用pwlib開發庫的工程的makefile的編寫
PWLib是Portable Windows Library的縮寫,翻譯爲輕便的Windows類庫.PWLib採用C++編寫,設計初衷是爲了能讓Openh323在Windows和Unix的X-Windows下運行, 不過隨着一步步的完善PWLib已經被跨平臺的程序所廣泛採用。
查看Pwlib的主目錄下/samples/hello_world/目錄下例子程序的makefile文件可以發現該Makefile文件內容如下:
# Simple makefile for the hello world program
PROG = hello
SOURCES = hello.cxx
ifndef PWLIBDIR
PWLIBDIR=$(HOME)/pwlib
endif
include $(PWLIBDIR)/make/ptlib.mak
# End of Makefile
實際上就是使用了Pwlib庫的ptlib.mak文件,編譯時需要的頭文件,相應的編譯選項都在ptlib.mak文件中設置好了。
我們只需在該makefile文件所在目錄下,命令行輸入make all命令即可編譯出程序的Release版本和Debug版本,它們分別放在當前目錄的obj_linux_x86_r和obj_linux_x86_d子目錄下。
下面對該makefile中的內容進行解釋:
l PROG變量爲編譯出來的程序名稱。
l SOURCES變量存儲的爲本工程要進行編譯和鏈接的源文件,當有多個源文件時可以用空格隔開,雖然文件名可以帶上路徑,但路徑在SOURCES變量中不起作用,實際編譯時對於每個文件它將截掉最後一個”/”字符前面的所有內容只保留文件名。
l PWLIBDIR爲pwlib的安裝目錄,需要設置該環境變量(若要系統每次重啓都自動設置好該環境變量則將該環境變量的設置放入/etc/profile文件中),若沒設置好則自動以”用戶主目錄/pwlib”作爲pwlib的安裝目錄。
3.1.2深入分析ptlib.mak文件
分析ptlib.mak文件,它的內容如下;
ifndef PWLIBDIR
PWLIBDIR=$(HOME)/pwlib
endif
include $(PWLIBDIR)/make/unix.mak
include $(PWLIBDIR)/make/common.mak
也就是ptlib.mak包含了另兩個文件unix.mak和common.mak文件,其中unix.mak爲定義編譯選項變量的文件,編譯規則放在common.mak文件中。
通過對這兩個文件的分析,歸納出一些我們寫makefile文件要用到的一些變量,列出如下:
l STDCCFLAGS:所有頭文件的include目錄編譯選項、預編譯宏定義選項、警告選項、優化選項都放在該變量中,各編譯選項之間用空格分開。
l LDFLAGS:共享庫、靜態庫搜索目錄設置放入該變量中。
l LDLIBS:鏈接時要用的庫的設置放入該變量中。
l VPATH_CXX:*.cxx源文件的搜索路徑放入該變量中,多個目錄以空格分開,編譯時將首先在makefile所在目錄查找相應源文件,沒找到則按照VPATH_CXX中的路徑設置進行查找。
l VPATH_C:*.c源文件的搜索路徑放入該變量中,多個目錄以空格分開,文件查找順序和VPATH_CXX變量的類似。
3.1.3加入新的編譯規則
common.mak文件中只對*.c和*.cxx的源文件定義了編譯規則,而一般windows下程序大多都使用了.cpp來作爲C++源文件的後綴。
怎樣加入對於.cpp後綴的源文件的編譯規則呢,這需要修改pwlib的common.mak文件,具體步驟如下:
1.加入對於.cpp文件的搜索目錄設置
在vpath %.cxx $(VPATH_CXX)語句後面加入如下語句:
vpath %.cpp $(VPATH_CXX)
2.加入對於.cpp文件的編譯規則
在$(OBJDIR)/%.o : %.cxx語句的前面加入如下語句:
$(OBJDIR)/%.o : %.cpp
@if [ ! -d $(OBJDIR) ] ; then mkdir -p $(OBJDIR) ; fi
$(CPLUS) $(STDCCFLAGS) $(OPTCCFLAGS) $(CFLAGS) $(STDCXXFLAGS) -x c++ -c $< -o $@
3.加入對於.cpp文件的.o文件(目標代碼文件)的命名規則
在SRC_OBJS := $(SRC_OBJS:.cxx=.o)語句後面加入如下語句
SRC_OBJS := $(SRC_OBJS:.cpp=.o)
4.加入對於.cpp文件的.dep文件(依賴文件)的命名規則
在SRC_DEPS := $(SRC_DEPS:.cxx=.dep)語句後面加入如下語句
SRC_DEPS := $(SRC_DEPS:.cpp=.dep)
5.加入對於.cpp文件生成.dep文件的生成規則,加入如下語句:
在$(DEPDIR)/%.dep : %.cxx語句前面加入如下語句
$(DEPDIR)/%.dep : %.cpp
@if [ ! -d $(DEPDIR) ] ; then mkdir -p $(DEPDIR) ; fi
@printf %s $(OBJDIR) > $@
$(CPLUS) $(STDCCFLAGS:-g=) -M $< >> $@
3.1.4一個makefile範例
3.2.程序的移植
進行程序移植的過程中碰到的問題較多,但大都主要集中在文件名大小寫、路徑分隔符、數據類型等方面。
3.2.1Linux和Windows操作系統API差異
Windows下基於MFC的API、基於消息的API、基於註冊表的API等在Linux下都是沒有的,由於文件系統的差異,和文件系統相關的API也是不可以移植的。
解決方法:程序中不使用上面所列的不可移植的操作系統API,通過使用開源庫如PWLIB或ACE中的可移植的類來實現所需的功能。
如:
SYSTEMTIME pTime;
GetLocalTime(&pTime); //爲windows獨有的API
sprintf(sTemp,"[%02.2d-%02.2d-%02.2d]%02.2d:%02.2d:%02.2d %s(%d)",
pTime.wYear,pTime.wMonth,pTime.wDay,
pTime.wHour,pTime.wMinute,pTime.wSecond,file,lineNum);
改爲:
使用pwlib的PTime來實現
PTime curTime; //pwlib中可以跨平臺使用的時間類
sprintf(sTemp,"[%02.2d-%02.2d-%02.2d]%02.2d:%02.2d:%02.2d %s(%d)",
curTime.GetYear(), curTime.GetMonth(), curTime.GetDay(),
curTime.GetHour(), curTime.GetMinute(), curTime.GetSecond(), file, lineNum);
3.2.2開發庫函數的差異
一些函數在Windows操作系統的VC開發庫中有,但Linux下的GLIB C開發庫中沒有或是名字不一樣。
Windows下有而Linux下開發庫沒有的函數,例如: itoa(int, char *, int)、ltoa(long, char *, int)、ultoa(unsigned long, char *, int)等。
解決方法1:通過編寫相應的代碼來實現該函數。
解決方法2:使用Linux下含有類似功能的函數來替換,如itoa()、ltoa()等系列的函數都可以通過sprintf()或snprintf()函數來替換。
替換例子1:
ltoa( confHistb.conflong, caTemp, 10 );
可以替換爲:
#ifdef WIN32 //windows
ltoa( confHistb.conflong, caTemp, 10 );
#else //linux
sprintf(caTemp, "%d", confHistb.conflong);
#endif
或直接用sprintf(caTemp, "%d", confHistb.conflong);替換即可。
例2:Windows下Sleep()函數對應的Linux下函數爲sleep()和usleep(),要特別注意的是Windows下Sleep()爲休眠多少毫秒,而sleep()和usleep()分別爲休眠多少秒和微妙,所以替換的時候不僅要注意函數的名稱不同還要注意單位的不一致。
例3:Windows下stricmp()函數在Linux下對應的爲strcasecmp()函數,可以通過宏定義來區分不同平臺的代碼,也可以在WINTYPES.H文件中加入如下語句:
#define stricmp strcasecmp
通過宏替換來實現。
3.2.3Linux下對文件名大小寫敏感
Windows下由於操作系統對文件名大小寫不明感,#include語句中文件名的大小寫均可以,而Linux操作系統是對文件名大小寫敏感的,#include語句中的文件名必須和原文件名大小寫一模一樣才能找到。
解決方法:#include語句中文件名和原文件名大小寫不一致的全部要修改爲一致。
3.2.4Linux下路徑中各目錄的分隔符只能爲”/”
Windows下路徑的分隔符使用”/”和“/“均可,而Linux下只能使用”/“來作爲路徑中個目錄的分隔符。
解決方法:#include語句中路徑的分隔符全部使用“/”。
數據類型
3.2.5程序裏不能使用Windows特有的數據類型
例如:FAR PASCAL、HWND、HMENU、HFONT等,因爲這些類型在Linux下無法找到替代它們的類型,必然導致程序的不可移植。
3.2.6可以移植的數據類型
有些數據類型是可以通過類型定義來實現的,如CHAR、LONG、INT、INT32、FLOAT 、BOOL、VOID、UCHAR、CONST、WINAPI、CALLBACK等,這些類型在Windows下的VC開發庫中定義了,但在Linux下沒有。
解決方法:可以通過創建一個 WINTYPES.H的頭文件,將這些類型定義放在該文件裏。
編譯時加上“-include PATH/WTYPES.H”編譯選項即可不用在代碼中加入任何“#include”語句而使用WINTYPES.H中的類型,這裏的PATH爲WINTYPES.H文件所在的路徑。示例代碼如下:
typedef float FLOAT;
typedef char CHAR;
#define VOID void
#define WINAPI __attribute__((stdcall))
#define CALLBACK __attribute__((stdcall))
3.2.7一些宏定義Windows下有而Linux下沒有
有些宏定義如:
#define MAKEWORD(a, b) ((WORD)(((BYTE)(a)) | ((WORD)((BYTE)(b))) << 8))
#define MAKELONG(a, b) ((LONG)(((WORD)(a)) | ((DWORD)((WORD)(b))) << 16))
#define LOWORD(l) ((WORD)(l))
#define HIWORD(l) ((WORD)(((DWORD)(l) >> 16) & 0xFFFF))
#define LOBYTE(w) ((BYTE)(w))
#define HIBYTE(w) ((BYTE)(((WORD)(w) >> 8) & 0xFF))
等在Windows下有,而Linux下沒有。
解決方法:在使用到這類宏定義時將相應的宏定義放入WINTYPES.H文件中即可。
3.2.8同名但結構不同的數據類型
Winows下struct in_addr結構定義如下:
struct in_addr {
union {
struct { u_char s_b1,s_b2,s_b3,s_b4; } S_un_b;
struct { u_short s_w1,s_w2; } S_un_w;
u_long S_addr;
} S_un;
};
而Linux下struct in_addr結構定義爲:
struct in_addr
{
__u32 s_addr;
};
解決方法:因在使用這種類型的時候不同操作系統下面的代碼不一樣,要使用宏定義將不同操作系統下的代碼分開。
例:
#ifdef WIN32
ipAdd.S_un.S_addr = address_ip;
#else
ipAdd.s_addr = address_ip;
#endif
宏定義WIN32爲Windows下VC編譯器自帶的一個宏定義,該宏定義在Linux下不存在所以在Windows下和Linux下使用的代碼是不同的。
3.2.9Windows下的頭文件Linux下名字不同
有些頭文件在Windows下和Linux下名字不一樣,如strstrea.h在Linux下對應的文件名爲sstream或 strstream。
解決方法:當發現以後寫頭文件在Linux下找不到時,查看下是否Linux相應頭文件的名字不一樣,是否開發庫的不同版本頭文件不一樣,有些開發庫如STL開發庫由於不斷升級會淘汰一些頭文件而使用其它的頭文件來進行替代。
所以
#include <strstrea.h>
修改爲:
#ifdef WIN32 //windows
#include <strstrea.h>
#else //linux
#include <sstream>
#include <strstream>
#endif
3.2.10Windows和Linux下編譯器對語言理解的差異
由於編譯器的差異也導致要將不同平臺下的代碼要使用宏定義來區分開,如:
for(int i=0; i< iSize; i++)
語句定義的變量i在Windows下該變量將在for語句執行完後仍有效,而在Linux下變量i只在for語句內部有效出了for語句的範圍後就失效了。
解決方法:這種情況是由於編譯器對語言語義上的理解不同導致的,只要看下編譯的錯誤信息就可以很快解決,要注意的時要使用宏定義來包含不同平臺之間的代碼。
3.2.11Linux下編譯器檢查比Windows下VC的編譯器檢查更嚴格
Linux下的編譯器檢查比Windows下更嚴格,特別是類型轉換檢查方面,如:
char strTime=“2004/01/02 14:00:00”;
PTime starttime = strTime;
在windows下編譯可以通過,但由於PTime類只有PTime(const PString & str )構造函數,而strTime爲char[]類型,雖然在Windows下可以編譯通過但在Linux下編譯通不過。
解決方法:增加強制類型轉換即可。
char strTime=“2004/01/02 14:00:00”;
PTime starttime = Pstring(strTime);
這方面的代碼編譯錯誤只要看下編譯的錯誤信息也可以很快就解決。由於是Linux下編譯器檢查比Windows下嚴格,所以只要能保證在Linux下編譯通過Windows下肯定也能編譯通過不用使用宏定義來包含不同平臺之間的代碼。
四、效果評價
以上所列移植的問題是在進行zxms80項目 的CSS(會議調度模塊)移植時碰到的,CSS模塊採用pwlib的ptlib,mak文件來創建makefile文件,採用了前面所列的解決方法來解決碰到的問題,整個移植過程花了一個月左右。(CSS代碼大概40000行左右,使用了Pwlib庫、ACE+TAO庫、Libodbc++庫)
通過借用pwlib的ptlib.mak文件可以快捷的創建自己的makefile文件,創建出來的makefile簡單易讀。
Windows下程序往Linux下移植主要就是會碰到上面列出來的一些問題,文中爲每類問題都進行了舉例和提供了相應的解決方案希望對要進行程序平臺移植的同仁會有所裨益。
五、推廣建議
並不是任何程序都可以輕鬆進行移植的,只有在設計、開發初期考慮到程序的可移植性,使用了可移植的開發庫來進行開發,儘量避免使用和平臺相關的代碼,這樣的程序才能快速、方便的進行移植。
文中描述的移植中碰到的問題和解決方法對於Windows平臺下C/C++程序向Linux平臺移植均適用,特別是對於使用了pwlib庫來進行開發的程序提出了快捷的建立makefile工程文件的方法,並對makefile文件的關鍵部分進行了解釋,最後給出了一個makefile文件的完整範例。即使是沒有使用pwlib開發庫也可以使用pwlib的相應make文件來構建自己的makefile文件,只是需要將相應編譯pwlib庫的那部分腳本(common.mak文件中)屏蔽掉就可用於創建任何工程的makefile文件。
只要使用了可移植的開發庫來開發大部分代碼,移植過程還是比較順利的,主要是一些如文件名大小寫、路徑分隔符使用不對等小問題的重複修正,若是使用了很多和Windows Api相關的代碼如訪問註冊表、文件操作的Api則要費些功夫來重寫這部分代碼了。
通過對程序進行移植操作,一定更能深刻體會寫代碼時爲什麼要注意可移植性了,不能一味的爲了方便使用簡單而不可移植的方法來實現。