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

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

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

Rahul Kumar Kardam (rahul@syncad.com), 高级软件工程师, Synapti Computer Aided Design Pvt Ltd

Rahul Kardam 是一名高级软件开发人员,他尤其擅长开发复杂的、基于 C++ 的电子设计自动化工具,如用于硬件设计的仿真器。他在 Windows 和 UNIX 平台方面具有丰富的编程经验。Rahul 很喜欢修改开放源代码软件,并使用它作为健壮的、可伸缩代码的框架,以设计自己使用的自动化工具。



Arpan Sen, 技术主管, Synapti Computer Aided Design Pvt Ltd

Arpan Sen 是一名首席工程师,致力于开发电子设计自动化行业软件。多年以来,他一直研究多种类型的 UNIX,包括 Solaris、SunOS、HP-UX 和 IRIX,以及 Linux 和 Microsoft Windows。他感兴趣的内容包括软件性能优化技术、图像原理和并行计算。Arpan 拥有软件系统的研究生学位。


developerWorks 投稿作者

2007 年 12 月 03 日

比较和对照相关的编译器选项

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],那么在执行期间,将在程序的映像中仅保存该字符串的单个副本,且 string1string2 相等。需要说明的是,g++ 的行为正好与它相反,在缺省情况下,string1string2 相等。要在 g++ 中禁用字符串池,您必须将 -fwritable-strings 选项添加到 g++ 命令行。

使用 wchar_t

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 的大小强制规定为两个字节。

C++ 运行时类型识别 (Run Time Type Identification) 的支持

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

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

清单 2. 演示异常处理的代码片段
      #include <iostream>
      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;
        }

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

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

循环的一致性

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

清单 3. for 循环的一致性
      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 标志。

使用 g++ 属性

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

函数的调用约定

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

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

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

清单 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 个字节为单位对结构进行对齐。您可以使用 alignedg++ 属性来指定变量的对齐方式,如清单 5 中所示。

清单 5. Windows 和 UNIX 中结构成员的对齐方式
      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)));

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

Visual C++ declspec nothrow 属性

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

清单 6. Windows 和 UNIX 中的 nothrow 属性
      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++ 之间相似的内容

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

从 32 位的 Windows 移植到 64 位的 UNIX 环境时的潜在缺陷

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

数据类型大小方面的差别

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

个别数据结构的内存需求

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

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

清单 8. 正确的结构成员对齐方式
      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 的源代码。

清单 9. multithread.cpp 的源代码
#include <stdio.h>
#include <stdlib.h>

#ifdef WIN32
  #include <windows.h>
  #include <string.h>
  #include <conio.h>
  #include <process.h>
#else 
  #include <pthread.h>
#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 是该程序在两种环境中的输出。

清单 10. multithread.cpp 的输出
    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)以及实现特定的特性(如线程)。本系列文章介绍了移植工作的众多方面。有关这个主题的更深入信息,请参见参考资料部分。

参考资料

学习

获得产品和技术

  • IBM 试用软件:从 developerWorks 可直接下载这些试用软件,您可以利用它们开发您的下一个项目。

讨论

条评论

developerWorks: 登录

标有星(*)号的字段是必填字段。


需要一个 IBM ID?
忘记 IBM ID?


忘记密码?
更改您的密码

单击提交则表示您同意developerWorks 的条款和条件。 查看条款和条件

 


在您首次登录 developerWorks 时,会为您创建一份个人概要。您的个人概要中的信息(您的姓名、国家/地区,以及公司名称)是公开显示的,而且会随着您发布的任何内容一起显示,除非您选择隐藏您的公司名称。您可以随时更新您的 IBM 帐户。

所有提交的信息确保安全。

选择您的昵称



当您初次登录到 developerWorks 时,将会为您创建一份概要信息,您需要指定一个昵称。您的昵称将和您在 developerWorks 发布的内容显示在一起。

昵称长度在 3 至 31 个字符之间。 您的昵称在 developerWorks 社区中必须是唯一的,并且出于隐私保护的原因,不能是您的电子邮件地址。

标有星(*)号的字段是必填字段。

(昵称长度在 3 至 31 个字符之间)

单击提交则表示您同意developerWorks 的条款和条件。 查看条款和条件.

 


所有提交的信息确保安全。


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=AIX and UNIX
ArticleID=272499
ArticleTitle=从 Windows 移植到 UNIX,第 2 部分: 移植 C/C++ 源代码的内部探密
publish-date=12032007