ПРОГРАММИРОВАНИЕ    НА    V I S U A L   C + +
РАССЫЛКА САЙТА       
RSDN.RU  

    Выпуск No. 61 от 27 января 2002 г.

РАССЫЛКА ЯВЛЯЕТСЯ ЧАСТЬЮ ПРОЕКТА RSDN , НА САЙТЕ КОТОРОГО ВСЕГДА МОЖНО НАЙТИ ВСЮ НЕОБХОДИМУЮ РАЗРАБОТЧИКУ ИНФОРМАЦИЮ, СТАТЬИ, ФОРУМЫ, РЕСУРСЫ, ПОЛНЫЙ АРХИВ ПРЕДЫДУЩИХ ВЫПУСКОВ РАССЫЛКИ И МНОГОЕ ДРУГОЕ.

Добрый день, дорогие друзья!


 НОВОСТИ

Сегодня я чрезвычайно рад сообщить вам отличную новость - появился новый совместный проект сайтов www.rsdn.ru, delphi.mastak.ru и www.optim.ru - профессиональный журнал для программистов RSDN Magazine.

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

В журнале вы найдёте статьи самой различной тематики, ответы на вопросы, а на прилагаемом к нему компакт-диске - полезные утилиты, компоненты (в форматах ActiveX, Delphi, .Net) и многое другое. Кроме этого, в состав компакт-диска будут включены различные SDK, такие как Platform SDK, .Net SDK и т.п.

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

Тематика публикаций журнала будет охватывать:

  • Технологии (COM, Java, .Net, CORBA, DirectX, OpenGL и пр.)
  • Алгоритмы и структуры данных
  • Различные API (Win32, GDI+, и т.п)
  • Методологии организации процесса программирования
  • Инструментальные средства и средства разработки
  • Библиотеки (VCL, MFC, STL, ATL, и т.п.)
  • И разумеется постоянные обзоры новинок и перспективных направлений в IT индустрии

Регулярный выпуск журнала начнется со 2 полугодия 2002 года. Сигнальный номер RSDN Magazine выйдет в свет в 1 квартале 2002 года. Периодичность выхода на начальном этапе - 1 раз в 2 месяца. В дальнейшем планируется переход на ежемесячный выпуск. Примерный объем журнала - около 100 страниц формата A4. Ориентировочная цена номера с компакт-диском - около 100 руб.

Начиная со второй половины 2002 года журнал будет распространяться по подписке через Роспечать и альтернативные агентства распространения. Индекс по каталогу "Роспечать" - 81263.

Сигнальный номер журнала не будет доступен по подписке через Роспечать и альтернативные агентства распространения. Поэтому велика вероятность, что вы не увидите его в продаже. Но все желающие его получить могут заказать журнал прямо сейчас, заполнив соответствующую форму. Журнал будет доставлен вам по почте.

Стоимость журнала с доставкой:

  • по России - 100 руб.
  • в страны СНГ и Балтии- 170 руб.
  • в страны дальнего зарубежья - 250 руб.

Реквизиты, по которым необходимо произвести платеж, вы найдете здесь.

ПРИМЕЧАНИЕ
Единственной проблемой при подписке за пределами РФ является оплата. С доставкой проблем нет. В республиках бывшего СССР обычно можно заплатить через почту или сбербанк. Иностранцам придется (пока) искать собственный путь для оплаты. Например, можно оплатить подписку через знакомых (друзей, родственников) живущих в России.

Мы надеемся, что вас заинтересовало это новое издание. Мы же заинтересованы в том, чтобы сделать журнал как можно более интересным для вас. Направляйте любые предложения по адресу mag@rsdn.ru.


 CТАТЬЯ

Публикуемая в этом выпуске статья взята из пре-первого (#0) номера журнала.

Анатомия C Run-Time
или
Как сделать программу немного меньшего размера

Поводом к написанию этой статьи послужили частые обсуждения в Web-конференциях следующего вопроса:

"Я создал проект с использованием библиотеки ATL. Некоторое время он прекрасно компилировался как в Debug-, так и в Release-версии. Затем, после добавления очередной порции кода, при сборке Release-версии линкер выдал ошибку:

LIBCMT.lib(crt0.obj) : error LNK2001: unresolved external symbol _main

Что делать?"

Иногда на подобный вопрос можно получить следующий ответ:

"Да, у меня тоже была такая ошибка. Вылечилось добавлением в исходники пустой функции main(){}.Это какой-то глюк у Microsoft. :( "

Что же на самом деле стоит за этой проблемой и как ее решить? Давайте разберемся.

Многое в этой статье справедливо для любой среды программирования на C/C++, но детали реализации будут приводиться для Microsoft Visual C++ версий 5.0 и 6.0.

Большое спасибо Павлу Блудову за ценные замечания в ходе обсуждения статьи.

Библиотека C Run-Time

Обычно C/C++-программа опирается на мощную поддержку С Run-Time Library - библиотека времени исполнения языка C, далее - CRT; более редкое название - RTL (Run-Time Library). Многим функциям этой библиотеки для правильной работы требуется дополнительная инициализация (CRT startup code). В частности, для вывода текста на консоль с помощью функции printf необходимо, чтобы дескриптор стандартного вывода stdout был предварительно связан с устройством вывода операционной системы (например, стандартным выводом и консолью Win32). То же самое справедливо и для функций работы с кучей - таких, как malloc для C и оператора new для C++.

Таким образом, даже в минимальной программе, содержащей вызов printf или попытку выделения динамической памяти, будет содержаться внушительный (для такой программы) код инициализации CRT - свыше 30 килобайт.

ПРИМЕЧАНИЕ

При использовании CRT в виде дополнительной динамической библиотеки (DLL) размер исполняемого модуля может быть меньше 30 Кб - об этом речь пойдет чуть позже.

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

Так, некоторые операции с плавающей точкой требуют наличия кода инициализации: например, на случай, если будет выполняться обработчик исключительных ситуаций (floating point handler). Объявление глобальной переменной, являющейся экземпляром класса, имеющего конструктор или деструктор, тоже требует наличия стартового кода CRT. Это происходит из-за того, что вызовы конструкторов и деструкторов в VC реализованы как часть стартового кода CRT. Использование механизмов обработки исключений C++ и Run-Time Type Information (RTTI) также влечет за собой необходимость инициализации.

Исходя из этого, разработчики современных компиляторов C++ строят CRT таким образом, чтобы её стартовый код включался в программу по умолчанию. В большинстве случаев это - именно то поведение, которое требуется. В самом деле, большой проект на C++ редко обходится без использования CRT-функций или вычислений c плавающей точкой. Да и "довесок" в 30 Кб в таком случае невелик.

Если это вас устраивает, проблема с упомянутым ATL-проектом решается достаточно просто. Необходимо зайти в настройки проекта ("Project" - "Settings"), выбрать нужную Release-конфигурацию и на закладке "C++" удалить опцию препроцессора _ATL_MIN_CRT. Вопрос будет снят. Дальше можно не читать.

Но встречаются случаи, когда считаешь буквально каждый байт исполняемого модуля. Это может быть ядро инсталлятора или самораспаковывающегося архива, элемент управления ActiveX, который скачивается через Интернет, или приложение для встраиваемой системы. Компиляторы C++ (и Visual C++, в том числе), на мой взгляд, наиболее подходят для такого рода разработок. Приложение может, в конце концов, состоять из большого количества модулей, и мало что значащие 30 Кб могут превратиться в несколько сотен килобайт, а то и мегабайт. Но для контроля над процессом сборки придется погрузиться в некоторые детали реализации поддержки CRT.

main или WinMain?

Среди начинающих программистов можно услышать такое мнение: для консольной программы используется только функция main, а для оконной - WinMain. Это мнение, хотя и подтвержденное умолчаниями компилятора и линкера, в общем случае, является ошибочным.

Чтобы немного развлечься, проведем эксперимент. Создадим файл test.cpp:

#include <windows.h>

int main()
{
    MessageBox(0, "Hello from main()", "A test program", MB_OK);
    return 0;
}

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
    LPSTR lpCmdLine, int nShowCmd)
{
    MessageBox(0, "Hello from WinMain()", "A test program", MB_OK);
    return 0;
}

Внимание, вопрос: что появится на экране после запуска такой программы? Постарайтесь ответить на этот вопрос, не заглядывая в дальнейшее описание.

ПРИМЕЧАНИЕ

Я не стал рассматривать еще два возможных варианта стартовой функции: wmain или wWinMain, предназначенных для проектов, компилируемых в Unicode. Кроме того, при создании DLL имеется еще один вариант стартовой функции - DllMain.

Точка входа в программу

Функция [w]main или [w]WinMain, с которой начинается выполнение программы, вовсе не является точкой входа исполняемого модуля! На самом деле, программа на C++ начинает работу с выполнения специальной процедуры инициализации. Что касается Win32, то адрес этой процедуры и содержится в поле AddressOfEntryPoint заголовка Portable Executable (PE) выполняемого файла. Она представляет собой обычную функцию C, описанную с соглашением о вызовах __stdcall. В зависимости от настроек проекта, в Visual C++ эта функция может называться [w]mainCRTStartup, [w]WinMainCRTStartup или _DllMainCRTStartup (символ 'w' добавляется к имени для Unicode-проектов). Конкретно же для сборки приложения имя функции-точки входа можно задать опцией линкера /entry. Умолчанием для Visual C++ является "mainCRTStartup". Все сказанное справедливо и для некоторых других компиляторов C++ для Win32.

Что же происходит во время ее выполнения? Вот типичный сценарий работы такой функции (случай DLL здесь не рассматривается).

  • Инициализируются переменные CRT (такие, как errno и osver). Многопоточная библиотека требует особой инициализации.
  • Происходит инициализация динамической памяти (кучи).
  • Инициализируется среда обработки ошибок в вычислениях с плавающей точкой. Это необходимо не только для библиотечных функций (таких, как sqrt), но и для преобразований между целочисленными и плавающими типами данных.
  • Получаются значения аргументов командной строки программы и переменных среды.
  • В случае необходимости, происходит инициализация консоли и привязка стандартного вывода к файловым дескрипторам C. При старте исполняемого файла, у которого в уже упомянутом заголовке PE значение поля Subsystem равно 3 (Windows character-mode executable), создается консоль. Это значение можно задать опцией линкера /subsystem. Выбор подсистемы выполнения также влияет на выбор стартовой функции (если ее имя не задано явно). Умолчанием является "console".
  • Происходит вызов цепочки функций инициализации CRT и конструкторов глобальных переменных (подробнее об этом - в следующем разделе).
  • И лишь после этого вызывается функция [w]main или [w]WinMain. Коротко можно сказать, что функция xxxCRTStartup вызывает соответствующую функцию xxx.
  • Программа работает.
  • Выполняется последовательность действий по очистке, к которой мы еще вернемся.
  • И, наконец, происходит завершение процесса.

Теперь, наконец, можно ответить на мой вопрос: он был задан некорректно :). В самом деле, результат сборки будет зависеть от набора опций компоновщика, установленных в проекте или по умолчанию.

Так, например, при вызове компилятора в командной строке таким образом:

cl test.cpp user32.lib

мы получим консольную программу и сообщение "Hello from main()" (вспомните, что говорилось об умолчаниях).

А вызвав компилятор вот так:

cl test.cpp user32.lib /link /entry:WinMainCRTStartup /subsystem:console

мы получим "чудо чудное": программу, у которой выполняется функция WinMain, но создается окно консоли.

Код инициализации глобальных переменных

Как в VC++ реализован вызов цепочки функций инициализации/завершения?

Наличие в программе хотя бы одной глобальной переменной - экземпляра класса - заставляет компилятор сделать следующее. Во-первых, он генерирует невидимую за пределами модуля функцию, в которой и выполняются необходимые действия - вычисляется значение инициализатора или вызывается конструктор. Далее создается специальная запись с указателем на эту функцию в сегменте с именем вида ".CRT$xxx". Детально разбирать формат именования сегмента мы не будем, сейчас важно только то, что все сегменты такого типа будут при сборке объединены в алфавитном порядке в один сегмент. Таким образом, в момент старта программы в памяти будет находиться массив указателей на функции, при вызове которых и произойдут необходимые действия. В стартовом коде CRT VC этим занимается функция _initterm.

А почему здесь используется термин "функции инициализации/завершения " вместо терминов "конструкторы/деструкторы"?

Напомню, что стандарт языка C++ разрешает инициализацию переменных с помощью неконстантных выражений. Если переменная (даже простого типа) описана в глобальной области, то ее инициализатор должен быть выполнен до вызова функции main/WinMain:

int len = strlen("Hello, world!");

Обработка в этом случае ничем не отличается от инициализации экземпляра класса имеющего конструктор.

Код завершения

Упомянув инициализацию CRT, нельзя умолчать о коде очистки, или завершения. В нем выполняются действия обратного характера (и, в том числе, деструкторы глобальных переменных). Что действительно заслуживает описания, так это то, что код очистки можно вызвать собственноручно. Да-да, он содержится в функции exit. Если же не вызвать ее явно, то она вызовется после возврата из main/WinMain. Наиболее выразительную реализацию вышесказанного я встретил однажды в исходных файлах CRT компилятора WATCOM C++:

exit(main(__argv, __argc, __envp));

То есть, можно сказать, что все выполнение программы имеет целью получение параметра для функции exit. :)

ПРИМЕЧАНИЕ

Вообще-то, exit (вернее, возможность ее прямого вызова) является, скорее, "пережитком" со времен программирования на C. При вызове этой функции из программы на C++ не выполнятся деструкторы для локальных переменных (что естественно, поскольку, в отличие от глобальных объектов, их деструкторы нигде не зарегистрированы). Кроме того, вызов exit из деструктора может привести к входу программы в бесконечный цикл, так что не злоупотребляйте этой функцией.

Со времен создания библиотеки языка C осталась и такая возможность, как регистрация цепочки обработчиков завершения с помощью функций atexit/_onexit. Функции, зарегистрированные вызовом atexit/_onexit, будут вызваны в ходе завершения программы в порядке, обратном порядку их регистрации. Для программы на C++ с этой целью лучше воспользоваться глобальными деструкторами.

На самом деле, в программе на VC регистрация деструкторов глобальных объектов также выполняется с помощью внутреннего вызова atexit после вызова конструктора. Это имеет довольно веские основания: если конструктор объекта вызван не был, то не будет вызван и его деструктор. Но, в любом случае, это - деталь реализации, на которую полагаться не стоит.

Внутри exit содержится вызов функции более низкого уровня - _exit. Ее вызов не приведет к вызову деструкторов и exit-обработчиков, а только выполнит самую необходимую очистку (не буду вдаваться в подробности, замечу только, что при этом вызываются C-терминаторы (функции из таблицы в сегментах "CRT$XT[A-Z]"), в частности, подчищается low-level i/o) и завершит программу вызовом функции Windows API ExitProcess.

И, наконец, функция abort является способом "пожарного" завершения программы. Она выводит диагностическое сообщение и также вызывает _exit для завершения процесса.

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

Уменьшаем размер выполняемого модуля

Но в нашем примере нет ничего, что потребовало бы использовать CRT. Более того, включив оптимизацию по размеру (/O1) и генерацию карты исполняемого файла (Generate Link Map, /Fm), можно заметить, что размер функции main - всего 23 байта. А размер выполняемого модуля составляет около 36 килобайт. Неужели нельзя его немного уменьшить?

Конечно, такие способы существуют, и некоторые из них я опишу ниже. Но важно понимать, что каждый из них не является общим решением (иначе именно он использовался бы по умолчанию), и имеет свои недостатки.

Использование внешней библиотеки CRT

Откомпилируем нашу программу следующей командой:

cl /MD test.cpp user32.lib

Размер полученного в результате EXE-файла составляет около 16 килобайт. Что за чудеса? Куда делась половина исполняемого модуля? Неужели он "похудел" за счет исключения CRT?

И да, и нет. Опция компилятора /MD указывает использовать для сборки библиотеку MSVCRT.LIB. В ней содержится только тот набор кода, который позволяет линкеру разрешить внешние связи. А сам код CRT находится в динамической библиотеке MSVCRT.DLL в системном каталоге Windows. Эта многопоточная библиотека используется и некоторыми бесплатными компиляторами C/C++ для Windows, например, MinGW.

Такое решение достаточно удобно, если проект состоит из нескольких модулей - каждый из них станет меньше на объем рантайма. Кроме того, оно позволяет Microsoft исправлять ошибки в уже выпущенных программах простой заменой старой DLL на исправленную версию. Этот подход активно используется многими разработчиками, использующими библиотеку MFC: если в опциях проекта выбрать "Use MFC in a shared DLL", то придется использовать динамическую версию CRT, иначе проект попросту не соберется. В интегрированной среде версия CRT выбирается в свойствах проекта: на закладке C/C++ в категории Code Generation.

Плохая новость заключается в том, что MSVCRT.DLL существует не на всех версиях Windows. Она начала поставляться в составе ОС, начиная с Windows 95 OSR2. Приложение, запущенное в системе без этой библиотеки, выполняться не будет. Правда, таких систем становится все меньше и меньше.

Уменьшение выравнивания файловых секций

Возможно, владельцы Visual C++ 5.0 заметили, что у них в результате получаются EXE-файлы куда меньшего размера, чем сказано здесь. Дело в том, что компоновщик версии 5.0 использовал выравнивание секций исполняемого файла на величину 512 байт. Начиная же с версии 6.0, при сборке приложения используется другая величина выравнивания - 4К. Это позволяет быстрее загружать такой файл в Windows 98 и более новых версиях ОС.

Вернуть прежнюю величину выравнивания можно, задав недокументированную опцию компоновщика /opt:nowin98:

cl /MD test.cpp user32.lib /link /opt:nowin98

Размер EXE в результате составляет менее 3-х килобайт! Но не забудьте, что такой файл будет медленнее загружаться в память, и что он по-прежнему требует наличия MSVCRT.DLL.

Радикальные меры: отказываемся от CRT Startup

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

Что это означает? Отказавшись от некоторых привычных удобств, которые предоставляет CRT, можно писать на C/C++, не используя возможностей, которые требуют поддержки со стороны CRT.

В мире Windows API такое решение не пугает многих. Взгляните, например, на NullSoft Installer.

В самом деле, для файловых операций можно использовать функции Win API, вместо динамической памяти C++ использовать кучу (хип) Windows, для форматирования можно использовать wsprintf вместо sprintf, для сравнения строк - lstrcmp вместо strcmp и т.д.

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

Мэтт Питрек, давний ведущий колонки "Under The Hood" в Microsoft Systems Journal (ныне - MSDN Magazine), посвятил этому вопросу цикл статей в MSJ под общей тематикой "Code Liposuction" ("обезжиривание кода"). Интересующиеся могут найти их в архиве Periodicals MSDN.

Более свежая информация содержится в его статье "Reduce EXE and DLL Size with LIBCTINY.LIB" в январском выпуске MSDN Magazine за 2001 год. Предлагаемая автором версия "крохотной" библиотеки исполнения выполняет минимальную инициализацию (например, вызывает конструкторы глобальных объектов) и даже предоставляет собственные версии таких функций, как printf и malloc. При этом размер выполняемого модуля оказывается зачастую меньше 3 Кб.

Но не будем забираться так далеко - ведь в нашем коде нет никаких конструкторов, правда?

В данном случае можно просто указать, что функция main будет точкой входа в программу (вместо функции инициализации):

cl test.cpp user32.lib /link /entry:main /opt:nowin98 /subsystem:console

В результате также получим исполняемый файл размером менее 3 Кб (я вновь использовал опцию /opt:nowin98). Разница теперь лишь в том, что он не требует внешней CRT-библиотеки (библиотека user32.lib необходима для функции MessageBox, но она является частью ядра Windows).

Версия ATL: макрос _ATL_MIN_CRT

Пригодность этого подхода доказывается тем, что с его помощью создано множество легких COM-компонентов. Но непонимание принципов его работы может легко завести в тупик, как видно из цитаты в начале статьи.

В составе библиотеки ATL версии 3 и более ранних имеется файл atlimpl.cpp. Он, как правило, включается в один из исходных файлов проекта (чаще всего в stdafx.cpp) с помощью директивы #include. В atlimpl.cpp находится "облегченная" реализация стартового кода CRT: в нее входят только вариант функции xxxCRTStartup, упомянутой ранее, и "обертки" для работы с динамической памятью - функции malloc, calloc, realloc, free и операторы new/delete. Они непосредственно вызывают функции Windows для работы с кучей - HeapAlloc и HeapFree. Как ни странно, этого достаточно, чтобы заставить заработать без CRT startup множество программ.

Собственно, сама эта реализация доступна, только если определен символ препроцессора _ATL_MIN_CRT. Таким образом, есть возможность легко управлять включением или исключением стартового кода CRT.

ПРИМЕЧАНИЕ

Важный момент при использовании макроса _ATL_MIN_CRT: по-прежнему нельзя включать объявления глобальных переменных, классы которых имеют конструкторы или деструкторы, так как код, их вызывающий, содержится только в CRT.

Эта проблема решена в библиотеке ATL 7.0 (не удивляйтесь, как и многие другие приложения Microsoft, ATL перескочила с версии 3 на версию 7), поставляемой с компилятором MS VC++ 7.0. Тем же, кто пользуется прежними версиями компилятора, могу посоветовать воспользоваться отличной библиотекой Andrew Nosenko's ATL/AUX Library, в которой содержится код вызова конструкторов/деструкторов. Для этого необходимо включать в проект вместо atlimpl.cpp файл AuxCrt.cpp из комплекта библиотеки.

Кто виноват?

Теперь ясно, что причиной появления ошибки "unresolved external symbol _main" стало включение стартового кода CRT. То есть, была явно или неявно использована какая-либо функция, которая содержит ссылку на структуру данных, находящуюся в модуле с кодом инициализации. При включении компоновщиком в программу этого модуля возникает следующая внешняя ссылка: в теле mainCRTStartup есть вызов main. Вот и все, мы получили наше "любимое" сообщение об ошибке.

Отдельной "увлекательной" стадией сборки приложения является поиск функции или фрагмента кода, вызвавшего такую ситуацию. Для этого применяются следующие шаги:

  • Включается опция компоновщика /verbose, при которой он выдает значительно большее количество диагностической информации.
  • Включается опция компоновщика /nodefaultlib (или /nod), которая подавляет при сборке поиск библиотек, кроме указанных явно. При этом в списке неразрешенных внешних ссылок будут как "безобидные" функции CRT (которые можно будет включить явно), так и "тянущие" за собой стартовый код CRT.
  • Локализовав модуль или функцию проекта, в которой появилась нежелательная внешняя ссылка на CRT, можно включить генерацию ассемблерного листинга (опция компилятора /FA) и простым поиском обнаружить, где происходит реальное включение.
Использование Standard Template Library

А как же насчет Standard Template Library (STL)? Насколько она завязана на CRT, можно ли использовать её в сверхмалых проектах?

Реализация STL от Dinkumware, поставляемая вместе с VC 5.0 и 6.0, доступна в исходных файлах, так что проблем с компоновкой не возникает. В крайнем случае, всегда можно исправить исходники или сделать какую-нибудь заглушку на #define'ах (перебивающую имена конструкций, тянущих за собой CRT). Другая проблема - в том, что STL повсеместно использует операторы динамического выделения памяти. Как уже говорилось, это вызывает необходимость собственной реализации операторов new/delete. Это можно сделать, например, так (идея позаимствована из atlimpl.cpp):

// stub.cpp - the "mini-CRT" implementation file

void* __cdecl malloc(size_t n)
{
    void* pv = HeapAlloc(GetProcessHeap(), 0, n);
    return pv;
}
void* __cdecl calloc(size_t n, size_t s)
{
    return malloc(n*s);
}
void* __cdecl realloc(void* p, size_t n)
{
    if (p == NULL) return malloc(n);
    return HeapReAlloc(GetProcessHeap(), 0, p, n);
}
void __cdecl free(void* p)
{
    if (p == NULL) return;
    HeapFree(GetProcessHeap(), 0, p);
}
void* __cdecl operator new(size_t n)
{
    return malloc(n);
}
void __cdecl operator delete(void* p)
{
    free(p);
}

Вот пример программы, которая будет спокойно собрана с помощью такого подхода без стартового кода CRT:

#include <windows.h>
#include "stub.cpp"
#include <map>

typedef std::map<int, int> IntMap;

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
    LPSTR lpCmdLine, int nShowCmd   )
{
    IntMap m;
    for (int j=0; j<100; j++) m[j*j]=j;
    IntMap::iterator i=m.find(49);
    MessageBox(0, (i==m.end())?"49 was not found":"49 was found", 
        "std::map test", MB_OK);
    return 0;
}

Для сборки этого примера необходимо использовать следующую командную строку:

cl test.cpp user32.lib kernel32.lib /link /nod /opt:nowin98 
                             /subsystem:windows /entry:WinMain

Библиотека импорта kernel32.lib необходима для функций работы с Win32-кучей.

Что касается других реализаций STL, предоставлю слово Павлу Блудову:

"Страшная тайна STL от SGI и HP в том, что им совершенно не нужна CRT.

С двумя оговорками:

  1. Не используется C++ Exception Handling
  2. (Вытекает из первой) определен макрос __THROW_BAD_ALLOC, например, так:
#ifndef _CPPUNWIND 
#define __THROW_BAD_ALLOC \ 
::MessageBox(NULL, _T("STL: Out of memory."), NULL, MB_OK | MB_ICONSTOP); \ 
::ExitProcess(-5); 
#endif _CPPUNWIND 
#include <stl_config.h> 

если посмотреть на __THROW_BAD_ALLOC, то он являет собой

#define __THROW_BAD_ALLOC fprintf(stderr, "out of memory\n"); exit(1) 

именно эта строчка, и никакая другая, нуждается в CRT. Ну, если быть совсем точным, std::string'у может понадобиться CRT. Тут уж ничего не попишешь. Используйте WTL::CString.

Павел."

Слова о std::string в полной мере справедливы и для реализации STL от Dinkumware. Если вы ищете реализацию полноценного строкового класса, не использующего стартовый код CRT, советую взглянуть на CascString в составе библиотеки ascLib.

Директива #import и ее ограничения в облегченных проектах

Частой причиной появления зависимости от CRT является необдуманное применение директивы #import - расширения Visual C++ для удобства работы с COM-объектами, предоставляющими библиотеки типов. Подробнее о ней можно прочитать в MSDN, а на русском языке - в статье Игоря Ткачева "Использование директивы #import в Visual C++".

При ее использовании компилятор генерирует описания интерфейсов и, если не указано обратное, создает набор оберточных классов (wrappers) для упрощения работы с указателями на эти интерфейсы. Кроме того, детали реализации COM-объектов скрываются за высокоуровневыми средствами. В число таких деталей входят преобразование [out,retval]-параметров в возвращаемые значения функций, упрощение работы с BSTR-строками, управление сроками жизни объектов, доступ к свойствам и преобразование COM-HRESULT в исключения С++. Но поддержка всех этих приятных "мелочей" реализована с использованием CRT и требует включения стартового кода CRT.

Директива #import, несомненно, полезна для C++-программиста - ведь иначе, не имея описания интерфейсов, пришлось бы извлекать необходимую информацию вручную с помощью утилит типа OleView. Эту директиву можно применять и в проектах, не использующих CRT, но с рядом ограничений. В частности, необходимо подавить создание оберточных классов и трансляцию типов COM в классы-обертки _com_ptr, _com_error, _variant_t и _bstr_t. Вот пример выверенного использования #import, которое не "потянет" за собой половину кода CRT:

#import "file.dll" no_namespace, \
  named_guids, no_implementation, \
  raw_interfaces_only, raw_dispinterfaces, \
  raw_native_types

Иногда при использовании #import можно обойтись "малой кровью". Это возможно, например, если в интерфейсах импортируемой библиотеки типов не используются BSTR- и VARIANT-параметры (вообще-то, достаточно редкий случай). Тогда можно воспользоваться всеми удобствами, предоставляемыми #import, но подавить генерацию исключений C++ при возврате ошибок. Для этого потребуется реализовать функцию

void __stdcall _com_issue_error(HRESULT hr);

Такая возможность определяется в каждом конкретном случае экспериментально. Все же, если вы не используете исключения, лучше отказаться от расширенной помощи директивы #import и обрабатывать HRESULT вручную.

ПРИМЕЧАНИЕ

В составе уже упомянутой библиотеки ATL/AUX есть средство автоматической генерации классов из библиотек типов, которое более пригодно для сверхмалых проектов, чем директива #import.

Использование вычислений с плавающей точкой

В статьях, посвященных использованию макроса _ATL_MIN_CRT, часто говорится, что в минимальных ATL-проектах нельзя использовать вычисления с плавающей точкой. К счастью, это не так. Уже давно миновали времена, когда программа на C++ не могла стартовать без кода эмуляции сопроцессора. Но трудности все-таки остались, и их придется обходить, поэтому:

постарайтесь использовать fixed-арифметику вместо floating point-вычислений

Это означает, что, например, для расчета суммы с точностью до копеек можно просто выразить сумму в копейках. Другой вариант этой методики - выделить несколько (например, 16) двоичных разрядов числа на целую часть, а оставшуюся часть считать дробной. Тогда целая часть числа получается простым сдвигом вправо или обращением по следующему адресу, что очень быстро. Такие расчеты здорово помогут там, где нужны только целые числа, но рассчитанные с хорошей точностью - например, в машинной графике.

Если floating-point вычисления необходимы, попробуйте обмануть компилятор с помощью _fltused

Встретив в программе объявление float-переменной (или double), компилятор автоматически вставляет в генерируемый код внешнюю ссылку на переменную _fltused, находящуюся в одном из файлов CRT. Это делается для того, чтобы прилинковать к программе код обработчика ошибок вычислений с плавающей точкой.

В случае, когда нельзя обойтись без плавающей арифметики, но код заведомо не вызовет ошибок, можно попытаться отключить стартовый код CRT для плавающих вычислений с помощью примерно такого объявления:

extern "C" int _fltused = 0;

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

Но фокус не сработает, если произойдет вызов функции CRT. Это может случиться неявно, например, при преобразовании между целочисленными и плавающими типами:

double t;
int a;
a = t; // Получили внешнюю ссылку на функцию _ftol

Правда, _ftol - это как раз пример функции CRT, которая может быть безболезненно использована в минимальной программе. Просто укажите в списке библиотек LIBC.LIB и позаботьтесь о том, чтобы обеспечить компоновщик своей версией стартового кода (при использовании _ATL_MIN_CRT ничего дополнительно делать не нужно).

Если же вызываемая неявно функция требует инициализации, есть два пути: отказаться от борьбы или реализовать ее иным способом, о чем сейчас и пойдет речь.

Несколько рекомендаций

Напоследок дам несколько советов, которые помогут обойтись без стартового кода CRT во многих случаях. Но помните, что это достигается за счет отказа от использования исключений, и работает не всегда. Как правило, начиная с какого-то объема кода, выигрыш от всех этих ухищрений сходит на нет, а неудобства отказ от CRT доставляет по-прежнему немалые.

Забудьте об этом, если используете MFC

Библиотека MFC требует наличия кода инициализации, и тут уж ничего не поделаешь. Если очень хочется использовать библиотеку оконных классов в сверхмалых проектах, посмотрите в сторону ATL/WTL и их расширений (например, Attila).

Используйте SEH вместо C++ Exceptions

Обработка исключений в стиле C++ неизбежно потребует стартового кода CRT. Если исключения использовать необходимо, попробуйте воспользоваться структурными исключениями Win32 с помощью ключевых слов __try, __except, __finally и т.д. Для их использования нужно подключить библиотеку импорта kernel32.lib.

Попробуйте позаимствовать необходимую функцию из исходных файлов CRT

Visual C++ поставляется с большим набором исходных файлов, в число которых входит и реализация CRT. Их изучение, кстати, приносит и еще одну выгоду - это поможет разобраться, как именно устроена поддержка стандартной библиотеки. В общем, "Use the source, Luke"!

Используйте директиву #pragma intrinsic

Некоторые функции, требующие инициализации CRT, могут быть попросту вставлены компилятором в точку вызова. К ним относятся cos, strlen и многие другие. Изучите документацию на #pragma intrinsic и опцию компилятора /Oi.

Для преобразования типов воспользуйтесь Automation API

Это мощнейшее средство преобразования данных разных типов ничего не будет стоить - кроме, разве что, лишних тактов процессора.

Можно использовать как функции высокого уровня VariantChangeType/VariantChangeTypeEx, так и вспомогательные функции преобразования вида VarXXXFromYYY. В приведенном выше блоке кода, например, поможет функция VarI4FromR8:

double t;
long a;
VarI4FromR8(t, &a); // Никаких проблем с внешними ссылками

Использование Automation API позволит не только решить проблему преобразования, описанную выше, но и учесть при этом региональные настройки - например, получить локализованную строку даты. Кроме того, функция VarBstrCmp поможет при сравнении строк Unicode (но будьте осторожны с ней - в старых версиях Windows она отличается от новых реализаций, также нужно иметь установленный Service Pack 4 или выше для VC, иначе заголовочный файл будет содержать некорректное описание этой функции).

Для использования этих функций необходимо подключить библиотеку импорта oleaut32.lib.


До следующей встречи!

Алекс Jenter   jenter@rsdn.ru
Duisburg, 2001.    Публикуемые в рассылке материалы принадлежат сайту RSDN.

| Предыдущие выпуски     | Статистика рассылки