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

Разъяснение процесса портирования программ на C/C++ из Windows в UNIX

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

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

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



04.06.2010

Перенос из Windows в UNIX

Большинство программ для Microsoft® Windows® создаются при помощи Microsoft Visual Studio®, оснащенной сложной интегрированной средой разработки (integrated development environment, IDE), которая автоматизирует почти всю работу разработчика. Также программисты Windows используют специальные программные интерфейсы (API), заголовки и расширения языка для Windows. Большинство UNIX®-систем, таких как SunOS, OpenBSD и IRIX, не поддерживают IDE, а также заголовки и расширения Windows, поэтому портирование из Windows в Unix – это весьма трудная задача. Кроме того, приложения Windows используют 16- или 32-битную архитектуру x86, в то время как в среде UNIX чаще всего используется 64-битная архитектура, а большинство версий UNIX не поддерживают набор инструкций x86. Данная статья является первой в серии из двух статей, снимающих завесу тайны с процесса портирования обычного проекта Visual C++ под управлением ОС Windows в среду g++ под управлением ОС SunOS. Проблемы, упомянутые выше, будут рассмотрены подробно.

Типы проектов C/C++ в Visual Studio

При помощи Visual C++ вы можете создать один из следующих типов проекта (одно- или многопоточных):

  • динамически подключаемая библиотека (DLL или .dll);
  • статическая библиотека (LIB или .lib);
  • исполняемый файл (.exe).

Для более сложных типов проектов используйте продукт Visual Studio .NET – при помощи этого решения можно создавать и обрабатывать множество проектов. Следующие два раздела этого документа посвящены портированию динамических и статических библиотек из Windows в UNIX.

Портирование DLL в среду UNIX

Эквивалентом библиотеки .dll в UNIX является разделяемый объект (.so), однако процесс создания файла .so значительно отличается от создания файла .dll. В листинге 1 приведен пример создания небольшой библиотеки .dll, которая содержит единственную функцию printHello, вызываемую из главной команды файла main.cpp.

Листинг 1. Файл hello.h, содержащий объявление для команды printHello
#ifdef BUILDING_DLL
  #define PRINT_API __declspec(dllexport)
#else
  #define PRINT_API __declspec(dllimport)
#endif

extern "C" PRINT_API void printHello();

В листинге 2 приведен исходный код hello.cpp.

Листинг 2. Файл hello.cpp
#include <iostream>
#include "hello.h"

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

extern "C" PRINT_API void printHello();

Если вы используете стандартный 32-битный компилятор C/C++ Microsoft для платформ 80x86 (cl), то для создания файла hello.dll будет использована следующая команда:

cl /LD  hello.cpp /DBUILDING_DLL

Параметр /LD указывает компилятору cl создать файл .dll. Он также может указывать на создание других форматов, таких как exe или .obj. Параметр /DBUILDING_DLL указывает использовать для данной компиляции макрос PRINT_API так, чтобы идентификатор printHello экспортировался из этой DLL.

В листинге 3 приведен исходный код файла main.cpp, использующий команду printHello. Подразумевается, что hello.h, hello.cpp и main.cpp находятся в одной и той же папке.

Листинг 3. Исходный код файла main, использующий команду printHello
#include "hello.h"

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

Для компиляции и привязки кода файла main используйте следующую команду:

cl main.cpp hello.lib

Беглый обзор исходного кода и полученного на выходе файла обнаруживает два важных факта. Во-первых: специфичный синтаксис Windows __declspec(dllexport)необходим для экспорта из DLL всех функций, переменных или классов. И, во-вторых: компиляция создает два файла – printHello.dll и printHello.lib. PrintHello.lib используется для связывания исходного кода файла main и заголовков UNIX для разделяемых объектов и не нуждается в синтаксисе declspec. Результатом успешной компиляции является единственный файл .so, связанный с исходным кодом файла main.

Для создания разделяемой библиотеки в среде UNIX при помощи g++ скомпилируйте все исходные файлы как перемещаемые разделяемые объекты при помощи флага --fPIC. Параметр PIC генерирует перемещаемый код. Потенциально разделяемая библиотека каждый раз при загрузке может размещаться в новой области памяти, поэтому она зависит от изменения адресов всех переменных и функций внутри библиотеки и нуждается в способе простого расчета соответствующих параметров относительно начала адресации, которые также загружаются вместе с библиотекой. Такой код называется перемещаемым и генерируется параметром -fPIC. Параметр -o используется для указания имени выходного файла, а параметр -shared создает разделяемую библиотеку, в которой допускаются неразрешимые ссылки. Для создания файла hello.so вам необходимо изменить заголовок, как показано в листинге 4.

Листинг 4. Модифицированный заголовок для hello.h с изменениями для UNIX
#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();

Для привязки разделяемой библиотеки hello.so используйте следующую команду:

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

Для создания исполняемого файла main скомпилируйте исходный код:

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

Скрытие идентификаторов в g++

Существуют два основных способа экспорта идентификаторов из библиотек DLL OC Windows. Первый метод заключается в использовании __declspec(dllexport)только для определенных элементов (например, классов, глобальных переменных или глобальных функций), экспортируемых из DLL. Второй метод заключается в использовании файла определений модулей (.def). Def-файл имеет собственный синтаксис и содержит список идентификаторов, которые необходимо экспортировать из DLL.

По умолчанию редактор связей g++ экспортирует все идентификаторы из файла .so. Это может оказаться нежелательным и сделать связывание множества DLL трудоемкой задачей. Для выбора экспортируемых идентификаторов из разделяемой библиотеки используйте механизм атрибутов g++. Например, предположим, что пользовательский код содержит два метода 'void print1();' и ' int print2(char*);', и пользователю необходимо экспортировать только print2. В листинге 5 показано, как сделать это в Windows и UNIX.

Листинг 5. Скрытие идентификаторов в g++
#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.

Обзор механизма атрибутов в g++

Подобно среде Visual Studio, которая поддерживает различный дополнительный синтаксис поверх C/C++, g++ поддерживает множество нестандартных расширений языка С. Одно из них – это механизм атрибутов g++, используемый для портирования. В предыдущем примере мы рассмотрели скрытие идентификаторов. Также атрибуты используются для задания типов функций, таких как cdecl, stdcall и fastcall в Visual C++. Во второй статье этой серии механизм атрибутов обсуждается более подробно.

Явная загрузка DLL или разделяемого объекта в среде UNIX

В ОС Windows, как правило, библиотеки .dll явно загружаются приложением Windows. Например, рассмотрим сложный редактор для ОС Windows с функциями печати. Поскольку редактор динамически загружает DLL для драйвера принтера, при первом использовании пользователь делает соответствующий запрос. Разработчики программ для Windows используют интерфейсы API, поставляемые с Visual Studio, такие как 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

#ifdef UNIX
#include <dlfcn.h>
#endif 

#include <iostream>
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.

Листинг 7. Главный файл Loadlib.cpp
#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;
  }

Путь поиска DLL в средах Windows и UNIX

В ОС Windows поиск библиотек DLL ведется в следующем порядке.

  1. Каталог, в котором находится исполняемый файл (например, notepad.exe in 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 поиск осуществляется в каталогах, указанных в переменных SHLIB_PATH, а затем в LD_LIBRARY_PATH. В ОС IBM AIX® порядок поиска разделяемых библиотек определяет переменная LIBPATH.

Портирование статических библиотек из Windows в UNIX

Объектный код статических библиотек, в отличие от динамически подключаемых библиотек, подключается при компиляции приложения и, таким образом, становится его частью. В системах UNIX статические библиотеки именуются согласно соглашению, по которому к их названию добавляется приставка lib и расширение .a. Например, библиотека Windows user.lib в UNIX, как правило, будет называться libuser.a. Для создания статических библиотек используются встроенные команды ОС ar и ranlib. В листинге 8 показано, как создать статическую библиотеку libuser.a из исходных файлов user_sqrt1.cpp и user_log1.cpp.

Листинг 8. Создание статической библиотеки в среде UNIX
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 -s]приводит к тому же результату, что и запуск ranlib.

Предварительно скомпилированные заголовки

Приложения C/C++ в Visual C++ часто используют предварительно скомпилированные заголовки. Предварительная компиляция заголовков увеличивает производительность некоторых компиляторов, таких как cl, для ускорения компиляции. Комплексные приложения часто используют файлы заголовков (.h or .hpp), являющиеся отрывками кода, которые включаются в качестве составной части в один или несколько исходных файлов. В рамках проекта файлы заголовка изменяются крайне редко. Таким образом, для ускорения компиляции эти файлы могут быть конвертированы в промежуточную форму, которая проще для последующих компиляций. В среде Visual Studio эта промежуточная форма называется файлы предварительно скомпилированных заголовков (precompiled header files, PCH).

Для примера рассмотрим файл hello.cpp, приведенный выше, в листингах 1 и 2. Включение iostream и определение макроса EXPORT_API может быть рассмотрено как использование фрагментов с неизменным кодом в рамках проекта. Таким образом, они являются хорошим примером для включения в файл заголовка. В листинге 9 показаны необходимые для этого изменения в коде.

Листинг 9. Содержимое precomp.h
#ifndef __PRECOMP_H
#define __PRECOMP_H

#include <iostream>

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

В листинге 10 показан исходный код DLL с соответствующими изменениями.

Листинг 10. Содержимое нового файла hello.cpp
#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. После того как обнаружена такая команда для включаемого файла, компилятор ищет предварительно скомпилированный заголовок во всех каталогах, начиная с каталога, в котором находится сам файл. Ведется поиск имени, указанного в команде #include, с расширением .gch. Если предварительно скомпилированный заголовок невозможно использовать, он игнорируется.

Ниже приведена команда для использования предварительно скомпилированных заголовков в 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), а также многопоточность.

Ресурсы

Научиться

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

Обсудить

Комментарии

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=494424
ArticleTitle=Перенос программ из Windows в UNIX : Часть 1. Портирование исходных текстов C/C++
publish-date=06042010