В прошлом посте я запостил ссылку на документ, опсывающий некоторые тонкости, разработки портируемых программ на C++. Наконец дошли руки более подробно прочитать этот документ и я с удивлением обнаружил, что на прошлой работе с некоторыми описываемыми ситуациями мне приходилось сталкиваться. Несмотря на то, что сам язык был спроектирован как платформонезависимый, создание кроссплатформенного ПО не является таким простым, как хотелось бы. Однако обо всем по порядку...
Одной из основных проблем, при написании портируемого кода на С++, являются отличия компиляторов. Несмотря на то, что сам язык описан довольно детально в стандарте, трактовки самого стандарта в станах компиляторостроителей иногда различаются. Кроме того, C++ часто ругают (вполне заслуженно) за сложность, особенно когда дело касается шаблонов. Эта сложность по определению ну никак не может облегчить жизнь как разработчикам компиляторов, так и "обычным" программистам, которые эти компиляторы используют.
Однако не только сложность языка источник проблем. Стандарт - один из этих источников. Описывая поведение большинства (но не всех) элементов языка, он, вместе с тем, оставляет некоторые из них implementation-defined, unspecified или undefined. В тексте документа приводится такой пример:
И оговаривается, что на разных платформах результат выполнения программы будет разным
Что характерно, - оба результата верны, потому такие моменты следует учитывать и не полагаться на ваш любимый компилятор, поскольку даже следующая его версия может иметь отличный behaviour.
"Расширения" языка, которыми зачастую грешат разработчики из обоих лагерей (MS и GNU), тоже мешают писать портируемый код. Особенно "грешат" на майкрософтовский компилер, у которого есть опция для отключения подобного рода расширений. Заодно автоматически у вас пропадает возможность использовать Platform SDK, так как его заголовочные файлы используют эти расширения. Меня самого когда-то возмущала необходимость использовать тип BOOL, вместо bool (сейчас уже правда не возмущает) и я до сих пор иногда забываю ставить новую строчку в конце файла, с раздражением наблюдая ворнинги компилятора.
Типы. На разных платформах фундаментальные типы могут иметь разный размер. Поэтому если в вашей программе необходимо 32 битное беззнаковое целое, то проще всего, наверное, определить свой тип в заголовочном файле сразу, чем потом по всему коду выискивать и менять unsigned на myint32_t. В стандарте языка С ISO/IEC 988:1999 к стати есть типы int8_t, int16_t, int32_t, int64_t, uint8_t, uint16_t, uint32_t, uint64_t, определенные в stdint.h, однако это тот случай, где С++ не обратно совместим с С. Может в следующем стандарте поправят. Там вроде как намечается соласовать стандарты. Про типы с плавающей запятой вообще отдельная сказка, ее лучше почитать в оригинале.
Порядок байт. Определяет как размещаются байты в памяти - старший->младший или младший->старший. Это потенциальная проблема, если есть необходимость производить обмент бинарными данными между системами с различным порядком байт.
Выравнивание данных. На многих архитектурах данные обычно выравниваются на границу слова, что иногда может приводить к проблемам. В частности, приводится следующий пример:
и говорится, что если адрес на который указывает pData не выравнен на границу 4х байт, то на другой архитектуре это может привести к ошибке времени выполнения.
Там где дело касается API операционных систем дела обстоят еще хуже - совсем недавно в одном из блогов наткнулся на замечания по поводу портирования софта linux -> windows (блог к стати мне понравился, жаль ссылку потерял). Для меня, к стати, оказалась сюрпризом информация о том, что Windows NT поддерживала POSIX, а с приходом WindowsXP ее убрали.
В общем, проблем вполне достаточно. В следующей части напишу про некоторые решения, призванные прийти на помощь в борьбе за чистое небо над головой программиста... Тфу! ну в общем вы поняли ))
Одной из основных проблем, при написании портируемого кода на С++, являются отличия компиляторов. Несмотря на то, что сам язык описан довольно детально в стандарте, трактовки самого стандарта в станах компиляторостроителей иногда различаются. Кроме того, C++ часто ругают (вполне заслуженно) за сложность, особенно когда дело касается шаблонов. Эта сложность по определению ну никак не может облегчить жизнь как разработчикам компиляторов, так и "обычным" программистам, которые эти компиляторы используют.
Однако не только сложность языка источник проблем. Стандарт - один из этих источников. Описывая поведение большинства (но не всех) элементов языка, он, вместе с тем, оставляет некоторые из них implementation-defined, unspecified или undefined. В тексте документа приводится такой пример:
void f(int a, int b, int c)
{
cout << "a = " << a << ", b = " << b << ", c = " << c << endl;
}
int main(int argc, char** argv)
{
int i = 0;
f(i++, i++, i++);
return 0;
}
И оговаривается, что на разных платформах результат выполнения программы будет разным
For example, on Mac OS X, using GCC 3.3, the program fragment yields the following output
a = 0, b = 1, c = 2
This is what one would intuitively expect, assuming left-to-right argument evaluation. Compiled with HP ANSI C++ A.03.57 on HP-UX 11.11, the result is the same. However,when compiled with Compaq C++ 6.5 on HP Tru64 5.1, the program yields:
a = 0, b = 0, c = 0
which may be surprising
Что характерно, - оба результата верны, потому такие моменты следует учитывать и не полагаться на ваш любимый компилятор, поскольку даже следующая его версия может иметь отличный behaviour.
"Расширения" языка, которыми зачастую грешат разработчики из обоих лагерей (MS и GNU), тоже мешают писать портируемый код. Особенно "грешат" на майкрософтовский компилер, у которого есть опция для отключения подобного рода расширений. Заодно автоматически у вас пропадает возможность использовать Platform SDK, так как его заголовочные файлы используют эти расширения. Меня самого когда-то возмущала необходимость использовать тип BOOL, вместо bool (сейчас уже правда не возмущает) и я до сих пор иногда забываю ставить новую строчку в конце файла, с раздражением наблюдая ворнинги компилятора.
Типы. На разных платформах фундаментальные типы могут иметь разный размер. Поэтому если в вашей программе необходимо 32 битное беззнаковое целое, то проще всего, наверное, определить свой тип в заголовочном файле сразу, чем потом по всему коду выискивать и менять unsigned на myint32_t. В стандарте языка С ISO/IEC 988:1999 к стати есть типы int8_t, int16_t, int32_t, int64_t, uint8_t, uint16_t, uint32_t, uint64_t, определенные в stdint.h, однако это тот случай, где С++ не обратно совместим с С. Может в следующем стандарте поправят. Там вроде как намечается соласовать стандарты. Про типы с плавающей запятой вообще отдельная сказка, ее лучше почитать в оригинале.
Порядок байт. Определяет как размещаются байты в памяти - старший->младший или младший->старший. Это потенциальная проблема, если есть необходимость производить обмент бинарными данными между системами с различным порядком байт.
Выравнивание данных. На многих архитектурах данные обычно выравниваются на границу слова, что иногда может приводить к проблемам. В частности, приводится следующий пример:
struct Header
{
UInt32 size;
UInt32 checksum;
};
...
void handleData(void* pData)
{
Header* pHeader = reinterpret_cast<Header*>(pData);
for (int i = 0; i < pHeader->size; ++i)
...
}
и говорится, что если адрес на который указывает pData не выравнен на границу 4х байт, то на другой архитектуре это может привести к ошибке времени выполнения.
Там где дело касается API операционных систем дела обстоят еще хуже - совсем недавно в одном из блогов наткнулся на замечания по поводу портирования софта linux -> windows (блог к стати мне понравился, жаль ссылку потерял). Для меня, к стати, оказалась сюрпризом информация о том, что Windows NT поддерживала POSIX, а с приходом WindowsXP ее убрали.
В общем, проблем вполне достаточно. В следующей части напишу про некоторые решения, призванные прийти на помощь в борьбе за чистое небо над головой программиста... Тфу! ну в общем вы поняли ))
Читаем Зуева ;-)
ReplyDeletehttp://zouev.blogspot.com/2008/04/blog-post_28.html
да, причем уже довольно давно )). Однако это тут причем?
ReplyDelete"Порядок байт. Определяет как размещаются байты в памяти"
ReplyDeleteА разве не биты?
2victor: нет, насколько мне известно именно байты. Вот выдержка:
ReplyDeleteInteger values made up of more than one byte can be stored in two ways. If a 16-bit (2 byte) integer is taken as an example, it can be stored either with the low-order byte at the starting address, or with the high-order byte at the starting address. The first alternative is known as little-endian byte order, the second is known as big-endian byte order.
Создатели Интернет с этим тоже столкнулись:
The inventors of the Internet protocols faced the same problem and they solved it by specifying a network byte order, which all protocol data sent over the network must obey. Network byte order is defined to be big-endian, and every operating system supporting the Internet protocols provides functions for converting values from host byte order to network byte order and back.
По поводу различных ++i++.
ReplyDeleteЕсли программист такое допускает - он Сам Себ Злобный Буратина и это его проблемы, что такое совсем не портируется.
По поводу uint32_t и прочих.
Да, по своему замечательный набор типов. В свое время перешел на их использование и сейчас стараюсь использовать везде. Но и тут есть свои проблемы. Так, например, Linux, FreeBSD 4.x и FreeBSD более старших версий определяют эти типы в разных файлах.
А уж когда дело доходит до сетевой подсистемы - там совсем тьма.
Ну и еще много замечательных сюрпризов доставляют дистростроители. Заголовочные файлы и библиотеки понатыкивают куда только придумается. И, обычно, если это не /usr/lib - в кеш компоновщика они не попадают. Отчасти спасает наличие pkg_config и прочих *_config, но они не во всех дистрибутивах есть.
Так что я больше не удивляюсь размерам и скорости работы Autotools
я в последнее время стараюсь писать только кросс-платформенно, благо есть где тестировать - linux, macosx, windows, freebsd.
ReplyDeleteсейчас получается следующий набор - cmake для сборки и конфигурации софта, boost.asio для нетворкинга, boost.threads - треды и локи, ну и там дальше - портабельная работа с файлами и путями, interprocess - memory mapping и т.п.
P.S. все не соберусь написать статью с обзором бустовских библиотек на тему кросс-платформенного программирования
CMake, boost...
ReplyDeleteЭто все классно. Но я постоянно сталкиваюсь с тем, что дорогие и уважаемые наши товарищи админы не в состоянии даже Expat поставить.
Именно по этому стараюсь обходиться минимумом внешних библиотек и утилит. Т.к. наш продук пока включен только в репозитарий ALT Linux, а самим заниматься мейнтейнерством некогда.
2Anonymous:
ReplyDeleteЕсли программист такое допускает - он Сам Себ Злобный Буратина и это его проблемы, что такое совсем не портируется.
Это верно в том случае, если портировать придется ему. Однако зачастую портирование осуществляется кем-то другим.
2Alex Ott:
все не соберусь написать статью с обзором бустовских библиотек на тему кросс-платформенного программирования
очень интересно было бы почитать
2Anonymous:
Но я постоянно сталкиваюсь с тем, что дорогие и уважаемые наши товарищи админы не в состоянии даже Expat поставить.Именно по этому стараюсь обходиться минимумом внешних библиотек и утилит. Т.к. наш продук пока включен только в репозитарий ALT Linux, а самим заниматься мейнтейнерством некогда.
если честно, то я далек от того как осуществляется управление пакетами в Альте, но по-моему там rpm и пакеты бинарные, получается админам влом работать с зависимостями при установке пакетов что-ли? Что за продукт-то? Интересно как-никак ))
2anonymous: а причем тут админы?
ReplyDeleteу меня весь софт для разработки ставится в домашний каталог, а продукт я отдаю часто в виде статической сборки
При том, что целевая аудитория нашего софта - админы и ISP. Делать бинарные сборки для десятка дистрибутивов - это куча времени, которого как обычно нет. Тем более, даже если делать бинарные сборки - остаются зависимости по Shared Libraries, которые конечному пользователю приходится удовлетворять.
ReplyDelete2Yuriy Volkov
ReplyDeleteНет, как раз в Альте проблем нету - там зависимости разрешаются пакетным менеджером, а компиляцию проводит мейнтейнер.
Проблемы в других дистрибутивах и системах. Так, например, заголовочные файлы mysql иногда находятся в /usr/include, иногда - в /usr/include/mysql. Иногда чуваки ставят его из исходников - тогда вобще в /usr/local/include.
Те-же проблемы и с библиотеками...
А продукт - Stargazer - Open Source Billing System
К стати, еще интересные штуки всплывают, когда приходится поддерживать несколько версий компиляторов (особенно старые версии).
2madf:
ReplyDeleteдаже если делать бинарные сборки - остаются зависимости по Shared Libraries, которые конечному пользователю приходится удовлетворять
я почему-то всегда думал, что задача разрешения зависимостей - это работа пакетного менеджера, в том числе и зависимостей от shared libraries.
ну а по поводу того, что хидеры в разных местах, насколько я помню, то компилеру можно указать где их искать.
про старые компиляторы знаю, - приходилось сталкиваться с gcc 2.95 и egcs (из Red Hat 7.3). Их целесообразно использовать только для поддержки legacy кода, так как он, обычно новыми компиляторами переваривается плохо. Кроме того новый компилятор - это исправленные баги, и если софт неправильно работает с вероятностью 0,01% например, то вполне может быть, что компилятор просто сгенерировал некачественный код. В частности, если мне не изменяет память, то у gcc 2.95 (или это был еще egcs), была проблема с вызовами виртуальных деструкторов глобальных объектов при генерировании исключения. Было что- то еще, но я уже не помню. В конечном итоге я делал статическую сборку своего проекта при помощи 4-й версии gcc, так как устал бороться с багами старых компиляторов.
1. По поводу пакетных менеджеров - ты совершенно прав. Но мы пока есть только в Альте, а аудитория достаточно большая.
ReplyDelete2. По поводу заголовочных файлов - тоже прав. Но чтобы сказать компилятору где что искать - нужно сперва самому найти. Тут бы и помогли различные *_config, но, оказывается, нои есть не во всех системах.
3. 2.95 перестали поддерживать после 31 декабря 2007 года - последнего релиза :) Бинарных сборок на 10 систем нет ни возможности ни желания делать.
Больше всего удручает то, что все эти мелкие "хаки" для поддержки разных версий компиляторов и обхода их ограничений и багов приходится поддерживать годами.
не знаю, я бы на вашем месте определил целевую аудиторию (админы это конечно хорошо, но там корпоративный сектор или средний бизнес звучит более как-то более конкретно) ну и выбрать наиболее распространенные системы в этом секторе. Кроме того, бинарные сборки как мне кажеться уменьшают adoption time. По поводу отсутствия возможности делать эти сборки тоже непонятно - насетапить виртуальных машин не так уж и долго, да - придется изучить процесс сборки пакета под конкретную систему, да - на это необходимо некоторое время, но это все же возможно. С отсутствием желания тоже можно справиться. Когда пакет приобретет популярность можно будет предоставлять саппорт на коммерческой основе для корпоративного сектора. В общем вот так как-то.
ReplyDeleteНасчёт переносимых целочисленных типов фиксированного размера: их есть в Boost:
ReplyDeletehttp://www.boost.org/doc/libs/1_35_0/libs/integer/cstdint.htm
2Yuriy Volkov:
ReplyDeleteИсторически так сложилось, что проект - открытый. А целевая аудитория плавненько расширялась от мелких сеток, до крупных домашних сетей и ISP.
С другой стороны - это не основной наш источник дохода, по этому занимаемся мы им эпизодически. Практически "Just for fun". А сборка пакетов уже хотя-бы для 3 разных систем (Ubuntu, Debian и Gentoo) вызывает массу проблем. Сегодня пол дня убил на дружественную помощь ALT Linux в написании спеки для создания RPM-ки - у них там требования достаточно высокие. Особенно по части зависимостей от библиотек. В частности, это единственный из известных мне дистрибутивов, который по умолчанию ставит --as-needed для компоновщика. Я уже не говорю об уровне предупреждений компилятора и различного рода проверках. Если самим делать бинари - нужно убить неделю на все это. А с учетом загрузки по другим проектам - это все растянется на неопределенный срок.
Программеру - программерово, мейнтейнеру - мейнтейнерово.
Пакет уже имеет немалую популярность (Украина, Россия, Белорусия - это то что я знаю и отслеживаю) и поддержку на комерческой основе мы тоже предоставляем. Но серьезно этим заниматься, думаю, будем не скоро. Мы все-таки программисты, а не бизнесмены :) Уже несколько раз поднимался вопрос о закритии исходников и переходе на полностью комерческую основу. В прошлом году бродили мысли о поддержке двух веток: комерческой и свободной. В результате все осталось как есть.
2madf: ну что ж, понятно, желаю успехов вам в нелегких программистских буднях ;-)
ReplyDelete