Содержание


Перенос программ из Windows в UNIX

Часть 2. Портирование исходных текстов C/C++ в деталях

От атрибутов компилятора к типичным проблемам

Comments

Серия контента:

Этот контент является частью # из серии # статей: Перенос программ из Windows в UNIX

Следите за выходом новых статей этой серии.

Этот контент является частью серии:Перенос программ из Windows в UNIX

Следите за выходом новых статей этой серии.

Сравнение и отличия опций компиляторов

Оба компилятора, Visual C++ cl и GNU g++, поддерживают несколько опций. Хотя вы можете использовать cl в качестве самостоятельного инструмента компиляции, Visual C++ предоставляет удобную интегрированную среду разработки (integrated development environment, IDE) для настройки опций компилятора. Программное обеспечение, созданное при помощи Visual Studio®, часто использует специфичные функции компилятора и платформы, которые управляются при помощи опций компилятора и компоновщика. При портировании исходных кодов между различными платформами при помощи различных компиляторов и инструментов важно знать опции компилятора. В этом разделе рассматриваются некоторые из наиболее полезных опций компилятора.

Включение пула строк

Для примера рассмотрим следующий фрагмент кода:

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

Если в Visual C++ включено использование пула строк [/GF], то во время работы программы в ее образе будет храниться единственная копия строки, а string1 будет равна string2. Для отключения пула строк в g++ необходимо добавить к команде опцию -fwritable-strings.

Использование wchar_t

Стандарт C++ определяет тип широких символов wchar_t. Если компилятору передана опция /Zc:wchar_t, то Visual C++ использует в качестве встроенного типа данных wchar_t. В противном случае необходимо добавлять заголовки, зависящие от реализации, такие как windows.h или стандартные заголовки, такие как wchar.h. g++ поддерживает wchar_t по умолчанию и не требует использования специальных заголовков. Обратите внимание, что размер wchar_t различается для разных платформ. Для g++ можно использовать опцию -fshort-wchar g++, чтобы принудительно задать для wchar_t размер 2 байта.

Поддержка RTTI для С++

Если в исходном коде не используются операторы dynamic_cast или typeid, можно отключить поддержку информации о типах в процессе исполнения (Run Time Type Identification, RTTI). По умолчанию в Visual Studio 2005 RTTI включена (при помощи опции /GR). Для отключения RTTI в среде Visual Studio используйте опцию /GR-. Отключение RTTI может способствовать уменьшению размера исполняемых файлов. Обратите внимание, что отключение RTTI для кода, содержащего dynamic_cast или typeid, может привести к нежелательному эффекту, в том числе к сбоям. Рассмотрим фрагмент кода в листинге 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;
        }

Чтобы скомпилировать этот фрагмент кода при помощи компилятора cl без использования среды разработки Visual Studio, необходимо явно указать опцию /GR. В отличие от cl, компилятор g++ не требует указания дополнительных опций для включения RTTI. Тем не менее, как и опция /GR- в Visual Studio, в g++ существует опция -fno-rtti, которая явно отключает RTTI. Компиляция данного фрагмента при помощи g++ с опцией -fno-rtti приводит к появлению сообщений об ошибках компиляции. Хотя cl компилирует этот код без опции /GR, полученный исполняемый файл работает некорректно.

Обработка исключений

Для обработки исключений в cl используйте опции /GX или /EHsc. Без какого-либо из этих опций код, использующий try и catch, может продолжать работу, а система не будет вызывать деструкторы локальных объектов до оператора throw. Обработка исключений уменьшает производительность. Поскольку компилятор генерирует код, в котором описывается каждая функция C++, это приводит к увеличению размеров исполняемых файлов и замедлению их работы. Возможно, для конкретного проекта такое падение производительности неприемлемо, и вам понадобится отключить эту функцию. Для отключения обработки исключений вам необходимо удалить все блоки обработчиков try и catch из исходного кода и скомпилировать его с опцией /GX-. По умолчанию в g++ включена обработка исключений. Для ее отключения используйте g++ с опцией -fno-exceptions. Обратите внимание, что использование этой опции для исходного кода, содержащего операторы try, catch и throw, может привести к ошибкам компиляции. Вам необходимо вручную удалить все блоки обработчиков try и catch из исходного кода перед использованием этой опции в 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;
        }

Ниже приведены выдачи компиляторов cl и g++ с использованием и без использования опций, описанных выше в данном разделе:

  • cl с опцией /GX: Destroying A Caught!
  • cl без опции /GX: Caught!
  • g++ без опции -fno-exceptions: Destroying A Caught!
  • g++ с опцией -fno-exceptions: Compile time error

Согласование циклов

Пример согласования циклов приведен в листинге 3.

Листинг 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 приводит к ошибке компиляции. В отличие от cl, поведение g++ является более точным и выдает для этого теста следующую ошибку:

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

Чтобы исправить эту ошибку, во время компиляции вы можете использовать флаг -fno-for-scope flag.

Использование атрибутов g++

Оба компилятора Visual C++ и GNU g++ поддерживают нестандартные расширения языка. Механизм атрибутов g++ используется при портировании функций, зависящих от платформы в коде Visual C++. Синтаксис атрибутов представляет собой следующую форму: __attribute__ ((attribute-list)), где attribute list – список атрибутов, разделенных запятыми. Отдельные элементы списка атрибутов представляют собой либо слово, либо слово с опциями, заключенными в скобки. В этом разделе рассматривается возможное использование атрибутов при портировании.

Соглашение вызова функций

Вы можете использовать специальные ключевые слова Visual Studio, такие как __cdecl, __stdcall и __fastcall, чтобы обозначать соглашения вызова для функций компилятора. Подробности приведены в таблице 1.

Таблица 1. Соглашения о вызовах в среде Windows
Соглашение о вызовахИспользующаяся семантика
__cdecl (cl опция: /Gd)Аргументы для вызываемой функции размещаются в стеке справа налево. После однократного выполнения вызываемая функция извлекает аргументы из стека.
__stdcall (cl опция: /Gz)Аргументы для вызываемой функции размещаются в стеке справа налево. После однократного выполнения вызываемая функция извлекает аргументы из стека.
__fastcall (cl опция: /Gr)Первые два аргумента помещаются в регистрах ECX и EDX , а все остальные размещаются справа налево. После выполнения вызываемая функция очищает стек.

В g++ используются соответствующие атрибуты cdecl, stdcall и fastcall. В листинге 4 показана тонкая разница между способами объявления атрибутов в Windows® и UNIX®.

Листинг 4. Способы объявления атрибутов в Windows и UNIX
      Объявление в Visual C++:
      double __stdcall compute(double d1, double d2);

      Объявление в g++:
      double __attribute__((stdcall)) compute(double d1, double d2);

Выравнивание данных

Опция /Zpn управляет выравниванием данных в памяти. Например, /Zp8 выравнивает данные в пределах 8 байтов (является значением по умолчанию), а /Zp16 выравнивает данные в пределах 16 байтов. Вы можете использовать атрибут alignedg++ для указания переменных выравнивания, как показано в листинге 5.

Листинг 5. Выравнивание данных в Windows и UNIX
      Способ объявления Visual C++ при помощи опции /Zp8:
      struct T1 { int n1; double d1;};

      Способ объявления g++:
      struct T1 { int n1; double d1;}  __attribute__((aligned(8)));

Однако эффективность выровненных атрибутов ограничивается возможностями компоновщика. Во многих системах компоновщик способен выровнять переменные до определенной величины.

Атрибут declspec nothrow Visual C++

Этот атрибут указывает компилятору, что функция, объявленная при помощи данного атрибута, и последующие вызванные ею функции не генерируют исключения. Использование этой функции является оптимизацией, которая сокращает общий размер кода, так как по умолчанию, даже если код не генерирует исключения, cl продолжает генерировать множество подробной информации для исходных текстов C++. Для той же цели в g++ вы можете использовать атрибут nothrow, как показано в листинге 6.

Листинг 6. Атрибут nothrow в Windows и UNIX
      Способ объявления Visual C++:
      double __declspec(nothrow) sqrt(double d1);

      Способ объявления g++:
      double __attribute__((nothrow)) sqrt(double d1);

Для портирования больше подходит использование стандартного способа: double sqrt(double d1) throw ();.

Параллели между Visual C++ и g++

В отличие от предыдущих примеров, существует несколько параллелей между схемами атрибутов Visual C++ и g++. Например, атрибуты noinline, noreturn, deprecated и naked используются в обоих компиляторах.

Возможные проблемы при портировании из 32-битной среды Windows в 64-битную среду UNIX

Код C++, созданный в системах Win32, основывается на модели ILP32, в которой int, long и типы указателей являются 32-битными. Системы UNIX основываются на модели LP64, в которой long и типы указателей являются 64-битными, но int остается 32-битным. Это различие является основной причиной проблем с кодом. В этом разделе приведен краткий обзор двух основных проблем, с которыми вы можете столкнуться из-за этих различий. Портирование из 32-битной в 64-битную среду заслуживает отдельного изучения. За дополнительной информацией по этой теме обратитесь к разделу Ресурсы.

Различие между размерами типов данных

Здравый смысл подсказывает использовать типы данных, являющиеся одинаковыми в обеих моделях ILP32 и LP64. В общем случае вам необходимо, насколько это возможно, избегать данных long и pointer. Так же, как правило, следует использовать типы данных, определенные в стандартном файле заголовка sys/types.h, но размер отдельных типов данных в этом файле, таких как ptrdiff_t, size_t и им подобных, различается для 32- и 64-битной моделей, и вам необходимо быть внимательными при их использовании.

Требования к памяти для отдельных структур данных

Требования к памяти для отдельных структур данных могут отличаться в зависимости от способа упаковки, используемого компилятором. Для примера рассмотрим фрагмент кода в листинге 7.

Листинг 7. Неверное выравнивание данных
      struct s { 
                int var1;  // пропуск между var1 и var2 
                long var2;
                int var3; // пропуск между var3 и ptr1
                char* ptr1;
             };
      // sizeof(s) = 32 bytes

В модели LP64 типы long и pointer выравниваются в пределах 64 битов. Также размер структуры выравнивается по размеру ее наибольшего элемента. В этом примере структура s выравнивается в пределах 8 байтов и, таким образом, является s.var2 переменной. Это вызывает пропуски в структуре и, как следствие, повышенный расход памяти. Правильный способ выравнивания показан в листинге 8, он делает размер структуры равным 24 байтам.

Листинг 8. Правильное выравнивание данных
      struct s { 
                int var1;  
                int var3;
                long var2;
                char* ptr1;
             };
      // sizeof(s) = 24 bytes

Портирование многопоточных приложений

Технически поток является независимым потоком инструкций, между которыми может переключаться операционная система для выполнения. В обеих средах поток является частью процесса и использует его ресурсы. Он также имеет собственный управляющий поток на время, пока существует родительский процесс и пока он обслуживается операционной системой. Он может разделять ресурсы процесса с другими потоками, действующими независимо (или в зависимости) друг от друга, и прекращается вместе с прекращением родительского процесса. Здесь приведен обзор некоторых типичных интерфейсов прикладного программирования (API), которые вы можете использовать для превращения проекта в многопоточный как в среде Windows, так и UNIX. Предпочтительным интерфейсом являются команды C, которые, в отличие от интерфейсов API WIN32, соответствуют стандарту Portable Operating System Interface (POSIX) благодаря их простоте и четкости.

Примечание. Из-за ограничения объема статьи мы не можем подробно описать другие способы создания подобных приложений.

Создание потока

Windows использует API _beginthread из функций библиотеки этапа исполнения С. Существуют другие API Win32 для создания потоков, но, забегая вперед, скажем, что вы будете иметь дело только с функциями библиотеки этапа исполнения С. Как следует из ее имени, функция _beginthread() создает поток, который выполняет команду, где в качестве первого аргумента используется указатель для этой команды. Эта команда использует соглашение об объявлении вызовов С __cdecl и возвращает void. Когда поток возвращает эту команду, он удаляется.

В UNIX то же самое достигается использованием функции pthread_create(). Подкоманда pthread_create() возвращает идентификатор нового потока, использующийся в качестве его аргумента. Вызывающий может использовать этот идентификатор для выполнения различных операций с потоком. Проверьте этот идентификатор, чтобы убедиться в том, что данный поток существует.

Уничтожение потока

Функция _endthread уничтожает поток, созданный функцией _beginthread(). Потоки уничтожаются автоматически, когда заканчивается их последовательное выполнение. Функция _endthread() используется для самостоятельного уничтожения потока согласно условию.

В UNIX то же самое достигается использованием функции pthread_exit(). Эта функция завершает поток, если нормальное последовательное выполнение не было завершено. Если функция main() завершается раньше, чем созданные ею потоки, при помощи pthread_exit(), то другие потоки продолжают выполняться. В противном случае они автоматически завершаются вместе с main().

Синхронизация в потоке

Чтобы добиться синхронизации, вы можете использовать мьютексы (mutexes). В 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;             // Количество запущенных потоков 

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

#ifdef WIN32
void InitWinApp()
  {
      
  /* Создание мьютекса и сброс счетчика потоков. */
  mutexObject = CreateMutex( NULL, FALSE, NULL );   /* Очищено */
  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();
  }
Листинг 9. Исходный код multithread.cpp
void ShutDown() 
  {
  while ( threadsStarted > 0 )
    {
    ReleaseMutex( mutexObject ); /* Уничтожение потока. */
    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];

  /* Создание независимых потоков, каждый из которых будет выполнять functionC */

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

  // Дожидаемся завершения работы потоков, в противном случае 
  // завершение главной программы будет завершать все порожденные ею потоки
  for(;count >= 0;count--)
    { 
    pthread_join( threads[count], NULL);
    }
  // Примечание: Для явного удаления потока может использоваться функция pthread_exit() 
  // но поскольку поток уничтожается автоматически после выполнения, мы не
  // выполняли явных вызовов pthread_exit(); 
  exit(0);
  }

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

#endif

Мы скомпилировали исходный код multithread.cpp в среде Visual Studio Toolkit 2003 и Microsoft Windows 2000 Service Pack 4 при помощи следующей команды:

    cl multithread.cpp /DWIN32 /DMT /TP

Мы также скомпилировали его на платформе UNIX при помощи компилятора g++ версии 3.4.4 при помощи следующей команды:

    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 и специфическими функциями реализации, такими как потоки. Данная серия статей служит введением во множество аспектов портирования. За более подробной информацией по этой теме обратитесь к разделу Ресурсы.


Ресурсы для скачивания


Похожие темы


Комментарии

Войдите или зарегистрируйтесь для того чтобы оставлять комментарии или подписаться на них.

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=40
Zone=AIX и UNIX
ArticleID=500415
ArticleTitle=Перенос программ из Windows в UNIX: Часть 2. Портирование исходных текстов C/C++ в деталях
publish-date=07142010