Chinaunix首页 | 论坛 | 博客
  • 博客访问: 1642418
  • 博文数量: 268
  • 博客积分: 8708
  • 博客等级: 中将
  • 技术积分: 3764
  • 用 户 组: 普通用户
  • 注册时间: 2007-04-06 15:58
文章分类

全部博文(268)

文章存档

2014年(1)

2013年(15)

2012年(23)

2011年(60)

2010年(51)

2009年(12)

2008年(59)

2007年(47)

分类: BSD

2010-04-12 11:47:35

从 Windows 移植到 UNIX,第 1 部分: 移植 C/C++ 源代码

揭秘如何将基于 C/C++ 的项目从 Windows 移植到 UNIX

developerWorks
文档选项


级别: 中级

Rahul Kumar Kardam (), 高级软件工程师, Synapti Computer Aided Design Pvt Ltd

2007 年 10 月 11 日

通常,软件程序需要运行于各种系统中,而在这些系统中,程序的编码或者开发是完全不同的。这种在不同系统之间修改软件的过程称为移植。出于某种原因,您可能需要对软件进行移植。例如最终用户希望在一个新的环境中使用该软件,如不同版本的 UNIX®,或者开发人员正在将他们自己的代码集成到该软件中,以便为了组织的平台对其进行优化。

大多数基于 Microsoft® Windows® 的项目都是使用 Microsoft Visual Studio® 构建的,这是一种复杂的集成开发环境 (IDE),它可以为开发人员实现几乎整个构建过程的自动化。此外,Windows 开发人员使用了 Windows 平台特定的应用程序程序接口 (API)、头文件和语言扩展。大多数类 UNIX® 系统,如 SunOS、OpenBSD 和 IRIX,都不支持 IDE 或者任何 Windows 特定的头文件或扩展,因此进行移植是一项非常耗费时间的活动。更麻烦的是,遗留的基于 Windows 的代码需要运行于 16 位或者 32 位的 x86 体系结构中。基于 UNIX 的环境通常是 64 位的,并且大多数 UNIX 供应商都不支持 x86 指令集。本系列文章共由两个部分组成,本文是其中的第一部分,介绍将 Windows 操作系统中一个典型的 Visual C++ 项目移植到 SunOS 中的 g++ 环境的过程,同时详细说明了如何解决前面提到的一些问题。

您可以使用 Visual C++ 项目创建三种项目变体(单线程或者多线程)中的一种:

  • 动态链接库(DLL 或者 .dll)
  • 静态库(LIB 或者 .lib)
  • 可执行文件(.exe)

对于更复杂的变体,可以使用 Visual Studio .NET 解决方案,这种解决方案允许创建和管理多个项目。本文在下面的几个部分中将重点关注如何将动态和静态库项目变体从 Windows 移植到 UNIX。

对于 Windows 中的 .dll 文件,UNIX 的等价物是共享对象 (.so) 文件。然而,创建一个 .so 文件的过程与创建一个 .dll 文件的过程完全不同。请考虑清单 1 中的示例,在这个示例中,您尝试创建一个小的 .dll 文件,其中仅包含一个函数 printHello,并且在 main.cpp 文件的 main 例程中调用了这个函数。



                
#ifdef BUILDING_DLL
  #define PRINT_API __declspec(dllexport)
#else
  #define PRINT_API __declspec(dllimport)
#endif

extern "C" PRINT_API void printHello();

清单 2 提供了 hello.cpp 的源代码。



                
#include 
#include "hello.h"

void printHello  
  {
  std::cout << "hello Windows/UNIX users\n";
  }

extern "C" PRINT_API void printHello();

如果您使用了用于 80x86 平台的 Microsoft 32 位 C/C++ 标准编译器 (cl),那么可以使用下面的命令来创建 hello.dll 文件:

cl /LD  hello.cpp /DBUILDING_DLL

/LD 指示 cl 创建一个 .dll 文件。(还可以指示它创建其他格式的文件,如 .exe 或者 .obj。)/DBUILDING_DLL 为这个特定的构建过程定义了 PRINT_API 宏,以便从这个 DLL 导出 printHello 符号。

清单 3 包含了 main.cpp main 源文件,其中使用了 printHello 例程。这里所做的假设是,hello.h、hello.cpp 和 main.cpp 都位于相同的文件夹中。



                
#include "hello.h"

int main ( )
  {
  printHello();
  return 0;
  }

要编译并连接 main 代码,可以使用下面的命令行:

cl main.cpp hello.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 表示位置无关代码 (position independent code)。在每次加载一个共享库时,可以将其潜在地映射为一个新的内存地址。因此,需要通过某种很容易进行计算的方式在库中生成所有变量和函数的地址(相对于加载该库的起始地址)。这个代码由 -fPIC 选项生成,并使得代码成为可重定位的。-o 选项用于指定输出文件的名称,而 -shared 选项用于构建一个共享库,其中允许出现未解析的引用。要创建 hello.so 文件,您必须修改头文件,如下面的清单 4 所示。



                
#if defined (__GNUC__) && defined(__unix__)
  #define PRINT_API __attribute__ ((__visibility__("default")))
#elif defined (WIN32)
  #ifdef BUILDING_DLL
    #define PRINT_API __declspec(dllexport)
  #else
    #define PRINT_API __declspec(dllimport)
#endif

extern "C" PRINT_API void printHello();

下面的 g++ 命令用于连接共享库 hello.so:

g++ -fPIC -shared hello.cpp -o hello.so

要创建 main 可执行文件,请编译源代码:

g++ -o main main.cpp hello.so

有两种典型的方式可以从一个基于 Windows 的 DLL 中导出符号。第一种方法是仅对从 DLL 中导出的选择元素(例如,类、全局变量或者全局函数)使用 __declspec(dllexport)。第二种方法是使用一个模块-定义 (.def) 文件。.def 文件具有自己的语法,并且包含需要从 DLL 中导出的符号。

g++ 连接器的缺省行为是从一个 .so 文件中导出所有的符号。这可能并不是所需要的,并且将使得连接多个 DLL 变成一项非常耗时的任务。为了从一个共享库中有选择地导出符号,可以使用 g++ 属性机制。例如,可以考虑用户源代码中包含两个方法,'void print1();' 和 ' int print2(char*);',并且用户只需要导出 print2。清单 5 包含一种实现这个目的的方法,可用于 Windows 和 UNIX。



                
#ifdef _MSC_VER // Visual Studio specific macro
  #ifdef BUILDING_DLL
    #define DLLEXPORT __declspec(dllexport)
  #else
    #define DLLEXPORT __declspec(dllimport)
  #endif
  #define DLLLOCAL 
#else 
  #define DLLEXPORT __attribute__ ((visibility("default")))
  #define DLLLOCAL   __attribute__ ((visibility("hidden")))
#endif 

extern "C" DLLLOCAL void print1();         // print1 hidden 
extern "C" DLLEXPORT int print2(char*); // print2 exported

使用 __attribute__ ((visibility("hidden"))) 可以防止从 DLL 中导出符号。最新版本的 g++(4.0.0 以及更高的版本)还提供了 -fvisibility 开关,您可以使用它从一个共享库中有选择地导出相关符号。在命令行中使用 g++ 加上 -fvisibility=hidden 延迟从共享库中导出所有的符号,除了那些使用 __attribute__ ((visibility("default"))) 声明的符号。这是一种非常简洁的方式,用于通知 g++ 没有显式地标注可见属性的每项声明,其可见性都是隐藏的。使用 dlsym 提取一个隐藏的符号将会返回 NULL

与 Visual Studio 环境非常相似(Visual Studio 环境在 C/C++ 的基础上提供了许多附加的语法),g++ 也支持该语言的许多非标准扩展。在 g++ 中,属性机制的用途之一就是便于进行移植。前面的示例讨论了符号隐藏。属性的另一个用途是为 Visual C++ 设置函数类型,如 cdeclstdcall 和 fastcall。本系列文章的第 2 部分将详细地介绍属性机制。

在 Windows 系统中,可以由 Windows 程序显式地加载一个 .dll 文件,这是很常见的情况。例如,可以考虑一个复杂的、提供了打印功能的、基于 Windows 的编辑器。在用户第一次提出相应请求的时候,这种编辑器将动态地加载打印机驱动程序 DLL。基于 Windows 的开发人员可以使用 Visual Studio 提供的 API,如 LoadLibrary 显式地加载一个 DLL,GetProcAddress 用于查询 DLL 中的符号,而 FreeLibrary 则用于卸载一个显式加载的 DLL。对于这些函数,UNIX 的等价物分别是 dlopendlsym 和 dlclose 例程。而且在 Windows 中,有一个特殊的 DllMain 方法,在第一次将 DLL 加载到内存时将调用这个方法。类 UNIX 系统提供了一个对应的方法,称为 _init

可以考虑前面示例的一个变体。清单 6 中是 loadlib.h 头文件,在调用 main 方法的源文件中使用了这个文件。



                
#ifndef  __LOADLIB_H
#define  __LOADLIB_H

#ifdef UNIX
#include 
#endif 

#include 
using namespace std;

typedef void* (*funcPtr)();

#ifdef UNIX
#  define IMPORT_DIRECTIVE __attribute__((__visibility__("default")))
#  define CALL  
#else
#  define IMPORT_DIRECTIVE __declspec(dllimport) 
#  define CALL __stdcall
#endif

extern "C" {
  IMPORT_DIRECTIVE void* CALL LoadLibraryA(const char* sLibName); 
  IMPORT_DIRECTIVE funcPtr CALL GetProcAddress(
                                    void* hModule, const char* lpProcName);
  IMPORT_DIRECTIVE bool CALL  FreeLibrary(void* hLib);
}

#endif

main 方法现在显式地加载 printHello.dll 文件,并调用相同的 print 方法,如所示清单 7 中所示。



                

#include "loadlib.h"

int main(int argc, char* argv[])
  {
  #ifndef UNIX
    char* fileName = "hello.dll";
    void* libraryHandle = LoadLibraryA(fileName);
    if (libraryHandle == NULL)
      cout << "dll not found" << endl;
    else  // make a call to "printHello" from the hello.dll 
      (GetProcAddress(libraryHandle, "printHello"))();
    FreeLibrary(libraryHandle);
#else // unix
    void (*voidfnc)(); 
    char* fileName = "hello.so";
    void* libraryHandle = dlopen(fileName, RTLD_LAZY);
    if (libraryHandle == NULL)
      cout << "shared object not found" << endl;
    else  // make a call to "printHello" from the hello.so
      {
      voidfnc = (void (*)())dlsym(libraryHandle, "printHello"); 
      (*voidfnc)();
      }
    dlclose(libraryHandle);
  #endif

  return 0;
  }

在 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 中列举的目录。对于 IBM AIX® 操作系统,由 LIBPATH 变量确定共享库搜索顺序。

与动态库不同,在编译应用程序时对静态库的目标代码进行连接,并且因此成为该应用程序的一部分。在 UNIX 系统中,静态库遵循一种命名规范,使用 lib 作为前缀,而使用 .a 作为库名的后缀。例如在 UNIX 系统中,Windows 的 user.lib 文件通常被命名为 libuser.a。操作系统提供的命令 ar 和 ranlib 可用于创建静态库。清单 8 说明了如何从 user_sqrt1.cpp 和 user_log1.cpp 源文件创建一个静态库 libuser.a。



                
g++ -o user_sqrt1.o -c user_sqrt1.cpp 
g++ -o user_log1.o -c user_log1.cpp
ar rc libuser.a user_sqrt1.o user_log1.o 
ranlib libuser.a

ar 工具创建了静态库 libuser.a,并将 user_sqrt1.o 和 user_log1.o 目标文件的副本放置于其中。如果存在一个现有的库文件,那么将目标文件添加到其中。如果所使用的目标文件比库中的文件更新一些,那么则替换旧的目标文件。r 标志表示使用相同目标文件的更新版本替换库中旧的目标文件。如果这个库并不存在,那么 c 选项将创建这个库。

在创建了一个新的存档文件,或者修改了一个现有的存档文件之后,需要创建存档文件内容的索引,并将其作为该存档文件的一部分进行存储。这个索引列出了存档文件的成员(可重定位目标文件)所定义的每个符号。该索引可以提高与静态库进行连接的速度,并允许调用库中的例程,而不考虑它们在库中的实际位置。请注意,GNU ranlib 是 ar 工具的扩展,并且使用 s 参数调用 ar[ar -s] 与调用ranlib 具有相同的效果。

在 Visual C++ 中,基于 C/C++ 的应用程序通常会使用预编译头文件。预编译头文件是某些编译器(如 Visual Studio 中的 cl)的一项性能特性,它可以帮助提高编译的速度。复杂的应用程序通常会使用头文件(.h 或者 .hpp)文件,它们是需要作为一部分进行包括的一个或多个源文件的代码部分。在一个项目的范围内,很少对头文件进行修改。因此,为了提高编译的速度,可以将这些文件转换为一种编译器更容易理解的中间形式,以便提高后续编译工作的速度。在 Visual Studio 环境中,这种中间形式称为预编译头文件或者 PCH。

考虑本文前面清单 1 和 2 中包括 hello.cpp 的示例。其中包含了 iostream 和 EXPORT_API 宏的定义,在该项目的范围内,这些可以被看作是该文件中不变的代码部分。因此,它们适合放在一个头文件中进行包含。清单 9 显示了可能会发生相关更改的代码。



                
#ifndef __PRECOMP_H
#define __PRECOMP_H

#include 

#  if defined (__GNUC__) && defined(__unix__)
#    define EXPORT_API __attribute__((__visibility__("default")))
#  elif defined WIN32
#    define EXPORT_API __declspec(dllexport) 
#  endif

清单 10 显示了 DLL 的源代码,其中包括相关的更改。



                
#include "precomp.h"
#pragma hdrstop

extern "C" EXPORT_API void printHello()
  {
  std::cout << "hello Windows/UNIX users" << std::endl;
  }

正如其名称所表示的,预编译头文件在 头中止 (header stop) 点之前,以一种经过编译的形式包含目标代码。源文件中的这个点通常由一个词素进行标记,而预处理程序不会使用该词素作为一个语言符号,这表示它并不是一项预处理程序指令。或者,还可以将这个头中止点指定为 #pragma hdrstop,如果在源文本中,它出现在一个有效的非预处理程序语言关键字之前。

在 Solaris 中进行编译时,当碰到 #include 时,将搜索预编译头文件。在搜索包含文件的过程中,编译器首先在每个目录中查找预编译头文件,然后再在这些目录中搜索包含文件。需要搜索的名称可以在带 .gch 的 #include 中进行指定。如果无法使用这个预编译头文件,那么将忽略它。

下面的命令行可用于在 Windows 中实现预编译头文件功能:

cl /Yc precomp.h hello.cpp /DWIN32 /LD

/Yc 通知 cl 编译器从 precomp.h 生成预编译头文件。可以使用下面的命令在 Solaris 中实现相同的功能:

g++ precomp.h
g++ -fPIC -G hello.cpp -o hello.so

第一个命令创建了预编译头文件 precomp.h.gch。剩下的生成共享对象的过程与本文前面所描述的相同。

注意: g++ 版本 3.4 及更高的版本提供了对预编译头文件的支持。

在两个完全不同的系统之间(如 Windows 和 UNIX)进行移植,绝不是一项简单的任务,并且它需要大量的调整工作和耐心。本文说明了将最基本的项目类型从 Visual Studio 环境移植到基于g++/Solaris 环境的基本要素。第二篇文章作为本系列文章的总结,将介绍 Visual Studio 环境及其 g++ 等价物中各种可用的编译器选项、g++ 属性机制、从 32 位(通常是指 Windows)环境移植到 64 位(UNIX)环境时的一些问题,以及多线程等等。


从 Windows 移植到 UNIX,第 2 部分: 移植 C/C++ 源代码的内部探密

从编译器的各种属性到一些经常碰到的问题

developerWorks
文档选项
将打印机的版面设置成横向打印模式

将此页作为电子邮件发送

将此页作为电子邮件发送


级别: 中级

Rahul Kumar Kardam (), 高级软件工程师, Synapti Computer Aided Design Pvt Ltd
Arpan Sen (), 技术主管, Synapti Computer Aided Design Pvt Ltd

2007 年 12 月 03 日

本系列文章的第 1 部分介绍了在 Microsoft® Visual Studio® 环境中使用的典型 C/C++ 项目类型,并介绍了将动态和静态库项目的各种变体移植到 UNIX® 平台的过程。第 2 部分将深入讨论一些用于构建 Visual C++ 项目的编译器选项以及它们的 UNIX 和 g++ 等价选项,详细介绍与移植相关的 g++ 属性机制,并研究从 32 位 Windows® 环境移植到 64 位 UNIX 环境时您可能经常会遇到的一些问题。最后,对移植多线程应用程序的概念进行了概述,并提供了一个示例项目,以便实际应用本文中介绍的内容。

Visual C++ 和 GNU g++ 都为 cl 编译器提供了一些选项。尽管您可以使用 cl 作为独立的工具进行编译工作,但是,Visual C++ 提供了一种灵活的集成开发环境 (IDE) 以设置编译器选项。使用 Visual Studio® 开发的软件通常使用了一些编辑器特定的和平台相关的特性,可以使用编译器或者连接器来控制这些特性。当您在不同的平台(使用了不同的编译器或者工具链)之间移植源代码的时候,了解编译器的相关选项,这一点是非常重要的。这部分内容深入分析了一些最有价值的编译器选项。

可以考虑下面的代码片段:

      char *string1= "This is a character buffer";
      char *string2= "This is a character buffer";
      

如果在 Visual C++ 中启用了字符串池选项 [/GF],那么在执行期间,将在程序的映像中仅保存该字符串的单个副本,且 string1 与 string2 相等。需要说明的是,g++ 的行为正好与它相反,在缺省情况下,string1 与 string2 相等。要在 g++ 中禁用字符串池,您必须将 -fwritable-strings 选项添加到 g++ 命令行。

C++ 标准定义了 wchar_t 宽字符类型。如果将 /Zc:wchar_t 选项传递给编译器,那么 Visual C++ 会将 wchar_t 作为本地类型。否则,需要包含一些实现特定的 Header,如 windows.h 或者一些标准的 Header(如 wchar.h)。g++ 支持本地 wchar_t 类型,并且不需要包括特定的 Header。请注意,在不同的平台之间,wchar_t 的大小是不相同的。您可以使用 -fshort-wchar g++ 选项将wchar_t 的大小强制规定为两个字节。

如果源代码没有使用 dynamic_cast 或者 typeid 操作符,那么就可以禁用运行时类型识别 (RTTI)。在缺省情况下,Visual Studio 2005 中打开了 RTTI(即 /GR 开关处于打开状态)。可以使用 /GR-开关在 Visual Studio 环境中禁用 RTTI。禁用 RTTI 可能有助于产生更小的可执行文件。请注意,在包含 dynamic_cast 或者 typeid 的代码中禁用 RTTI,可能会产生一些负面的影响,包括代码崩溃。可以考虑清单 1 中的代码片段。



                
      #include 
      struct A { 
        virtual void f() 
          { std::cout << "A::f\n"; } 
        };
        
      struct B : A { 
        virtual void f() 
          { std::cout << "B::f\n"; } 
        };
        
      struct C : B { 
        virtual void f() 
          { std::cout << "C::f\n"; } 
        };
        
      int main (int argc, char** argv ) 
        {
        A* pa = new C;
        B* pb = dynamic_cast (pa);
        if (pb) 
          pb->f();
        return 0;
        }
      

为在 Visual Studio IDE 之外独立的 cl 编译器中编译这个代码片段,需要显式地打开 /GR 切换开关。与 cl 不同,g++ 编译器不需要任何特殊的选项以打开 RTTI。然而,与 Visual Studio 中的 /GR- 选项一样,g++ 提供了 -fno-rtti 选项,用以显式地关闭 RTTI。在 g++ 中使用 -fno-rtti 选项编译这个代码片段,将报告编译错误。然而,即使 cl 在编译这个代码时不使用 /GR 选项,但是生成的可执行文件在运行时将会崩溃。

要在 cl 中启用异常处理,可以使用 /GX 编译器选项或者 /EHsc。如果不使用这两个选项,try 和 catch 代码仍然可以执行,并且系统执行到 throw 语句时才会调用局部对象的析构函数。异常处理会带来性能损失。因为编译器将为每个 C++ 函数生成进行堆展开的代码,这种需求将导致更大的可执行文件、更慢的运行代码。对于特定的项目,有时无法接受这种性能损失,那么您需要关闭该特性。要禁用异常处理,您需要从源代码中删除所有的 try 和 catch 块,并使用 /GX- 选项编译代码。在缺省情况下,g++ 编译器启用了异常处理。将 -fno-exceptions 选项传递给 g++,会产生所需的效果。请注意,对包含 trycatch 和 throw 关键字的源代码使用这个选项,可能会导致编译错误。您仍然需要手工地从源代码中删除 try 和 catch 块(如果有的话),然后将这个选项传递给 g++。可以考虑清单 2 中的代码。



                
      #include 
      using namespace std;

      class A { public: ~A () { cout << "Destroying A "; } };
      void f1 () { A a; throw 2; }

      int main (int argc, char** argv ) {
        try { f1 (); } catch (...) { cout << "Caught!\n"; }
        return 0;
        }
      

下面是 cl 和 g++ 在使用以及不使用该部分中所介绍的相关选项时得到的输出结果:

  • cl 使用 /GX 选项: Destroying A Caught!
  • cl 不使用 /GX 选项: Caught!
  • g++ 不使用 -fno-exceptions: Destroying A Caught!
  • g++ 使用 -fno-exceptions:编译时间错误

对于循环的一致性,可以考虑清单 3 中的代码片段。



                
      int main (int argc, char** argv )
        {
        for (int i=0; i<5; i++);
        i = 7;
        return 0;
        }
      

根据 ISO C++ 的指导原则,这个代码将无法通过编译,因为作为循环中的一部分而声明的 i 局部变量的范围仅限于该循环体,并且在该循环之外是不能进行访问的。在缺省情况下,cl 将完成这个代码的编译,而不会产生任何错误。然而,如果 cl 使用 /Zc:forScope 选项,将导致编译错误。g++ 的行为正好与 cl 相反,对于这个测试将产生下面的错误:

error: name lookup of 'i' changed for new ISO 'for' scoping

要想禁止这个行为,您可以在编译期间使用 -fno-for-scope 标志。

Visual C++ 和 GNU g++ 都为语言提供了一些非标准的扩展。g++ 属性机制非常适合于对 Visual C++ 代码中的平台特定的特性进行移植。属性语法采用格式 __attribute__ ((attribute-list)),其中属性列表是以逗号分隔的多个属性组成的列表。该属性列表中的单个元素可以是一个单词,或者是一个单词后面紧跟使用括号括起来的、该属性的可能的参数。这部分研究了如何在移植操作中使用这些属性。

您可以使用 Visual Studio 中特定的关键字,如 __cdecl__stdcall 和 __fastcall,以便向编译器说明函数的调用约定。表 1 对有关的详细内容进行了汇总。



调用约定隐含的语义
__cdecl(cl 选项:/Gd)从右到左地将被调用函数的参数压入堆栈。在执行完毕之后,由调用函数将参数弹出堆栈。
__stdcall(cl 选项:/Gz)从右到左地将被调用函数的参数压入堆栈。在执行完毕之后,由调用函数将参数弹出堆栈。
__fastcall(cl 选项:/Gr)将最前面的两个参数传递到 ECX 和 EDX 寄存器中,同时将所有其他参数从右到左地压入堆栈。由被调用函数负责清除执行后的堆栈。

用以表示相同行为的 g++ 属性是 cdeclstdcall 和 fastcall清单 4 显示了 Windows® 和 UNIX® 中属性声明风格的细微差别。



                
      Visual C++ Style Declaration:
      double __stdcall compute(double d1, double d2);

      g++ Style Declaration:
      double __attribute__((stdcall)) compute(double d1, double d2);
      

/Zpn 结构成员对齐选项可以控制结构在内存中的对齐方式。例如,/Zp8 以 8 个字节为单位对结构进行对齐(这也是缺省的方式),而 /Zp16 则以 16 个字节为单位对结构进行对齐。您可以使用aligned g++ 属性来指定变量的对齐方式,如清单 5 中所示。



                
      Visual C++ Style Declaration with /Zp8 switch:
      struct T1 { int n1; double d1;};

      g++ Style Declaration:
      struct T1 { int n1; double d1;}  __attribute__((aligned(8)));
      

然而,对齐属性的有效性将受到固有的连接器局限性的限制。在许多系统中,连接器只能够以某个最大的对齐方式对变量进行对齐。

这个属性可以告诉编译器,使用该属性声明的函数以及它调用的后续函数都不会引发异常。使用这个特性可以对减少整体代码的大小进行优化,因为在缺省情况下,即使代码不会引发异常,cl 仍然会为 C++ 源代码生成堆栈展开信息。您可以使用 nothrow g++ 属性以实现类似的目的,如清单 6 中所示。



                
      Visual C++ Style Declaration:
      double __declspec(nothrow) sqrt(double d1);

      g++ Style Declaration:
      double __attribute__((nothrow)) sqrt(double d1);
      

一种更加具有可移植性的方法是,使用标准定义的风格: double sqrt(double d1) throw ();.

除了前面的一些示例之外,Visual C++ 和 g++ 属性方案之间还存在一些相似的内容。例如,这两种编译器都支持 noinlinenoreturndeprecated 和 naked 属性。

在 Win32 系统中开发的 C++ 代码是基于 ILP32 模型的,在该模型中,intlong 和指针类型都是 32 位的。UNIX 系统则遵循 LP64 模型,其中 long 和指针类型都是 64 位的,但是 int 仍然保持为 32 位。大部分的代码破坏,都是由于这种更改所导致的。这部分简要讨论了您可能会遇到的两个最基本的问题。从 32 位到 64 位系统的移植是一个非常广阔的研究领域。有关这个主题的更多信息,请参见参考资料部分。

某些数据类型在 ILP32 和 LP64 模型中是相同的,使用这样的数据类型才是合理的做法。通常,您应该尽可能地避免使用 long 和 pointer 数据。另外,通常我们会使用 sys/types.h 标准 Header 中定义的数据类型,但是这个文件中的一些数据类型(如 ptrdiff_t, size_t 等等)的大小,在 32 位模型和 64 位模型之间是不一样的,您在使用时必须小心。

个别数据结构的内存需求可能会发生改变,这依赖于编译器中实现对齐的方式。可以考虑清单 7 中的代码片段。



                
      struct s { 
                int var1;  // hole between var1 and var2 
                long var2;
                int var3; // hole between var3 and ptr1
                char* ptr1;
             };
      // sizeof(s) = 32 bytes
      

在 LP64 模型中,long 和 pointer 类型都以 64 位为单位进行对齐。另外,结构的大小以其中最大成员的大小为单位进行对齐。在这个示例中,结构 s 以 8 个字节为单位进行对齐,s.var2 变量同样也是如此。这将导致在该结构中出现一些空白的地方,从而使内存膨胀。清单 8 中的重新排列导致该结构的大小变为 24 个字节。



                
      struct s { 
                int var1;  
                int var3;
                long var2;
                char* ptr1;
             };
      // sizeof(s) = 24 bytes
      

从技术上讲,一个线程是操作系统可以调度运行的独立指令流。在这两种环境中,线程都位于进程之中,并且使用进程的资源。只要线程的父进程存在,并且操作系统支持线程,那么线程将具有它自己的独立控制流。它可能与其他独立(或者非独立)使用的线程共享进程资源,如果它的父进程结束,那么它也将结束。下面对一些典型的应用程序接口 (API) 进行了概述,您可以使用这些 API 在 Windows 和 UNIX 环境中建立多线程的项目。对于 WIN32 API,所选择的接口是 C 运行时例程,考虑到简单性和清晰性,这些例程符合可移植操作系统接口(Portable Operating System Interface,POSIX)的线程。

请注意:由于本文篇幅有限,我们不可能为编写多线程应用程序的其他方式提供详细的介绍。

Windows 使用 C 运行时库函数中的 _beginthread API 来创建线程。您还可以使用一些其他的 Win32 API 来创建线程,但是在后续的内容中,您将仅使用 C 运行时库函数。顾名思义,_beginthread() 函数可以创建一个执行例程的线程,其中将指向该例程的指针作为第一个参数。这个例程使用了 __cdecl C 声明调用约定,并返回空值。当线程从这个例程中返回时,它将会终止。

在 UNIX 中,可以使用 pthread_create() 函数完成相同的任务。pthread_create() 子程序使用线程参数返回新的线程 ID。调用者可以使用这个线程 ID,以便对该线程执行各种操作。检查这个 ID,以确保该线程存在。

_endthread 函数可以终止由 _beginthread() 创建的线程。当线程的顺序执行完成时,该线程将自动终止。如果需要在线程中根据某个条件终止它的执行,那么 _endthread() 函数是非常有用的。

在 UNIX 中,可以使用 pthread_exit() 函数实现相同的任务。如果正常的顺序执行尚未完成,这个函数将退出线程。如果 main() 在它创建的线程之前完成,并使用 pthread_exit() 退出,那么其他线程将继续执行。否则,当 main() 完成的时候,其他线程将自动终止。

要实现同步,您可以使用互斥信号量。在 Windows 中,CreateMutex() 可以创建互斥信号量。它将返回一个句柄,任何需要互斥信号量对象的函数都可以使用这个句柄,因为对这个互斥信号量提供了所有的访问权限。当拥有这个互斥信号量的线程不再需要它的时候,可以调用 ReleaseMutex(),以便将它释放回系统。如果调用线程并不拥有这个互斥信号量,那么这个函数的执行将会失败。

在 UNIX 中,可以使用 pthread_mutex_init() 例程动态地创建一个互斥信号量。这个方法允许您设置互斥信号量对象的相关属性。或者,当通过 pthread_mutex_t 变量声明它的时候,可以静态地创建它。要释放一个不再需要的互斥信号量对象,可以使用 pthread_mutex_destroy()

既然您已经掌握了本文前面所介绍的内容,下面让我们来看一个小程序示例,该程序使用在主进程中执行的不同线程向控制台输出信息。清单 9 是 multithread.cpp 的源代码。



                
#include 
#include 

#ifdef WIN32
  #include 
  #include 
  #include 
  #include 
#else 
  #include 
#endif

#define MAX_THREADS 32  

#ifdef WIN32
  void InitWinApp();
  void WinThreadFunction( void* );  
  void ShutDown(); 

 HANDLE  mutexObject;                   
#else
  void InitUNIXApp();   
  void* UNIXThreadFunction( void *argPointer );                

  pthread_mutex_t mutexObject = PTHREAD_MUTEX_INITIALIZER; 
#endif

int     threadsStarted;             // Number of threads started 

int main()                          
{
  #ifdef WIN32
    InitWinApp();
  #else 
    InitUNIXApp();
  #endif  
}

#ifdef WIN32
void InitWinApp()
  {
      
  /* Create the mutex and reset thread count. */
  mutexObject = CreateMutex( NULL, FALSE, NULL );   /* Cleared */
  if(mutexObject == NULL && GetLastError() != ERROR_SUCCESS) 
    {
    printf("failed to obtain a proper mutex for multithreaded application");
    exit(1);
    }
  threadsStarted = 0;
  for(;threadsStarted < 5 && threadsStarted < MAX_THREADS; 
       threadsStarted++)
    {
    _beginthread( WinThreadFunction, 0,  &threadsStarted );
    } 
  ShutDown();
  CloseHandle( mutexObject );
  getchar();
  }

void ShutDown() 
  {
  while ( threadsStarted > 0 )
    {
    ReleaseMutex( mutexObject ); /* Tell thread to die. */
    threadsStarted--;
    }
  }

void WinThreadFunction( void *argPointer )
  {
  WaitForSingleObject( mutexObject, INFINITE );
  printf("We are inside a thread\n");
  ReleaseMutex(mutexObject);
  }

#else 
void InitUNIXApp()
  {   
  int count = 0, rc;
  pthread_t threads[5];

  /* Create independent threads each of which will execute functionC */

  while(count < 5)
    {
    rc = pthread_create(&threads[count], NULL, &UNIXThreadFunction, NULL); 
    if(rc) 
      {  
      printf("thread creation failed");
      exit(1);
      }
    count++;
    }

  // We will have to wait for the threads to finish execution otherwise 
  // terminating the main program will terminate all the threads it spawned
  for(;count >= 0;count--)
    { 
    pthread_join( threads[count], NULL);
    }
  //Note : To destroy a thread explicitly pthread_exit() function can be used 
  //but since the thread gets terminated automatically on execution we did 
  //not make explicit calls to pthread_exit(); 
  exit(0);
  }

void* UNIXThreadFunction( void *argPointer )
  {
   pthread_mutex_lock( &mutexObject );
   printf("We are inside a thread\n");
   pthread_mutex_unlock( &mutexObject );
  }

#endif

    

我们利用 Visual Studio Toolkit 2003 和 Microsoft Windows 2000 Service Pack 4 通过下面的命令行对 multithread.cpp 的源代码进行了测试:

    cl multithread.cpp /DWIN32 /DMT /TP
    

我们还在使用 g++ 编译器版本 3.4.4 的 UNIX 平台中通过下面的命令行对它进行了测试:

    g++ multithread.cpp -DUNIX -lpthread
    

清单 10 是该程序在两种环境中的输出。



                
    We are inside a thread
    We are inside a thread
    We are inside a thread
    We are inside a thread
    We are inside a thread
    

在两种完全不同的平台(如 Windows 和 UNIX)之间进行移植,需要了解多个领域的知识,包括了解编译器和它们的选项、平台特定的特性(如 DLL)以及实现特定的特性(如线程)。本系列文章介绍了移植工作的众多方面。有关这个主题的更深入信息,请参见参考资料部分。


阅读(2476) | 评论(0) | 转发(0) |
0

上一篇:javascrip

下一篇:从UNIX到WINDOWS NT移植C++

给主人留下些什么吧!~~