Заметка о некоторых особенностях использования STL в DLL

Автор: Роман Хациев
Опубликовано: 27.02.2002
Версия текста: 1.0

Проблема 1
Проблема 2

Если вы пытались работать с экземплярами классов STL, передавая их в DLL, или получая оттуда, а потом бросили это занятие из-за непонятных ошибок, возникающих в вашей программе, то эта заметка для вас. Даже если видимых проблем в вашей программе нет, то все равно прочитайте эту заметку, чтобы знать что делать, когда они появятся :)

Проблемы с STL в DLL делятся на две категории:

Проблема 1

Начнем с первой категории и рассмотрим простой пример:

class test
{
	char data[100];
};

test *p=new test;
delete p;

Зададимся вопросом - каким образом библиотека (C Run-Time library, CRT) узнает, какое количество памяти нужно удалить при вызове delete? Возможно используется информация о типе указателя, передаваемого функции operator delete в качестве параметра? Напомню как выглядит объявление функции operator delete, фактически освобождающей память:

void operator delete(void *)

Следовательно, из параметров функции ничего получить нельзя. Что же происходит?

Существует два способа хранения информации о размере выделенной памяти: физический, перед выделенным фрагментом памяти (в нашем примере по адресу p-sizeof - структуры с информацией о блоке памяти), или же во внутренних структурах библиотеки. Для CRT, в Visual C++, фирма Microsoft воспользовалась вторым способом. Хорошо это или нет - вопрос больше академический. Нас же интересует что из этого вытекает.

А вытекает из этого следующее - каждая копия библиотеки, статически прилинкованная к EXE или DLL, имеет свою собственную копию всех внутренних данных. То есть, если ваш проект состоит из одного EXE и одной DLL, со статически прилинкованной CRT, фактически в памяти будут присутствовать две копии внутренних структур CRT. Соответственно, обращения к функциям управления памятью в коде из EXE и в коде из DLL будут работать каждый со своей копией внутренних структур CRT.

Все будет в порядке до тех пор, пока вы не попытаетесь в EXE освободить указатель, проинициализированный в DLL, или наоборот. Действительно, использование такого указателя ничем не грозит - и EXE и DLL находятся в одном адресном пространстве. Но, как уже говорилось, при работе функцией управления памятью будут использоваться разные копии внутренних структур CRT. И в этот момент, в отладочной версии произойдет assert, а релиз может и упасть, невероятно обрадовав этим пользователя.

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

Упомянутая инкапсуляция указателей как раз и затрагивает STL. Самый очевидный пример это std::string. Однако, помимо std::string, многие классы STL используют динамическую память для хранения данных, что делает их уязвимыми для указанной проблемы. Равно и любой ваш класс, содержащий указатели и применяемый вместе с классами STL, тоже подвержен этому.

Вообще, строго говоря, вышеописанная проблема не связана с ошибкой в STL или ошибкой в CRT. Это, как замечательно формулирует Microsoft, behavior by design, то есть так и задумано ;)

Итак, как избавиться от проблем, вызванных несколькими копиями внутренних структур статически прилинкованной CRT? Ответ напрашивается сам собой - прилинковать CRT динамически. Сделать это можно следующим образом: в Project Settings\С/C++\Code Generation\Use run-time library выбрать Multithreaded DLL или Debug Multithreaded DLL.

Конечно же, как и всегда, мы меняем одни проблемы на другие. Основное преимущество статической линковки CRT заключается в том, что программе не требуются дополнительные файлы при ее распространении. При динамической линковке мы этого преимущества лишаемся. Более того, MSVCRT.DLL и MSVCPxx.DLL присутствуют не на всех версиях Windows по умолчанию (то есть после чистой установки), что приводит к необходимости распространять их вместе с программой, однако исходную проблему такой подход решает.

Альтернативный подход заключается в переопределении операторов new и delete для всего проекта. Но это не всегда может быть приемлемо в середине разработки, и потому рассмотренный выше способ помогает, на мой взгляд, обойтись малой кровью.

Проблема 2

Обратимся ко второй категории проблем STL в DLL. Здесь надо сразу оговориться - речь идет о версии STL, поставляемой с Visual C++ 5 и 6. Другие версии STL могут и не иметь нижеописанных проблем.

Лично я столкнулся с проблемой при попытке в своей программе использовать по ссылке, или через указатель, экземпляр std::map, созданный в DLL. Применение operator[] приводило к падению программы где-то в недрах STL. Сразу хочу признаться, что, к сожалению, я не провел достаточного пристального изучения причин такого поведения. Предварительная отладка показала, что проблема гнездится в реализации двоичного дерева, применяемого классами map/set и им подобными. По результатам этого открытия я отправился в Google на поиски решения.

Вот найденная мною ссылка на статью автора версии STL, включенной в поставку Visual C++ 5 и 6. Краткий смысл в том, что фирма Microsoft изначально не ставила задачу обеспечения работы STL в DLL. В связи с этим, STL содержит код, приводящий к вышеописанным проблемам. Связано это с наличием статических членов в классах, что приводит к разным неприятным последствиям при пересечении указателя или ссылки на объект границ модуля (EXE или DLL). Автор выпустил исправления, которые, однако, до сих пор не включены ни в один Service Pack для Visual C++. Описание исправлений, которые надо вручную внести в заголовочные файлы STL содержится здесь. Однако, чтобы сэкономить ваше время, прикладываю архив с исправленными заголовками от Visual C++ 6 SP5. Распаковать архив надо в директорию MSDEV\VC98\INCLUDE, после чего крайне рекомендуется выполнить команду Rebuild All для всех проектов, использующих STL. Проблемы в моих проектах это решило.

Надеюсь, что эта заметка сэкономит вам какое-то количество времени, уже потраченного мною.


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