Windows下程序向Linux下移植细节

Windows下程序向Linux下移植细节

【摘要】

根据zxms80项目的会议调度模块移植的经验,本文提出了Windows平台上程序向Linux下移植所碰到的一些典型问题,并举例给出了相应的解决方法,文中还描述了使用pwlib库时makefile文件的编写方法,该方法也适用于不使用pwlib库开发时的一般情况,供要进行移植的同仁参考之用。

 

一、问题的提出
    在程序员中有这样一个说法,若一个程序不能移植到Linux下,那这个程序将看不到未来。

由于Linux操作系统源码公开是的,开发库等辅助工具都是源码公开的,这样就减少了程序的不可预知性,而且出现错误可以大家一起修正、完善,而Windows平台下所有的操作系统Api就给了个接口,即使出现莫名奇妙的错误也只能望着接口兴叹了。再加上Linux操作系统本身和它上面的许多工具软件是免费的,更是吸引了更多的公司和程序开发人员将程序开发转向Linux。

在程序跨平台的移植过程中,将存在操作系统API的不同、文件名大小写识别不同、路径分隔符不同、不同开发平台数据类型的不一致等较一般性的问题。对于这些一般性的问题怎样很好的来解决呢?Linux下的工程都是使用makefile文件来管理的,怎样编写出相应的makefile文件呢?这些问题都是本文后面将要阐述的。

 

二、解决思路
   本文撰写的目的是为了提供Windows平台上程序向Linux下移植所碰到的一些典型问题及相应的解决方法,供要进行程序平台移植的同仁参考之用。

文中还描述了使用pwlib库时makefile文件的编写方法,对于使用了pwlib库进行开发的程序能快捷的建立makefile工程文件,避免了自己手动书写makefile的繁杂工作。

特别是<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文件编写的文章:


下文将着重介绍使用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则要费些功夫来重写这部分代码了。

通过对程序进行移植操作,一定更能深刻体会写代码时为什么要注意可移植性了,不能一味的为了方便使用简单而不可移植的方法来实现。

 


参考资料
1.       王华等,Linux从入门到精通,中国水利水电出版社,2000年9月第一版北京第一次印刷。

2.       Stephen Figgins, Ellen Siever, Aaron Weber,LPI Linux Certification in a Nutshell,Publisher : O'Reilly,June 2003。