Есть еще одна проблема в таком сценарии: когда разработчик библиотеки находит дефект в классе FastString, нет способа всюду заменить его реализацию. После того как код FastString скомпонован с клиентским приложением, невозможно исправить машинный код FastString непосредственно в компьютере конечного пользователя. Вместо этого разработчик библиотеки должен известить разработчиков каждого клиентского приложения об изменениях в исходном коде и надеяться, что они переделают свои приложения, чтобы получить эффект от этих исправлений. Ясно, что модульность компонента FastString утрачивается, как только клиент запускает компоновщик и заново формирует исполняемый файл.
Динамическая компоновка и С++
Один из путей решения этих проблем – упаковка класса FastString в динамически подключаемую библиотеку (Dynamic Link Library – DLL). Это может быть сделано несколькими способами. Простейший из них – использовать директиву компилятора, действующую на уровне классов, чтобы заставить все методы FastString экспортироваться из DLL. Компилятор Microsoft C++ предусматривает для этого ключевое слово _declspec(dllexport):
class _declspec(dllexport) FastString
{
char *m_psz;
public:
FastString(const char *psz);
~FastString(void);
int Length(void) const;
// returns # of characters
// возвращает число символов
int Find(const char *psz) const;
// returns offset
// возвращает смещение
};
В этом случае все методы FastString будут добавлены в список экспорта соответствующей библиотеки DLL, что позволит записать время выполнения каждого метода в его адрес в памяти. Кроме того, компоновщик создаст библиотеку импорта (import library), которая объявляет символы для методов FastString. Вместо того чтобы содержать сам код, библиотека импорта включает в себя ссылки на имя файла DLL и имена экспортируемых символов. Когда клиент обращается к библиотеке импорта, эти ссылки добавляются к исполняемой программе. Это побуждает загрузчик динамически загружать DLL FastString во время выполнения и размещать импортируемые символы в соответствующие ячейки памяти. Это размещение автоматически происходит в момент запуска клиентской программы операционной системой.
Рисунок 1.2 иллюстрирует модель FastString на этапе выполнения (runtime model), объявляемую из DLL. Заметим, что библиотека импорта достаточно мала (примерно вдвое больше, чем суммарный размер экспортируемого символьного текста). Когда класс экспортируется из DLL, код FastString должен присутствовать на жестком диске пользователя только один раз. Если даже несколько клиентов применяют этот код для своей библиотеки, загрузчик операционной системы обладает достаточным интеллектом, чтобы разделить физические страницы памяти, содержащие исполняемый код FastString (только для чтения), между всеми клиентскими программами. Кроме того, если разработчик библиотеки найдет дефект в исходном коде, теоретически возможно послать новую DLL конечному пользователю, исправляя дефектную реализацию для всех клиентских приложений сразу. Ясно, что перемещение библиотеки FastString в DLL является важным шагом на пути превращения класса C++ в заменяемый и эффективный компонент повторного использования.
C++ и мобильность
Поскольку вы решили распространять классы C++ как DLL, вы непременно столкнетесь с одним из фундаментальных недостатков C++ – недостаточной стандартизацией на двоичном уровне. Хотя рабочий документ ISO/ANSI C++ Draft Working Paper (DWP) предпринимает попытку определить, какие программы будут транслироваться и каковы будут семантические эффекты при их запуске, двоичная динамическая модель C++ ею не стандартизируется. Впервые клиент сталкивается с этой проблемой при попытке скомпоновать библиотеку импорта DLL FastString из среды развития C++, отличной от той, в которой он привык строить эту DLL.
Для обеспечения перегрузки операторов и функций компиляторы C++ обычно видоизменяют символическое имя каждой точки входа, чтобы разрешить многократное использование одного и того же имени (или с различными типами аргументов, или в различных областях действия) без нарушения работы существующих компоновщиков для языка С. Этот прием часто называют коррекцией имени. Несмотря на то что ARM (C++ Annotated Reference Manual) документировала схему кодирования, использующуюся в CFRONT, многие разработчики трансляторов предпочли создать свою собственную схему коррекции. Поскольку библиотека импорта FastString и DLL экспортирует символы, используя корректирующую схему того транслятора, который создал DLL (то есть GNU C++), клиенты, скомпилированные другим транслятором (например, Borland C++), не могут быть корректно скомпонованы с библиотекой импорта. Классическая методика использования extern "С" для отключения коррекции символов не поможет в данном случае, так как DLL экспортирует функции-члены (методы), а не глобальные функции.
Для решения этой проблемы можно проделать фокусы с клиентским компоновщиком, применяя файл описания модуля (Module Definition File), известный как DEF-файл. Одно из свойств DEF-файлов заключается в том, что они позволяют экспортируемым символам совмещаться с различными импортируемыми символами. Имея достаточно времени и информации относительно каждой схемы коррекции, разработчик библиотек может создать особую библиотеку импорта для каждого компилятора. Это утомительно, но зато позволяет любому компилятору обеспечить совместимость с DLL на уровне компоновки, при условии, что разработчик библиотеки заранее ожидал ее использование и создал нужный DEF-файл.
Если вы разрешили проблемы, возникшие при компоновке, вам еще придется столкнуться с более сложными проблемами несовместимости, которые связаны со сгенерированным кодом. За исключением простейших языковых конструкций, разработчики трансляторов часто предпочитают реализовывать особенности языка своими собственными путями. Это формирует объекты, недоступные для кода, созданного любым другим компилятором. Классическим примером таких языковых особенностей являются исключительные ситуации (исключения). Исключительная ситуация в среде C++, исходящая от функции, которая была транслирована компилятором Microsoft, не может быть надежно перехвачена клиентской программой, оттранслированной компилятором Watcom. Это происходит потому, что DWP не может определить, как должна выглядеть та или иная особенность языка на этапе выполнения, поэтому для каждого разработчика компилятора вполне естественно реализовать такую языковую особенность в своей собственной, новаторской манере. Это несущественно при построении независимой однобинарной (single-binary) исполняемой программы, так как весь код будет транслироваться и компоноваться в одной и той же среде. При построении мультибинарных (multibinary) исполняемых программ, основанных на компонентах (component-based), это представляет серьезную проблему, так как каждый компонент может, очевидно, быть построен с использованием другого компилятора и компоновщика. Отсутствие двоичного стандарта в C++ ограничивает возможности того, какие особенности языка могут быть использованы вне границ DLL. Это означает, что простой экспорт функций-членов C++ из DLL недостаточен для создания независимого от разработчика набора компонентов.