从windows到unix移植from IBM论坛.docx
《从windows到unix移植from IBM论坛.docx》由会员分享,可在线阅读,更多相关《从windows到unix移植from IBM论坛.docx(28页珍藏版)》请在冰豆网上搜索。
从windows到unix移植fromIBM论坛
从Windows移植到UNIX环境
第一部分
大多数基于Microsoft®Windows®的项目都是使用MicrosoftVisualStudio®构建的,这是一种复杂的集成开发环境(IDE),它可以为开发人员实现几乎整个构建过程的自动化。
此外,Windows开发人员使用了Windows平台特定的应用程序程序接口(API)、头文件和语言扩展。
大多数类UNIX®系统,如SunOS、OpenBSD和IRIX,都不支持IDE或者任何Windows特定的头文件或扩展,因此进行移植是一项非常耗费时间的活动。
更麻烦的是,遗留的基于Windows的代码需要运行于16位或者32位的x86体系结构中。
基于UNIX的环境通常是64位的,并且大多数UNIX供应商都不支持x86指令集。
本系列文章共由两个部分组成,本文是其中的第一部分,介绍将Windows操作系统中一个典型的VisualC++项目移植到SunOS中的g++环境的过程,同时详细说明了如何解决前面提到的一些问题。
VisualStudio中的C/C++项目类型
您可以使用VisualC++项目创建三种项目变体(单线程或者多线程)中的一种:
∙动态链接库(DLL或者.dll)
∙静态库(LIB或者.lib)
∙可执行文件(.exe)
对于更复杂的变体,可以使用VisualStudio.NET解决方案,这种解决方案允许创建和管理多个项目。
本文在下面的几个部分中将重点关注如何将动态和静态库项目变体从Windows移植到UNIX。
将DLL移植到UNIX环境
对于Windows中的.dll文件,UNIX的等价物是共享对象(.so)文件。
然而,创建一个.so文件的过程与创建一个.dll文件的过程完全不同。
请考虑清单1中的示例,在这个示例中,您尝试创建一个小的.dll文件,其中仅包含一个函数printHello,并且在main.cpp文件的main例程中调用了这个函数。
清单1.包含printHello例程声明的文件hello.h
#ifdefBUILDING_DLL
#definePRINT_API__declspec(dllexport)
#else
#definePRINT_API__declspec(dllimport)
#endif
extern"C"PRINT_APIvoidprintHello();
清单2提供了hello.cpp的源代码。
清单2.文件hello.cpp
#include
#include"hello.h"
voidprintHello
{
std:
:
cout<<"helloWindows/UNIXusers\n";
}
extern"C"PRINT_APIvoidprintHello();
如果您使用了用于80x86平台的Microsoft32位C/C++标准编译器(cl),那么可以使用下面的命令来创建hello.dll文件:
cl/LDhello.cpp/DBUILDING_DLL
/LD指示cl创建一个.dll文件。
(还可以指示它创建其他格式的文件,如.exe或者.obj。
)/DBUILDING_DLL为这个特定的构建过程定义了PRINT_API宏,以便从这个DLL导出printHello符号。
清单3包含了main.cppmain源文件,其中使用了printHello例程。
这里所做的假设是,hello.h、hello.cpp和main.cpp都位于相同的文件夹中。
清单3.使用printHello例程的main的源代码
#include"hello.h"
intmain()
{
printHello();
return0;
}
要编译并连接main代码,可以使用下面的命令行:
clmain.cpphello.lib
快速地查看源文件和生成的输出,其中说明了两个重要的问题。
第一点,要从一个DLL中导出任何函数、变量、或者类,都需要使用Windows特定的语法__declspec(dllexport)。
同样地,要向一个DLL导入任何函数、变量、或者类,都需要使用Windows特定的语法__declspec(dllimport)。
第二点,这个编译过程生成了两个文件:
printHello.dll和printHello.lib。
PrintHello.lib用于连接main源文件,而UNIX中共享对象的头文件不需要declspec语法。
成功的编译过程将输出一个.so文件,它已经与main源文件进行了连接。
要在UNIX平台中使用g++创建一个共享库,需要通过向g++传递-fPIC标志,将所有的源文件编译为可重定位的共享对象。
PIC表示位置无关代码(positionindependentcode)。
在每次加载一个共享库时,可以将其潜在地映射为一个新的内存地址。
因此,需要通过某种很容易进行计算的方式在库中生成所有变量和函数的地址(相对于加载该库的起始地址)。
这个代码由-fPIC选项生成,并使得代码成为可重定位的。
-o选项用于指定输出文件的名称,而-shared选项用于构建一个共享库,其中允许出现未解析的引用。
要创建hello.so文件,您必须修改头文件,如下面的清单4所示。
清单4.包含UNIX特定更改的、经过修改的hello.h头文件
#ifdefined(__GNUC__)&&defined(__unix__)
#definePRINT_API__attribute__((__visibility__("default")))
#elifdefined(WIN32)
#ifdefBUILDING_DLL
#definePRINT_API__declspec(dllexport)
#else
#definePRINT_API__declspec(dllimport)
#endif
extern"C"PRINT_APIvoidprintHello();
下面的g++命令用于连接共享库hello.so:
g++-fPIC-sharedhello.cpp-ohello.so
要创建main可执行文件,请编译源代码:
g++-omainmain.cpphello.so
g++中的符号隐藏
有两种典型的方式可以从一个基于Windows的DLL中导出符号。
第一种方法是仅对从DLL中导出的选择元素(例如,类、全局变量或者全局函数)使用__declspec(dllexport)。
第二种方法是使用一个模块-定义(.def)文件。
.def文件具有自己的语法,并且包含需要从DLL中导出的符号。
g++连接器的缺省行为是从一个.so文件中导出所有的符号。
这可能并不是所需要的,并且将使得连接多个DLL变成一项非常耗时的任务。
为了从一个共享库中有选择地导出符号,可以使用g++属性机制。
例如,可以考虑用户源代码中包含两个方法,'voidprint1();'和'intprint2(char*);',并且用户只需要导出print2。
清单5包含一种实现这个目的的方法,可用于Windows和UNIX。
清单5.g++中的符号隐藏
#ifdef_MSC_VER//VisualStudiospecificmacro
#ifdefBUILDING_DLL
#defineDLLEXPORT__declspec(dllexport)
#else
#defineDLLEXPORT__declspec(dllimport)
#endif
#defineDLLLOCAL
#else
#defineDLLEXPORT__attribute__((visibility("default")))
#defineDLLLOCAL__attribute__((visibility("hidden")))
#endif
extern"C"DLLLOCALvoidprint1();//print1hidden
extern"C"DLLEXPORTintprint2(char*);//print2exported
使用__attribute__((visibility("hidden")))可以防止从DLL中导出符号。
最新版本的g++(4.0.0以及更高的版本)还提供了-fvisibility开关,您可以使用它从一个共享库中有选择地导出相关符号。
在命令行中使用g++加上-fvisibility=hidden延迟从共享库中导出所有的符号,除了那些使用__attribute__((visibility("default")))声明的符号。
这是一种非常简洁的方式,用于通知g++没有显式地标注可见属性的每项声明,其可见性都是隐藏的。
使用dlsym提取一个隐藏的符号将会返回NULL。
g++中的属性机制概述
与VisualStudio环境非常相似(VisualStudio环境在C/C++的基础上提供了许多附加的语法),g++也支持该语言的许多非标准扩展。
在g++中,属性机制的用途之一就是便于进行移植。
前面的示例讨论了符号隐藏。
属性的另一个用途是为VisualC++设置函数类型,如cdecl、stdcall和fastcall。
本系列文章的第2部分将详细地介绍属性机制。
在UNIX环境中显式加载DLL或者共享对象
在Windows系统中,可以由Windows程序显式地加载一个.dll文件,这是很常见的情况。
例如,可以考虑一个复杂的、提供了打印功能的、基于Windows的编辑器。
在用户第一次提出相应请求的时候,这种编辑器将动态地加载打印机驱动程序DLL。
基于Windows的开发人员可以使用VisualStudio提供的API,如LoadLibrary显式地加载一个DLL,GetProcAddress用于查询DLL中的符号,而FreeLibrary则用于卸载一个显式加载的DLL。
对于这些函数,UNIX的等价物分别是dlopen、dlsym和dlclose例程。
而且在Windows中,有一个特殊的DllMain方法,在第一次将DLL加载到内存时将调用这个方法。
类UNIX系统提供了一个对应的方法,称为_init。
可以考虑前面示例的一个变体。
清单6中是loadlib.h头文件,在调用main方法的源文件中使用了这个文件。
清单6.头文件loadlib.h
#ifndef__LOADLIB_H
#define__LOADLIB_H
#ifdefUNIX
#include
#endif
#include
usingnamespacestd;
typedefvoid*(*funcPtr)();
#ifdefUNIX
#defineIMPORT_DIRECTIVE__attribute__((__visibility__("default")))
#defineCALL
#else
#defineIMPORT_DIRECTIVE__declspec(dllimport)
#defineCALL__stdcall
#endif
extern"C"{
IMPORT_DIRECTIVEvoid*CALLLoadLibraryA(constchar*sLibName);
IMPORT_DIRECTIVEfuncPtrCALLGetProcAddress(
void*hModule,constchar*lpProcName);
IMPORT_DIRECTIVEboolCALLFreeLibrary(void*hLib);
}
#endif
main方法现在显式地加载printHello.dll文件,并调用相同的print方法,如所示清单7中所示。
清单7.主文件Loadlib.cpp
#include"loadlib.h"
intmain(intargc,char*argv[])
{
#ifndefUNIX
char*fileName="hello.dll";
void*libraryHandle=LoadLibraryA(fileName);
if(libraryHandle==NULL)
cout<<"dllnotfound"<else//makeacallto"printHello"fromthehello.dll
(GetProcAddress(libraryHandle,"printHello"))();
FreeLibrary(libraryHandle);
#else//unix
void(*voidfnc)();
char*fileName="hello.so";
void*libraryHandle=dlopen(fileName,RTLD_LAZY);
if(libraryHandle==NULL)
cout<<"sharedobjectnotfound"<else//makeacallto"printHello"fromthehello.so
{
voidfnc=(void(*)())dlsym(libraryHandle,"printHello");
(*voidfnc)();
}
dlclose(libraryHandle);
#endif
return0;
}
Windows和UNIX环境中的DLL搜索路径
在Windows操作系统中,按照下面的顺序搜索DLL:
1.可执行文件所处的目录(例如,notepad.exe位于Windows目录中)
2.当前工作目录(即,从哪个目录启动了notepad.exe。
)
3.Windows系统目录(通常为C:
\Windows\System32)
4.Windows目录(通常为C:
\Windows)
5.作为PATH环境变量中的一部分所列举的目录
在类UNIX系统中,如Solaris,LD_LIBRARY_PATH环境变量可以指定共享库搜索顺序。
指向一个新的共享库的路径需要追加到LD_LIBRARY_PATH变量末尾。
HP-UX的搜索顺序包括作为LD_LIBRARY_PATH的一部分所列举的目录,然后是SHLIB_PATH中列举的目录。
对于IBMAIX®操作系统,由LIBPATH变量确定共享库搜索顺序。
将静态库从Windows移植到UNIX
与动态库不同,在编译应用程序时对静态库的目标代码进行连接,并且因此成为该应用程序的一部分。
在UNIX系统中,静态库遵循一种命名规范,使用lib作为前缀,而使用.a作为库名的后缀。
例如在UNIX系统中,Windows的user.lib文件通常被命名为libuser.a。
操作系统提供的命令ar和ranlib可用于创建静态库。
清单8说明了如何从user_sqrt1.cpp和user_log1.cpp源文件创建一个静态库libuser.a。
清单8.在UNIX环境中创建静态库
g++-ouser_sqrt1.o-cuser_sqrt1.cpp
g++-ouser_log1.o-cuser_log1.cpp
arrclibuser.auser_sqrt1.ouser_log1.o
ranliblibuser.a
ar工具创建了静态库libuser.a,并将user_sqrt1.o和user_log1.o目标文件的副本放置于其中。
如果存在一个现有的库文件,那么将目标文件添加到其中。
如果所使用的目标文件比库中的文件更新一些,那么则替换旧的目标文件。
r标志表示使用相同目标文件的更新版本替换库中旧的目标文件。
如果这个库并不存在,那么c选项将创建这个库。
在创建了一个新的存档文件,或者修改了一个现有的存档文件之后,需要创建存档文件内容的索引,并将其作为该存档文件的一部分进行存储。
这个索引列出了存档文件的成员(可重定位目标文件)所定义的每个符号。
该索引可以提高与静态库进行连接的速度,并允许调用库中的例程,而不考虑它们在库中的实际位置。
请注意,GNUranlib是ar工具的扩展,并且使用s参数调用ar,[ar-s]与调用ranlib具有相同的效果。
预编译头文件
在VisualC++中,基于C/C++的应用程序通常会使用预编译头文件。
预编译头文件是某些编译器(如VisualStudio中的cl)的一项性能特性,它可以帮助提高编译的速度。
复杂的应用程序通常会使用头文件(.h或者.hpp)文件,它们是需要作为一部分进行包括的一个或多个源文件的代码部分。
在一个项目的范围内,很少对头文件进行修改。
因此,为了提高编译的速度,可以将这些文件转换为一种编译器更容易理解的中间形式,以便提高后续编译工作的速度。
在VisualStudio环境中,这种中间形式称为预编译头文件或者PCH。
考虑本文前面清单1和2中包括hello.cpp的示例。
其中包含了iostream和EXPORT_API宏的定义,在该项目的范围内,这些可以被看作是该文件中不变的代码部分。
因此,它们适合放在一个头文件中进行包含。
清单9显示了可能会发生相关更改的代码。
清单9.precomp.h的内容
#ifndef__PRECOMP_H
#define__PRECOMP_H
#include
#ifdefined(__GNUC__)&&defined(__unix__)
#defineEXPORT_API__attribute__((__visibility__("default")))
#elifdefinedWIN32
#defineEXPORT_API__declspec(dllexport)
#endif
清单10显示了DLL的源代码,其中包括相关的更改。
清单10.新的hello.cpp文件的内容
#include"precomp.h"
#pragmahdrstop
extern"C"EXPORT_APIvoidprintHello()
{
std:
:
cout<<"helloWindows/UNIXusers"<:
endl;
}
正如其名称所表示的,预编译头文件在头中止(headerstop)点之前,以一种经过编译的形式包含目标代码。
源文件中的这个点通常由一个词素进行标记,而预处理程序不会使用该词素作为一个语言符号,这表示它并不是一项预处理程序指令。
或者,还可以将这个头中止点指定为#pragmahdrstop,如果在源文本中,它出现在一个有效的非预处理程序语言关键字之前。
在Solaris中进行编译时,当碰到#include时,将搜索预编译头文件。
在搜索包含文件的过程中,编译器首先在每个目录中查找预编译头文件,然后再在这些目录中搜索包含文件。
需要搜索的名称可以在带.gch的#include中进行指定。
如果无法使用这个预编译头文件,那么将忽略它。
下面的命令行可用于在Windows中实现预编译头文件功能:
cl/Ycprecomp.hhello.cpp/DWIN32/LD
/Yc通知cl编译器从precomp.h生成预编译头文件。
可以使用下面的命令在Solaris中实现相同的功能:
g++precomp.h
g++-fPIC-Ghello.cpp-ohello.so
第一个命令创建了预编译头文件precomp.h.gch。
剩下的生成共享对象的过程与本文前面所描述的相同。
注意:
g++版本3.4及更高的版本提供了对预编译头文件的支持。
结束语
在两个完全不同的系统之间(如Windows和UNIX)进行移植,绝不是一项简单的任务,并且它需要大量的调整工作和耐心。
本文说明了将最基本的项目类型从VisualStudio环境移植到基于g++/Solaris环境的基本要素。
第二篇文章作为本系列文章的总结,将介绍VisualStudio环境及其g++等价物中各种可用的编译器选项、g++属性机制、从32位(通常是指Windows)环境移植到64位(UNIX)环境时的一些问题,以及多线程等等。
第二部分
比较和对照相关的编译器选项
VisualC++和GNUg++都为cl编译器提供了一些选项。
尽管您可以使用cl作为独立的工具进行编译工作,但是,VisualC++提供了一种灵活的集成开发环境(IDE)以设置编译器选项。
使用VisualStudio®开发的软件通常使用了一些编辑器特定的和平台相关的特性,可以使用编译器或者连接器来控制这些特性。
当您在不同的平台(使用了不同的编译器或者工具链)之间移植源代码的时候,了解编译器的相关选项,这一点是非常重要的。
这部分内容深入分析了一些最有价值的编译器选项。
启用字符串池
可以考虑下面的代码片段:
char*string1="Thisisacharacterbuffer";
char*string2="Thisisacharacterbuffer";
如果在VisualC++中启用了字符串池选项[/GF],那么在执行期间,将在程序的映像中仅保存该字符串的单个副本,且string1与string2相等。
需要说明的是,g++的行为正好与它相反,在缺省情况下,string1与string2相等。
要在g++中禁用字符串池,您必须将-fwritable-strings选项添加到g++命令行。
使用wchar_t
C++标准定义了wchar_t宽字符类型。
如果将/Zc:
wchar_t选项传递给编译器,那么VisualC++会将wchar_t作为本地类型。
否则,需要包含一些实现特定的Header,如windows.h或者一些标准的Header(如wchar.h)。
g++