溫輝敏(wenhm@sina.com)
2005年2月
【摘要】
根據(jù)zxms80項目的會議調(diào)度模塊移植的經(jīng)驗,本文提出了Windows平臺上程序向Linux下移植所碰到的一些典型問題,并舉例給出了相應(yīng)的解決方法,文中還描述了使用pwlib庫時makefile文件的編寫方法,該方法也適用于不使用pwlib庫開發(fā)時的一般情況,供要進行移植的同仁參考之用。
在程序員中有這樣一個說法,若一個程序不能移植到Linux下,那這個程序?qū)⒖床坏轿磥怼?/span>
由于Linux操作系統(tǒng)源碼公開是的,開發(fā)庫等輔助工具都是源碼公開的,這樣就減少了程序的不可預(yù)知性,而且出現(xiàn)錯誤可以大家一起修正、完善,而Windows平臺下所有的操作系統(tǒng)Api就給了個接口,即使出現(xiàn)莫名奇妙的錯誤也只能望著接口興嘆了。再加上Linux操作系統(tǒng)本身和它上面的許多工具軟件是免費的,更是吸引了更多的公司和程序開發(fā)人員將程序開發(fā)轉(zhuǎn)向Linux。
在程序跨平臺的移植過程中,將存在操作系統(tǒng)API的不同、文件名大小寫識別不同、路徑分隔符不同、不同開發(fā)平臺數(shù)據(jù)類型的不一致等較一般性的問題。對于這些一般性的問題怎樣很好的來解決呢?Linux下的工程都是使用makefile文件來管理的,怎樣編寫出相應(yīng)的makefile文件呢?這些問題都是本文后面將要闡述的。
本文撰寫的目的是為了提供Windows平臺上程序向Linux下移植所碰到的一些典型問題及相應(yīng)的解決方法,供要進行程序平臺移植的同仁參考之用。
文中還描述了使用pwlib庫時makefile文件的編寫方法,對于使用了pwlib庫進行開發(fā)的程序能快捷的建立makefile工程文件,避免了自己手動書寫makefile的繁雜工作。
特別是<
Linux下一般都是使用make工具來管理和編譯一個大的開發(fā)工程的所有源文件,make命令執(zhí)行時,需要一個 Makefile 文件,以告訴make命令需要怎么樣的去編譯和鏈接程序,makefile關(guān)系到了整個工程的編譯規(guī)則。一個工程中的源文件不計其數(shù),其按類型、功能、模塊分別放在若干個目錄中,makefile定義了一系列的規(guī)則來指定,哪些文件需要先編譯,哪些文件需要后編譯,哪些文件需要重新編譯,甚至于進行更復(fù)雜的功能操作,因為makefile就像一個Shell腳本一樣,其中也可以執(zhí)行操作系統(tǒng)的命令。在Windows的一些IDE如VC中將自動幫你生成相應(yīng)的makefile,所有這些都是透明的,但在Linux下你就不能不自己寫makefile了,會不會寫makefile,從一個側(cè)面說明了一個人是否具備完成大型工程的能力。
make工具采用增量編譯的方式,每次只編譯被改動過確實需要編譯的源文件,每次編譯時make工具將自動判斷那些源文件需要重新編譯,當(dāng)一個工程很大而又只改動了很少的幾個源文件,這將節(jié)省很多時間。
具體makefile文件的編寫規(guī)則可以查看make的man 和info文檔(在Linux命令行方式下輸入:man make 或info make)。makefile文件的編寫規(guī)則很多,重要的是怎樣使用最簡單的方式寫出我們自己需要的makefile文件。
網(wǎng)上也有很多介紹資料,網(wǎng)上有一篇很好的介紹makefile文件編寫的文章:
下文將著重介紹使用pwlib開發(fā)庫的工程的makefile的編寫,但對于其它工程只需將common.mak文件中對pwlib庫進行編譯的腳本去掉也可適用。
PWLib是Portable Windows Library的縮寫,翻譯為輕便的Windows類庫.PWLib采用C++編寫,設(shè)計初衷是為了能讓Openh323在Windows和Unix的X-Windows下運行, 不過隨著一步步的完善PWLib已經(jīng)被跨平臺的程序所廣泛采用。
查看Pwlib的主目錄下/samples/hello_world/目錄下例子程序的makefile文件可以發(fā)現(xiàn)該Makefile文件內(nèi)容如下:
# 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文件,編譯時需要的頭文件,相應(yīng)的編譯選項都在ptlib.mak文件中設(shè)置好了。
我們只需在該makefile文件所在目錄下,命令行輸入make all命令即可編譯出程序的Release版本和Debug版本,它們分別放在當(dāng)前目錄的obj_linux_x86_r和obj_linux_x86_d子目錄下。
下面對該makefile中的內(nèi)容進行解釋:
l PROG變量為編譯出來的程序名稱。
l SOURCES變量存儲的為本工程要進行編譯和鏈接的源文件,當(dāng)有多個源文件時可以用空格隔開,雖然文件名可以帶上路徑,但路徑在SOURCES變量中不起作用,實際編譯時對于每個文件它將截掉最后一個”/”字符前面的所有內(nèi)容只保留文件名。
l PWLIBDIR為pwlib的安裝目錄,需要設(shè)置該環(huán)境變量(若要系統(tǒng)每次重啟都自動設(shè)置好該環(huán)境變量則將該環(huán)境變量的設(shè)置放入/etc/profile文件中),若沒設(shè)置好則自動以”用戶主目錄\pwlib”作為pwlib的安裝目錄。
分析ptlib.mak文件,它的內(nèi)容如下;
ifndef PWLIBDIR
PWLIBDIR=$(HOME)/pwlib
endif
include $(PWLIBDIR)/make/unix.mak
include $(PWLIBDIR)/make/common.mak
也就是ptlib.mak包含了另兩個文件unix.mak和common.mak文件,其中unix.mak為定義編譯選項變量的文件,編譯規(guī)則放在common.mak文件中。
通過對這兩個文件的分析,歸納出一些我們寫makefile文件要用到的一些變量,列出如下:
l STDCCFLAGS:所有頭文件的include目錄編譯選項、預(yù)編譯宏定義選項、警告選項、優(yōu)化選項都放在該變量中,各編譯選項之間用空格分開。
l LDFLAGS:共享庫、靜態(tài)庫搜索目錄設(shè)置放入該變量中。
l LDLIBS:鏈接時要用的庫的設(shè)置放入該變量中。
l VPATH_CXX:*.cxx源文件的搜索路徑放入該變量中,多個目錄以空格分開,編譯時將首先在makefile所在目錄查找相應(yīng)源文件,沒找到則按照VPATH_CXX中的路徑設(shè)置進行查找。
l VPATH_C:*.c源文件的搜索路徑放入該變量中,多個目錄以空格分開,文件查找順序和VPATH_CXX變量的類似。
common.mak文件中只對*.c和*.cxx的源文件定義了編譯規(guī)則,而一般windows下程序大多都使用了.cpp來作為C++源文件的后綴。
怎樣加入對于.cpp后綴的源文件的編譯規(guī)則呢,這需要修改pwlib的common.mak文件,具體步驟如下:
1.加入對于.cpp文件的搜索目錄設(shè)置
在vpath %.cxx $(VPATH_CXX)語句后面加入如下語句:
vpath %.cpp $(VPATH_CXX)
2.加入對于.cpp文件的編譯規(guī)則
在$(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文件(目標代碼文件)的命名規(guī)則
在SRC_OBJS := $(SRC_OBJS:.cxx=.o)語句后面加入如下語句
SRC_OBJS := $(SRC_OBJS:.cpp=.o)
4.加入對于.cpp文件的.dep文件(依賴文件)的命名規(guī)則
在SRC_DEPS := $(SRC_DEPS:.cxx=.dep)語句后面加入如下語句
SRC_DEPS := $(SRC_DEPS:.cpp=.dep)
5.加入對于.cpp文件生成.dep文件的生成規(guī)則,加入如下語句:
在$(DEPDIR)/%.dep : %.cxx語句前面加入如下語句
$(DEPDIR)/%.dep : %.cpp
@if [ ! -d $(DEPDIR) ] ; then mkdir -p $(DEPDIR) ; fi
@printf %s $(OBJDIR) > $@
$(CPLUS) $(STDCCFLAGS:-g=) -M $< >> $@
進行程序移植的過程中碰到的問題較多,但大都主要集中在文件名大小寫、路徑分隔符、數(shù)據(jù)類型等方面。
Windows下基于MFC的API、基于消息的API、基于注冊表的API等在Linux下都是沒有的,由于文件系統(tǒng)的差異,和文件系統(tǒng)相關(guān)的API也是不可以移植的。
解決方法:程序中不使用上面所列的不可移植的操作系統(tǒng)API,通過使用開源庫如PWLIB或ACE中的可移植的類來實現(xiàn)所需的功能。
如:
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來實現(xiàn)
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);
一些函數(shù)在Windows操作系統(tǒng)的VC開發(fā)庫中有,但Linux下的GLIB C開發(fā)庫中沒有或是名字不一樣。
Windows下有而Linux下開發(fā)庫沒有的函數(shù),例如: itoa(int, char *, int)、ltoa(long, char *, int)、ultoa(unsigned long, char *, int)等。
解決方法1:通過編寫相應(yīng)的代碼來實現(xiàn)該函數(shù)。
解決方法2:使用Linux下含有類似功能的函數(shù)來替換,如itoa()、ltoa()等系列的函數(shù)都可以通過sprintf()或snprintf()函數(shù)來替換。
替換例子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()函數(shù)對應(yīng)的Linux下函數(shù)為sleep()和usleep(),要特別注意的是Windows下Sleep()為休眠多少毫秒,而sleep()和usleep()分別為休眠多少秒和微妙,所以替換的時候不僅要注意函數(shù)的名稱不同還要注意單位的不一致。
例3:Windows下stricmp()函數(shù)在Linux下對應(yīng)的為strcasecmp()函數(shù),可以通過宏定義來區(qū)分不同平臺的代碼,也可以在WINTYPES.H文件中加入如下語句:
#define stricmp strcasecmp
通過宏替換來實現(xiàn)。
Windows下由于操作系統(tǒng)對文件名大小寫不明感,#include語句中文件名的大小寫均可以,而Linux操作系統(tǒng)是對文件名大小寫敏感的,#include語句中的文件名必須和原文件名大小寫一模一樣才能找到。
解決方法:#include語句中文件名和原文件名大小寫不一致的全部要修改為一致。
Windows下路徑的分隔符使用”\”和“/“均可,而Linux下只能使用”\“來作為路徑中個目錄的分隔符。
解決方法:#include語句中路徑的分隔符全部使用“/”。
數(shù)據(jù)類型
例如:FAR PASCAL、HWND、HMENU、HFONT等,因為這些類型在Linux下無法找到替代它們的類型,必然導(dǎo)致程序的不可移植。
有些數(shù)據(jù)類型是可以通過類型定義來實現(xiàn)的,如CHAR、LONG、INT、INT32、FLOAT 、BOOL、VOID、UCHAR、CONST、WINAPI、CALLBACK等,這些類型在Windows下的VC開發(fā)庫中定義了,但在Linux下沒有。
解決方法:可以通過創(chuàng)建一個 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))
有些宏定義如:
#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下沒有。
解決方法:在使用到這類宏定義時將相應(yīng)的宏定義放入WINTYPES.H文件中即可。
Winows下struct in_addr結(jié)構(gòu)定義如下:
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結(jié)構(gòu)定義為:
struct in_addr
{
__u32 s_addr;
};
解決方法:因在使用這種類型的時候不同操作系統(tǒng)下面的代碼不一樣,要使用宏定義將不同操作系統(tǒng)下的代碼分開。
例:
#ifdef WIN32
ipAdd.S_un.S_addr = address_ip;
#else
ipAdd.s_addr = address_ip;
#endif
宏定義WIN32為Windows下VC編譯器自帶的一個宏定義,該宏定義在Linux下不存在所以在Windows下和Linux下使用的代碼是不同的。
有些頭文件在Windows下和Linux下名字不一樣,如strstrea.h在Linux下對應(yīng)的文件名為sstream或 strstream。
解決方法:當(dāng)發(fā)現(xiàn)以后寫頭文件在Linux下找不到時,查看下是否Linux相應(yīng)頭文件的名字不一樣,是否開發(fā)庫的不同版本頭文件不一樣,有些開發(fā)庫如STL開發(fā)庫由于不斷升級會淘汰一些頭文件而使用其它的頭文件來進行替代。
所以
#include <strstrea.h>
修改為:
#ifdef WIN32 //windows
#include <strstrea.h>
#else //linux
#include <sstream>
#include <strstream>
#endif
由于編譯器的差異也導(dǎo)致要將不同平臺下的代碼要使用宏定義來區(qū)分開,如:
for(int i=0; i< iSize; i++)
語句定義的變量i在Windows下該變量將在for語句執(zhí)行完后仍有效,而在Linux下變量i只在for語句內(nèi)部有效出了for語句的范圍后就失效了。
解決方法:這種情況是由于編譯器對語言語義上的理解不同導(dǎo)致的,只要看下編譯的錯誤信息就可以很快解決,要注意的時要使用宏定義來包含不同平臺之間的代碼。
Linux下的編譯器檢查比Windows下更嚴格,特別是類型轉(zhuǎn)換檢查方面,如:
char strTime=“2004/01/02 14:00:
PTime starttime = strTime;
在windows下編譯可以通過,但由于PTime類只有PTime(const PString & str )構(gòu)造函數(shù),而strTime為char[]類型,雖然在Windows下可以編譯通過但在Linux下編譯通不過。
解決方法:增加強制類型轉(zhuǎn)換即可。
char strTime=“2004/01/02 14:00:
PTime starttime = Pstring(strTime);
這方面的代碼編譯錯誤只要看下編譯的錯誤信息也可以很快就解決。由于是Linux下編譯器檢查比Windows下嚴格,所以只要能保證在Linux下編譯通過Windows下肯定也能編譯通過不用使用宏定義來包含不同平臺之間的代碼。
以上所列移植的問題是在進行zxms80項目 的CSS(會議調(diào)度模塊)移植時碰到的,CSS模塊采用pwlib的ptlib,mak文件來創(chuàng)建makefile文件,采用了前面所列的解決方法來解決碰到的問題,整個移植過程花了一個月左右。(CSS代碼大概40000行左右,使用了Pwlib庫、ACE+TAO庫、Libodbc++庫)
通過借用pwlib的ptlib.mak文件可以快捷的創(chuàng)建自己的makefile文件,創(chuàng)建出來的makefile簡單易讀。
Windows下程序往Linux下移植主要就是會碰到上面列出來的一些問題,文中為每類問題都進行了舉例和提供了相應(yīng)的解決方案希望對要進行程序平臺移植的同仁會有所裨益。
并不是任何程序都可以輕松進行移植的,只有在設(shè)計、開發(fā)初期考慮到程序的可移植性,使用了可移植的開發(fā)庫來進行開發(fā),盡量避免使用和平臺相關(guān)的代碼,這樣的程序才能快速、方便的進行移植。
文中描述的移植中碰到的問題和解決方法對于Windows平臺下C/C++程序向Linux平臺移植均適用,特別是對于使用了pwlib庫來進行開發(fā)的程序提出了快捷的建立makefile工程文件的方法,并對makefile文件的關(guān)鍵部分進行了解釋,最后給出了一個makefile文件的完整范例。即使是沒有使用pwlib開發(fā)庫也可以使用pwlib的相應(yīng)make文件來構(gòu)建自己的makefile文件,只是需要將相應(yīng)編譯pwlib庫的那部分腳本(common.mak文件中)屏蔽掉就可用于創(chuàng)建任何工程的makefile文件。
只要使用了可移植的開發(fā)庫來開發(fā)大部分代碼,移植過程還是比較順利的,主要是一些如文件名大小寫、路徑分隔符使用不對等小問題的重復(fù)修正,若是使用了很多和Windows Api相關(guān)的代碼如訪問注冊表、文件操作的Api則要費些功夫來重寫這部分代碼了。
通過對程序進行移植操作,一定更能深刻體會寫代碼時為什么要注意可移植性了,不能一味的為了方便使用簡單而不可移植的方法來實現(xiàn)。
1. 王華等,Linux從入門到精通,中國水利水電出版社,2000年9月第一版北京第一次印刷。
2. Stephen Figgins, Ellen Siever, Aaron Weber,LPI Linux Certification in a Nutshell,Publisher : O'Reilly,June 2003。
http://www.us1.openh323.org/。