#cpp #dll #memory
В связи с этим вопросом... Заинтересовало, как же действительно работать с памятью в DLL, и, в частности, что остается с динамической памятью после освобождения DLL. Создал простую DLL, в ней функция make_array, которая создает массив в динамической памяти. VC++, линковка статическая. Дальше издеваюсь примерно так: dll = LoadLibrary("Dll.dll"); memfunc m = (memfunc)GetProcAddress(dll,"make_array"); int * a = m(5); for(int i = 0; i < 5; ++i) cout << a[i] << " "; cout << endl; FreeLibrary(dll); for(int i = 0; i < 5; ++i) cout << (a[i] *= a[i]) << " "; cout << endl; dll = LoadLibrary("Dll.dll"); m = (memfunc)GetProcAddress(dll,"make_array"); for(int i = 0; i < 5; ++i) cout << (a[i] *= a[i]) << " "; cout << endl; delete[] a; a = m(6); for(int i = 0; i < 5; ++i) cout << a[i] << " "; cout << endl; Ну, т.е. я проверял, можно ли удалять память в программе, и остается ли память доступна после выгрузки DLL. Получается, если созданы одним компилятором - то все нормально, диспетчер памяти достаточно умен? То же самое сделал с помощью Open Watcom - результат такой же. Но если DLL от OW, а программа от VC++ - то работает все, кроме delete[]. Как, само собой, и следовало ожидать - из-за разных диспетчеров памяти. Нет, я понимаю, что где память выделили - там и освобождайте :) Но вот никак не могу ответить на два вопроса. Если память выделена диспетчером памяти в DLL, то какой механизм используется, чтоб два диспетчера не перессорились? Для диспетчера в DLL выделяется какая-то своя область памяти? но при выгрузке DLL, как я понимаю, эта область остается не освобожденной? Как реально работать с такими вещами, как интеллектуальные указатели? Типа, в DLL вызвать make_shared, результат которого потом использовать в программе - как гарантировать последнее удаление в DLL? Все, что придумывается - слишком уж искусственное. Разве что делетер вызывать из DLL? P.S. Ну не работал я всерьез с DLL, глубоко не закапывался...
Ответы
Ответ 1
Первое, что нужно знать - это рантайм. В случае с студией, есть MD и MT. В первом случае используется общий рантайм. И если в одной длл создать класс, а в другой удалить - все будет работать как ожидается. Если же рантайм у каждого свой - тут сложнее. Если создать в одном месте, а удалить в другом, то может отработать (если менеджер память достаточно умный), может отработать, но упадет потом (если длл и программа используют одинаковый рантайм), а может и сразу упасть (если рантаймы различны). Как приложение работает с памятью? Обычно диспетчер памяти (который является частью рантайма), запрашивает о операционной системы большой кусок памяти (4Мб или даже 64Мб) и внутри него уже "нарезает" под мелкие объекты, которые создаются с помощью new/malloc. Когда приложение выгружается, эта память отдается назад операционной системе. но при выгрузке DLL, как я понимаю, эта область остается не освобожденной? если рантайм динамический, то менеждер памяти общий. И этой памятью заведует само приложение. Если же используется статический рантайм, то тут все за программистом и рантаймом. Длл знает о том, что её выгружают и "должна" освободить память. Как реально работать с такими вещами, как интеллектуальные указатели? умные указатели в этом случае хорошие помощники. Нужно только убедиться, что у них правильно прописан "custom deleter" (даже не знаю, как это на русском - пользовательский удалятор? или пользовательская функция удаления?). В этом случае будет вызвана правильная функция в правильном менеждере памяти (в любом случае, у программиста есть возможность все сломать). Но если DLL от OW, а программа от VC++ - то работает все, кроме delete[]. Как, само собой, и следовало ожидать - из-за разных диспетчеров памяти. тут ситуация хитрая. когда вызывается delete[], то менеджер памяти должен знать, сколько удалять памяти. И где то это количество нужно сохранить. И я точно знаю, что студия и gcc используют различные, несовместимые модели. Поэтому, если такой указатель передать между длл с различными менджерами, может быть абсолютно все, что угодно.Ответ 2
Смотрите. Проблема в том, что у C++ нету бинарного стандарта. Это значит, что один и тот же класс при компиляции разными компиляторами (да что там, даже при компиляции одним и тем же компилятором с разными флагами) может иметь разный бинарный layout. Может быть разное представление таблицы виртуальных методов, разная декорация имён методов, да просто разный физический размер. Поэтому передача между компонентами, скомпилированными разными компиляторами или разными версиями одного и того же компилятора, чего-то сложнее, чем простые структуры, закономерно может привести к проблемам и топтанию по чужой памяти. И в сложных случаях имеет смысл указывать #pragma pack. Это объясняет, почему в экспортируемых из DLL заголовочных файлах часто встречается extern "C". Если DLL гарантированно скомпилирована той же версией компилятора с теми же ключами, в этом случае передавать объекты можно. Но это сразу же вносит проблемы с версионированием: обновление компонент возвращает проблему. Это объясняет, почему не получится передавать объекты по смарт-указателю, если в проекте не все DLL скомпилированы той же версией компилятора. (А также почему системные DLL не используют смарт-указатели, исключения и другие фичи C++.) Далее, о проблеме принадлежности памяти. Эта часть Microsoft-специфическая. Компиляция DLL имеет режимы со статической и динамической компоновкой рантайм-библиотеки. В случае статической компоновки рантайма у вас есть своя реализация new и delete в каждой DLL, они друг о друге не знают ничего, и обладают непересекающимися heap'ами. Это значит, что вы должны обеспечить, чтобы объект деаллоцировался тем же рантаймом, который его аллоцировал. В случае динамической компоновки рантайма, у вас есть один рантайм на приложение — но только в случае, когда ваше приложение скомпилировано одной и той же версией студии! В этом хорошем случае вы можете аллоцировать объект в одной DLL, и деаллоцировать в другой. В плохом случае, когда у вас разные DLL скомпилированы разными версиями студии, они ссылаются на разные версии рантайм-библиотеки, и в этом случае у вас в бегущей программе будет по одному рантайму на каждую версию студии, с которой компилировались её DLL. С очевидными последствиями.Ответ 3
Вот тут это, по моему, хорошо освещается: Семантика симметрического владения. Если DLL возвращает указатель на динамически выделенный участок памяти, то пользователь должен вызвать соответствующий деаллокатор для этого. Само собой, если они не будут совпадать то будет проблема - для этого случая рекомендуется использовать функции для аллокации и деаллокации выделенных участков памяти в загружаемой библиотеке (dll->free() или dll->freeArray()) Гибкость. Если пользователь захочет выделять память в другом месте - не обязательно в heap а где-то еще. То есть, в итоге передавая указатель НА ПАМЯТЬ в функцию DLL с уже выделенной памятью будет лучше чем выделять память в самой DLL.
Комментариев нет:
Отправить комментарий