Перенос программ из Windows в UNIX : Часть 2. Портирование исходных текстов C/C++ в деталях

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

В первой части этой статьи рассматривались основные типы проектов C/C++ среды Microsoft® Visual Studio® и было дано общее описание процесса портирования динамически подключаемых и статических библиотек на платформу UNIX®. Во второй части мы изучим некоторые опции компиляции, используемые при создании проектов Visual C++, и их эквиваленты g++ в среде UNIX, познакомимся ближе с механизмом атрибутов g++,применяемых при портировании, и рассмотрим некоторые типичные проблемы, связанные с портированием из 32-битной среды Windows® в 64-битную среду UNIX. Статья завершается обзором концепции портирования многопоточных приложений и примером проекта такого портирования.

Рахул Кардам, ведущий разработчик программного обеспечения, Synapti Computer Aided Design Pvt Ltd

Рахул Кардам (Rahul Kardam) – ведущий разработчик программного обеспечения, специализирующийся на сложных инструментах автоматизации электронного проектирования, построенных на C++, таких как системы, моделирующие технические объекты. Он обладает опытом программирования под платформами Windows и UNIX. Рахул получает удовольствие от правок программного обеспечения с открытым исходным кодом, которое он использует как основу для разработки устойчивого и масштабируемого кода инструментов автоматизации проектирования, над которыми он работает.



Арпан Сен, технический директор, Synapti Computer Aided Design Pvt Ltd

Арпан Сен (Arpan Sen) – ведущий инженер, работающий над разработкой программного обеспечения в области автоматизации электронного проектирования. На протяжении нескольких лет он работал над некоторыми функциями UNIX, в том числе Solaris, SunOS, HP-UX и IRIX, а также Linux и Microsoft Windows. Он проявляет живой интерес к методикам оптимизации производительности программного обеспечения, теории графов и параллельным вычислениям. Арпан является аспирантов в области программных систем.



14.07.2010

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

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

Ресурсы

Научиться

Получить продукты и технологии

Обсудить

Комментарии

developerWorks: Войти

Обязательные поля отмечены звездочкой (*).


Нужен IBM ID?
Забыли Ваш IBM ID?


Забыли Ваш пароль?
Изменить пароль

Нажимая Отправить, Вы принимаете Условия использования developerWorks.

 


Профиль создается, когда вы первый раз заходите в developerWorks. Информация в вашем профиле (имя, страна / регион, название компании) отображается для всех пользователей и будет сопровождать любой опубликованный вами контент пока вы специально не укажите скрыть название вашей компании. Вы можете обновить ваш IBM аккаунт в любое время.

Вся введенная информация защищена.

Выберите имя, которое будет отображаться на экране



При первом входе в developerWorks для Вас будет создан профиль и Вам нужно будет выбрать Отображаемое имя. Оно будет выводиться рядом с контентом, опубликованным Вами в developerWorks.

Отображаемое имя должно иметь длину от 3 символов до 31 символа. Ваше Имя в системе должно быть уникальным. В качестве имени по соображениям приватности нельзя использовать контактный e-mail.

Обязательные поля отмечены звездочкой (*).

(Отображаемое имя должно иметь длину от 3 символов до 31 символа.)

Нажимая Отправить, Вы принимаете Условия использования developerWorks.

 


Вся введенная информация защищена.


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