Ну кто так строит.....

С лета у меня висела плавающая бага, все руки не доходили.

Суть в том, что в LibRaw внутри аллоцируется кусок памяти, отдается вызвавшему приложению, а оно, когда с ним наиграется, должно освободить его путем банального free()

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

Оказалось

  • У Microsoft есть две библиотеки для работы с памятью, нормальная и отладочная. Ну, нормально, у всех так.
  • Но аллоцируемое этими библиотеками - несовместимо между собой. То что одна malloc(), то другая не может free(). Ну тоже понятно.
  • Но при этом - нет никаких проблем слинковать в приложение сразу две эти библиотеки. Релизный DLL тянет с собой релизную, отладочный код приложения - отладочную.
Ну а дальше - понятно, что релизный DLL аллоцировал, то отладочное приложение освободить не может, а может только упасть.

Дети, это нельзя понять, это надо запомнить.

Избаловались мы в юниксах с этими самыми

LD_PRELOAD=/path/to/debug-malloc.so ./myapp
А, да, сухой остаток, сижу выпиливаю в LibRaw зеркальный вызов, который будет освобождать аллоцированное строго правильным освободителем. Две строчки кода, блин.

Comments

Хм. А почему возникло ожидание, что результат работы одного кода будет совместим с результатом совершенно другого кода? То, что они решают одну задачу - это как бе непонятный аргумент. Автомобили тоже решают одну задачу, но колесо с УАЗа не поставишь на Ламборгини, вот и тут как бе не сходится...

В VC при завершении отлаживаемого DEBUG процесса, запущенного под дебагером, выводится дамп не освобождённых областей памяти. Т.е. там совсем разные алгоритмы. Не знаю, есть ли такое вообще в принципе под юниксом...

Плюс под VC есть очень хороший продукт BoundsChecker (когда-то его делала знаменитая NuMega, потом перекупила Compuware, потом ещё кто-то), который такие ошибки ловит на лету...

Ну, в общем, я б сказал, что не причём тут мелкомягкие) Они решали определённые задачи этим, поэтому код не совместим. Просто надо читать спеки и не мешать тёплое с мягким )))

Из меня виндовый программист тот еще, последние лет 18 - Unix only (хотя все меняется). Потому и удивляюсь.

И я привык, что в исполняемом образе (после динамической линковки) будет один malloc(), один free() ну и так далее. Ну никогда не было иначе в моей практике (до сегодня). Ну, понятно, если я их сам передефайнил на compile-time, то я сам себе дурак, но чтобы это стандартные includes за меня делали... ну не нравится мне это.

А практический вывод (последняя строчка поста) вводит меня в когнитивный диссонанс. Я должен писать никому не нужные три строчки кода:

static free_mem_image(...); // в определении класса

LibRaw::free_mem_image(sometype *p) { if(p) free(p); }

Вместо того, чтобы в вызывающем приложении просто позвать free(p);

Потому что этот самый free() может оказаться "не тот"

Что же до BoundsChecker - у нас есть valgrind (наконец то на FreeBSD), который тоже вполне ничего.

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

Итог - надо понимать, что каждому модулю соотвествует своя версия библиотеки, в unix думаю также, просто библиотеки наверное пореже обновляются.

Довольно часто разработчики интерфейсов скрывают работу с памятью, чтобы просто не требовать от клиента соответствия crt. Тогда надо либо давать доступ к аллокаторам, брать управление памятью на себя, либо наоборот давать пользователю переопределить аллокаторы, либо например вообще не давать управлять памятью пользователю, вобщем много вариантов.

Если посмотреть на всякие API к приложениям, то обычно такие варианты:
* COM style - не обязательно COM, но по сути есть фабрика классов, сам ты ничего не конструируешь и не удаляешь
* полный доступ - дают наследовать и прочее, обычно требуют точного соответствия crt debug и crt release
* переопределение alloc/free

Не, ну это же усраться.

Кроме аллокатора есть же и другие объекты, ну там файловые хэндлы (которые int) или FILE*

И че, если какая-то DLL-ка вернула мне результат fopen() чтобы я с ним покувыркался, то не факт что у меня оттуда fread() получится.

Йобнуться можно.

FILE* часть crt и она должна быть скрыта. Если такого слишком много, то наверное правильно требовать совпадения crt. Мне кажется зачастую можно обойтись без открытия наружу FILE* - ведь речь идет о взаимодействии библиотеки и пользователя. Внутри вашего проекта отдельные модули конечно лучше согласовать, чтобы о подобном не думать, а снаружи давать доступ без подобных вольностей.

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

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

Не, внутри *моего* проекта проблем как бы нет.

Речь идет о том, как должен себя вести поставщик DLL.

Ну то есть понятно - сам породил, так сам и убивай, но если это убивай состоит из free(), то обидно как-то.

В API которые я видел обычно сразу подобные вещи оговаривают, поставщик DLL должен четко описать это. Если он дает делать все что угодно и не закрывает интерфейсы памяти и доступ к crt объектам значит он как бы намекает на то что вы должны подстроится под меня и теперь это ваша проблема.

Вот пример из реальной жизни - подобный подход проповедует API AutoCAD, но они выпускают обновления API с заметным опозданием от версий VS. Пришлось поддерживать проект в старой студии, чтобы все это работало.

Мне кажется, при проектировании библиотек лучше сразу решать эту проблему и отказываться от жесткой привязки.

Все-таки, я не понимаю, если у меня вот такой вот код:
int someclass::open_file(...)
{
return open(....)
}
То мне потом надо самому это закрыть. ФАЙЛОВЫЙ ХЭНДЛ? Вызывающее приложение close() не может сказать?

Я смирюсь с этим, куда деваться, но это полная херня.

Принципиальной разницы между результатом malloc() (реальный случай) и этим примером - я не вижу.

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

прикладной код вообще не должен делать никаких предположений относительно реализации.
Сегодня у тебя там open(...), а завтра - sopen() какой-нибудь или ещё какая хрень.

Так что херня - это когда один "аллокирует", а другой - "деаллокирует"... а потом третий сидит и "рвёт на ж. волосы" в попытках развязать этот порочный круг.

Да хоть open, хоть sopen, хоть socketpair.

read() оттуда будет читать, а close() - закрывать.

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

Это всего лишь вопрос реализации, а не фундаментального устройства мира.

Не, сдуру можно и хрен сломать. Я же не сисколл напрямую делаю, а типа библиотечную функцию зову.

Меня удивило в первую очередь - наличие нескольких рантаймов одновременно. При том, что обе части собирались даже одним компилятором.

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

чёт ты уже откровенно психуешь и тупишь.

fread скомпилирован в том же environment, что и fopen, так что "контракт" между ними не нарушен... либо оба пойдут через "дебагнутую" либу, либо - через "релизную", "многопоточную", "многоядерную" и ты.ды.

>> И я привык, что в исполняемом образе (после динамической линковки) будет один malloc(), один free() ну и так далее. <<
Так ведь маллок/фрии связываются статически. Разве нет ?

PS: if(p) free(p) - здесь if() избыточный. free() не падает по NULL. это стандарт.

Вот в текущем проекте разрабатываем API, придерживаемся такого подхода:

Наши объекты конструируем через фабрику, либо их создает другой наш объект - тут пользователь ничего сам не строит, проблем нет.

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

Так всё верно, free() действительно будет не тот и это действительно забота того же самого код, который выделил память, её и освобождать.
А как вообще можно ожидать, что в другом модуле он будет "тот"? free() - это ведь не API операционки, это API ран-тайм библиотеки, поддерживающей ваш конкретный модуль. А если ваш модуль цепляют к другому языку? Там свои ран-тайм библиотеки...
Хотите универсальности? Пользуйтесь API операционки, какими-нить HeapAlloc/HeapFree всё будет как ожидается...
Это в юниксах почти всё собирается из исходников, если я ничего не путаю - почти всегда на с/с++ и всегда с одной системной ран-тайм библиотекой (скомпиленой под конкретно вашу операционку). А на винде миллион компилируемых языков и систем их поддержки. Забиваться на какую-то одну среду означает сразу сужать область использования своей разработки. Вот вам и ответ, кому нужны эти три строчки кода... Это нормальная инженерная практика, снижающая зацепление компонентов, - "где выделил, там и освобождаю", поэтому VC не ожидала такого подвоха ;))

Как тут уже написали, libc - часть системы. Есть всегда.
То бишь, free() всегда один, а если не один - это бардак и катастрофа.

Я могу его подменить (отладочной библиотекой аллокации, например), но для всего приложения (процесса) сразу.

Вот с stdc++ бывает веселее и со всякими темплейтами - тоже.

>> Как тут уже написали, libc - часть системы. Есть всегда.
То бишь, free() всегда один... <<

это если связывание статическое.
А если не статическое, то будет ровно тот же "бардак и катастрофа" что и у мелкомягких.

не так ли?

Нет, не так.

Иначе бы LD_PRELOAD не работал бы.

понятно.
Ну, в виндах похожего эффекта можно достигнуть скопировав дебагнутую версию ддлибы с именем релизной (or vise verse) в рабочую директорию экзешника (кажется, длл-ки ищутся сначала "дома", а потом - по путям), но, боюсь, всё равно всё будет крэшиться...

Да, к вопросу об инженерной практике.

Проблема в том, что проблемный вызов сделан именно для того, чтобы отвязать время жизни одного объекта (результата обработки RAW) от жизни другого (распаковщика RAW). Распаковщик отстрелил результат и пошел обрабатывать другой файл, например.

То бишь вполне жизненная задача, по заявкам телезрителей сделаная.

Так ведь маллок/фрии связываются статически. Разве нет ?

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

Это значит, что у тебя не хватает третьего элемента абстракции - "RAW_file" или что-то типа того.

Иначе и не было этой хрени с "бесхозным free()".

Есть-есть,
libraw_processed_image_t*

Но это С-шный объект, а не плюсовый, деструктора нету (в частности по той причине, что у LibRaw есть C-API, но не только).

Вот пришлось приделать, да. С явным вызовом.

Что значит "С-шный объект" ( это тот самый крокодил, который летает, но низэнько-низэнько ?

Ну вот FILE* - отличный пример такового объекта.

вот именно.
Никто не делает close( FILEptr->_file ), для этого сущ. fclose(FILE *)

т.е. тот хендл, который был "аллокирован" посредством fopen, никогда "наружу" не выдаётся.
И, кстати, сам "объект" FILE* тоже "аллокируется"/"деаллокируется" не прикладным кодом, а либой.

так что "отличный пример" свидетельствует как раз против твоей практики.

Ну да.

Ну так и в обсуждаемом примере аллоцированное malloc() освобождалось free().
Никакой самодеятельности.

на мой взгляд обсуждаемый пример ближе к метафоре "close( FILEptr->_file )".

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

С разными crt это известная хрень, много крови попортила. С шаблонами еще веселее получается - там код инстанцируется несколько раз и если аллокация торчит наружу, то она может быть разной. Побороть это можно используя виртуальные методы в шаблонах, тогда хоть и проинстанцируется много раз, но вызов будет через таблицу виртуальных функций, для каждого объекта будет вызываться то что надо. Кстати в stl из visual studio кажется до версии vs2005 была еще мина заложена в std::map - там статическое поле торчало. У нас мы приняли за правило stl в интерфесы не выносить, стараться прятать, да и вообще использовать по минимуму.

Еще неприятная проблема с решением которой компилятор не поможет - это опциональные поля структур/классов отключаемые условной компиляцией - при неаккуратной настройке проекта можно получить ситуацию передачи структуры другого размера в другой модуль.

Ну, это всё напоминает случаи верёвок достаточной длины, что выстрелить себе в ногу...
Или уж собирать всё в рамках единого проекта, или не ждать, что build- и run-time окружение в другом проекте будут такими же...

Насчет веревок и ноги не понял?

При разработке принято изолировать код на модули - это просто архитектура и дизайн. Естественно в рамках одного большого проекта модули скорее всего будут поддерживаться параллельно и везде все будет на одной crt и нативно юзать классы как хотят. Но какие-то проекты могут быть сторонними или по каким-то иным причинам быть в другом окружении - вот тут надо думать обо всех этих проблемах. Плохо что сама среда c++/с никак тут не помогает. В managed языках я думаю такой проблемы быть не должно в принципе - нет совместимости нет компиляции.

Не, ну я последние лет 15 или около того, как разделямые библиотеки в Linux/FreeBSD появились, собираю все с зависимостями от того что на машине есть, потом эти зависимости апгрейдю (в рамках сохранения ABI, естественно, там есть понятные грабли вроде "размер структуры поменялся") по одной - и не жужжу.

То бишь описанные вверху грабли - для меня внове.

Меня гораздо больше раздражает не наличие отладочной и дебажной библоиоки языка C, а то что библиотека C в виндах не часть системы.
То есть в состав системы какая-то древняя входит (в XP, например была только от VC6). Ставишь более новую версию компилятора, и приходится распространять вместе со скомпилированным кодом еще и MSVCRxx.DLL (это если без плюсов. А то еще и MSVCPxx)

Да, считаю они должны в windows update все версии crt пихать. Сейчас каждая программа тянет с собой crt redistributable package, он совсем небольшой, но обычно устанавливается в winsxs, а это системная вещь и например на vista и win7 требует поднятия прав до administrator, соответственно имеем UAC, что несколько кривовато при установке любой программы. Однако есть выход, которым не все пользуются - приватная сборка, когда все нужное в папке программы, кстати рекомендуемый подход. Только вот если программа состоит из нескольких компонент, то в чужие компоненты нельзя вмешатся, а им тоже нужен crt.

Почему-то во всех остальных распространненых системах не стесняются при апдейте ЗАМЕНИТЬ версию libc на более новую.
И ничего не ломается. В Solaris вообще у меня программа скомпилированная в Solaris 9 прекрасно работала в Solaris 8 с его более старой libc. (ну то есть я знаю какие функции надо было подергать, чтобы не работало, которые раньше были в отдельных библиотеках, а потом их в libc внесли. Но если их не использовать...)

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

А зачем ее менять? Языку 40 лет, чему там меняться? Внутреннюю реализацию можно оптимизировать, но API-то неизменен.

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

Что удивительно, Microsoft на протяжении четверти века поддерживала стандартный API MS-DOS. И все прекрасно работало. И новые возможности добавлялись (сеть, длинные имена файлов) и работать старым программам это не мешало.
А здесь - не хотят.

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

С crt ситуация такова, то к сожалению это не только стандартная библиотека C. Наверное часть его можно и заморозить, но это видимо и делать неудобно и возможно не решило бы проблему, т.к. используются не только стандартные функции, вот цитирую wikipeda:

http://ru.wikipedia.org/wiki/Libc

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

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

С MS-DOS скорее всего старались написанное не трогать, только добавлять новое. В win32 уже не все так просто, ядро они дорабатывали и что-то все-же менялось, находили баги, для совместимости со старым у них есть хитрая подсистема которая имитирует ошибочное или считающиеся нестандартным поведение для многих старых программ.

Понимаете, практика на Unix-системах, уж всяко в смысле CRT не менее развесистых, очень простая: берешь варез в бинарниках, никакой libc/CRT в поставке нет - и он работает.
Систему поапгрейдишь, часто со сменой версии системы (и libc тоже) - и все тоже работает.
Сюрпризы - да бывают. Но они редкие и не считаются нормальными.

Согласен удобно, но вот crt в windows меняется с версией visual studio или vs service pack, кто-то должен положить это обновление в систему. А потом как я понял есть альтернативные crt для unix и он уж наверняка идут просто в комплекте с софтом.

А я вот не уверен, кстати.

Если такие есть (не считая, скажем, зоопарка внутри одной системы - с тредами такими, с тредами сякими, отладочные) - они скорее всего будут бинарно совместимы.
Вышеописанный зоопарк уж тем более совместим.

Тут я могу точно сообщить - версия crt менялась с выпуском vs2005 sp1, название файла не менялось и с выпуском vs2008 sp1. Название меняется при смене версии vs, они прибавляют 1. Если нет такой библиотеки, то _ничего_ работать не будет, манифест внедренный в dll/exe четко это оговаривает. Дебажные версии поставляются всегда отличаются - там даже название файла другое (d в конце). Т.е. все жестко - имя и версия должны совпадать. Насчет многопоточности не помню, там вроде mt в названии есть.

Ну уж обновление рантайма от Microsoft сама Microsoft могла бы и класть, тоже мне проблема.

С 7-гигабайтным (у меня) winxs я понял что меня раздражает - нет способа узнать, что из 20 версий примерно одного и того же - 15 уже не нужны никому.

Судя по размеру winsxs у тебя многое из этого нужно, у меня то меньше почти в два раза. Наверное при деинсталляции оттуда может что-то убираться.

Все-же мне не очень понятна например такая ситуация. Один проект использует glibc, а другой например dietlibc, как они будут уживаться? Если предположить что есть системный сервис управления памятью, то допустим все через него работают, а как быть с FILE*, где гарантия что автор одной библиотеки не изменит эту структуру под себя. Опять-же есть ли там debug/release различия для одной и той же библиотеки?

Вопрос был бы своевременным, если бы альтернативные библиотеки действительно были бы. Я гуглением нашел только упоминания glibc для солярки (и то, это скорее пожелания, чем что-то еще).

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

Т.е. вся альтернативность сейчас - это glibc 2.6 против версии 2.2

Понятно. А с glibc как-бы гарантируют что подобного не произойдет. Т.е. они туда ни одной новой функции не добавили и все тотально взаимозаменяемо?

Добавили, естественно.

Но если кто-то требует новое, его нету (и версионирование не сработало, скажем) - то просто не слинкуется.

А за совместимостью ABI должны следить.

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

Если это делается на уровне линковки и как-то вшито в модуль, то как быть если я пытаюсь пристыковать два чужих модуля каждый из которых был слинкован с какой-то своей версией. Я должен взять самую старшую за основу и как-то переопределить из зависимость, правильно я понимаю?

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

Да, в пределах одного major все нормально, берем старшую версию и все.

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

Что касается ошибок, то ну нельзя сказать, что в libc их нет, но в этом мире принято пофиксить ошибку (и вернуть патч разработчику, например), а не обойти.
Принято - не означает, что оно всегда так.

Ошибку то пофиксили, а вот программа может работать только с ошибочной версией библиотеки. Попросить разработчика поправить это конечно здорово но не всегда возможно.

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

Если в unix есть поддержка версионирования dll в принципе, то чем тогда лучше glibc, почему нельзя несколько версий подгрузить одновременно?

Почему нельзя - понятно даже очень. Это же DLL hell, но не на этапе линковки, а на рантайме.

То самое против чего я возмущался - две free() в одном процессе.

Это не совсем то, dll hell получается не в процессе разработки, а в процессе эксплуатации написанного. Когда у тебя чудесным образом меняется поведение программы из-за подмены библиотеки. А то, что есть проблемы в момент разработки это несколько иное. То, что у юзера окружение будет совпадать с тем в котором отлаживал разработчик как-раз залог нормальной работы.

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

Стандартный линкер не даст нормально смешать разные версии библиотек.

Т.е. да, это можно сделать, влинковав одну "dll" в другую статически (возможно), но это удивительный секс.
И в этом случае, действительно, можно будет добиться, что malloc() в одном модуле несовместим с free() в другом.

Но для этого нужно специально постараться, само так не получится.

Заметим себе, что под вендами чудесным образом поведение программы у меня меняется от включения отладки по memory leak. Очень сексуально, когда релизная версия работает, а отладочная - нет.

> Заметим себе, что под вендами чудесным образом поведение программы у меня меняется от включения отладки по memory leak. Очень сексуально, когда релизная версия работает, а отладочная - нет.

Если я верно понимаю, то ты говоришь не о смешении разных crt в рамках одного процесса, а именно о замене crt release на crt debug. Если это так, то вообще при переходе от release к debug разные интересные вещи происходят. Ошибки проявляющиеся в релизе могут не происходить в дебаге, обычно воспроизводимость в дебаге намного лучше. В дебаге аллоцируемая память заполняется константой, кроме того адреса будут немного другими при запуске, поэтому ошибки на указателях по другому будут себя вести, от оптимизации кода тоже изменения в адресах будут, вобщем всякие чудесные ошибки при работе с памятью могут в дебаге и релизе отличаться. Такая проблема была всегда - рано или поздно выскакивает какая-нибудь заковыристая проблема которая плохо воспроизводится и при попытке отладки ускользает.

нет, просто есть versioned symbols. Что в glibc, что в солярисе, что в FreeBSD.

Понятно, но значит все-таки связывание идет с конкретной реализацией.

ога, а ещё есть всякие такие "манифесты", когда требуется не просто msvcpxx.dll, а msvcpxx.dll строго определённого релиза (в смысле последних циферок номера подверсии). Вот тут самая жесть начинается... Сменили dll hell на hell нового уровня...

Наоборот так побороли DLL hell - каждая программа строго привязана к crt библиотеке, манифест всегда внедряется внутрь DLL/EXE и описывает эту привязку. Библиотеки сейчас лежат не в одной куче в system32, а ставятся в winsxs, где четко описаны версии. Даже библиотека с одним именем может иметь несколько вариантов и это правильно, только тогда гарантируется точное совпадение окружения с тем что было при разработке.

Я не понимаю. Вот есть разработчик библиотеки, он дает мне DLL + .H (+lib, естественно), как-то он его скомпилировал.

Либо нужно все это версионирование DLL/библиотек энфорсить и просто не давать скомпилироваться (слинковаться) не с тем рантаймом, либо все должно работать.

А как сейчас - и не энфорсят и не работает.

Если у меня есть libtiff, собранный VC6 (и зависимый от того рантайма), плюс libjpeg, собранный VC8 (и зависимый от другого рантайма), то я правильно понимаю, что VC2010 мое приложение с ними слинкует, будет зависимость от третьего рантайма, а работать оно будет, скорее всего, ужасающе?

У меня под FreeBSD8/x64 работает антиспам, собранный (со всеми .so/dll) под FreeBSD5/x32 и отлично же работает.....

Разработчик дает тебе: module.dll, module.lib, module.h

module.dll зависит от crt_ver1.dll и это ты не изменишь, нельзя и не правильно подменять версии библиотек, т.к. они отличаются.

Другой вариант - он вообще может статически с ней слинковаться, тогда crt_ver1.dll не должна присутствовать в системе, но это так уже почти никто не делает.

Далее, если module.dll сделал выделение памяти и дал тебе указатель, а твоя crt имеет другую версию, то куча у тебя другая и при освобождении памяти будет падение.

> У меня под FreeBSD8/x64 работает антиспам, собранный (со всеми .so/dll) под FreeBSD5/x32 и отлично же работает.....

Я не очень понял - речь идет о программе собранной для bsd5 32-бита, она тянет с собой все необходимые библиотеки. Ты ее копируешь на 64-битную более новую систему и она тоже работает. Верно? Ну так она все необходимое с собой несет. Вот если ты откроешь в 32-битной системе состояние процесса и посмотришь список подключенных модулей и версии и тоже проделаешь в 64-битной системе - ты увидишь отличия?

Про твой пример с зависимостями:
app -> libtiff
app -> libjpeg
app -> msvcrt10
libtiff -> msvcrt6
libjpeg -> msvcrt8

Все будет работать, каждый со своей библиотекой. А если начнете работать с объектами crt одной версии (объекты кучи, FILE* и пр.) из другой все рухнет. Задача разработчика библиотеки, которую он выпускает во внешний мир обеспечить безопасный интерфейс и описать правила как этого добиться.

Вот мне удивительно, что общеупотребительные объекты, вроде того же FILE* - несовместимы между собой.

Непривычный я к этому.

У этой структуры в последней версии размер изменился, вобщем что-то дописывают. Вообще стандартная библиотека не совсем стандартная, там куча всего дополнительного и это не только к windows относится, это везде так. Посмотрел в wikipedia - оказывается под unix их наплодили уже несколько.

Их конечно несколько. Их даже на одной машине может быть несколько.

Просто механизм динамической линковки другой - тот же free() в рамках процесса будет один. А откуда он взялся, из системной библиотеки, или я jmalloc подсунул для скорости или, наоборот, еще какой *alloc для отладки - это уже другой вопрос.

Уж у malloc/free, по счастью, даже потенциальных проблем с интерфейсом нет в рамках одной битности.

Понятно, что грабли есть и на таком пути - когда вызывающее приложение получает не просто хэндл (FILE* или void* или результат open), а с чем-то вроде stat(), но это лечится версионированием "DLL" и stat все одно будет один (а если два "DLL" просят stat() от разных версий - то не слинкуется).

>Просто механизм динамической линковки другой - тот же free() в рамках процесса будет один

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

Ну да, естественно, есть много разницы.

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

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

С отладкой - да, теоретически приятно иметь отладочный аллокатор только для своего кода, а скажем для системного - не иметь. Но готов этим пожертвовать.

Многопоточность - это еще одна сторона одной проблемы - согласование разных версий. В .net для этого есть атрибуты которые помечают реентерабелен ли метод или класс, т.е. наверное уже компилятор что-то может диагностировать, аналогично наверное с системными библиотеками в .net. А если говорить про однопоточную версию crt, то сейчас ей я думаю уже мало кто пользуется, падение производительности совсем небольшое, поэтому лучше не мудрить.

Не, ничего она такого особенного с собой не тянет, всякий libc уж точно системный.

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

Другой вопрос - а есть ли альтернативы этому, чтобы всё работало и не кусало друг друга?... похоже, что нет...

а есть ли альтернативы этому, чтобы всё работало и не кусало друг друга?

Меня бесит ситуация, когда линкуется и не работает. Пусть уж лучше не линкуется (в смысле - динамическим линкером, на рантайме).

Вот с этим согласен - бесит именно то что линкуется и потом не работает.

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

источник проблем при смене версии libc я видел ровно один раз. Но это была Oracle которая сама себя высекла. Они там в процессе инсталляции перелинковывали приложение из объектников. А тут при смене версии о совместимости по объектникам никто не позаботился - старые слинкованные бинарники работают, старые исходники после компиляции работают и ладно.

Не-не.
Источники проблем конечно есть, но это все разовые понятные акции:

off_t, который был 32 бита, а стал 64. На эту тему все-таки был вялый секс, который впрочем версионированием libc вполне лечится (как оно лечится в Linux - отдельная грустная история, по поводу которой я уже возмущался).

То же самое предстоит с time_t и эти часы уже тикают.

alex пишет, что в unix есть версионирование DLL/.SO, поэтому вполне возможно, что версии то разные реально работают, либо они осознанно остались на старой.

Я солярку лет 10 уже не встречал, но и там должно бы быть, как без него.

В Linux и FreeBSD - есть. В Макоси - судя по номерам версии в именах библиотек - тоже есть.

Но если я правильно помню, версионирование ELF-библиотек идет по major версии (в soname нету minor), т.е. если апгрейд был c сохранением major, то даже если старую версию не снести, ее считай что и нету, динамический линковщик не найдет.
Это к Linux/FreeBSD относится, в макоси может быть все вообще не так.

В солярке, что характерно, как в 8-ке была libc.so.1, так и в десятке libc.so.1.

И с выкрутасами от Оракла - имеет шанс умереть молодым, без 64-битного time_t.

Жалко будет. Потому что Linux совсем опопсел. А BSD я не люблю почему-то.

Хорошо, версионирование есть (пусть и между major версиями), теперь вопрос почему не может быть двух версий в пределах одного процесса, как быть если один модуль хочет одну версию, а другой другую?

Да не хочет он другую версию. Ну не так это в UNIX'ах работает как в винде - что вы на этом зациклились. При загрузке DLL в UNIX происходит динамическая линковка, причем в отличие от виндов она полноценна - с настоящим линкером и разборкой символов. Поэтому привязка идет не к конкретной версии библиотеки а к символу (функция с определенной сигнатурой, глобальный символ с роеделенным именем и типом итп). Вся эта туфта как в виндах по привязке к номкру или функции и версии и имени библиотеки тут не нужна. Линкер просто найдет символ в библиотеке и слинкует а если сингатура не совпадет то нет. Все просто и работает более надежно. Пожтому неважно из какой версии libc вы использовали malloc/free - их сигнатура не менялась уже лет 40.

Версионирование DLL/.SO - есть благо, сменил ABI - увеличь номерок. А программа, без дополнительных ужимок со стороны программиста, должна линковаться только со своим major version number (а minor - типа багфиксы с сохранением ABI)

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

версионирование экспортируемых символов -- ещё лучше.

Да тоже ничего хорошего.

fclose() версии 7 должен закрыть результат fopen() версии 6 или нет? А c версиями наоборот?

То бишь, кроме symbols (т.е. точек входа/функций) надо еще данные версионировать. В-принципе, хорошая идея, только вот боюсь "срыв колпака моментально".

И при этом, кстати, fclose() версии 6 обнаружив FILE* версии 7 может склеить ласты, и это будет уже не на этапе линковки, а на рантайме.

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

В windows winsxs тоже дает возможность оверрайдить версии, но это конечно может плохо кончиться.

Ну вот у меня то с чего мой вопль начался - собралося ведь. Взяло и собралось. И из десятка примеров не работал только один.

А этого не должно было бы вовсе быть, должно было где-то заругаться.

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

Иногда полезно разкрыть граф зависимостей и просмотреть кто кого тянет. В windows - это утилита depends или dumbbin с консоли. Кстати если чего не хватает depends покажет.

Но другого выхода просто нет. Именно поэтому сейчас winsxs занимает например у меня почти 5 gb, но там конечно не только crt.

Даже с .net они вынуждены менять версию, но там сделали хитро - они CLR (низкий уровень) меняли всего один раз - в версии 2.0 добавили generics, а .net 3, 3.5 и 4 это добавление новых классов, а старые они видимо не трогают.

В windows есть еще вариант пользоваться HeapAlloc/HeapFree и HANDLE для файлов.
Ну не posix конечно.

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

Граждане, ну malloc/free так еще можно заменить, но ведь есть new/delete.

Думаю речь о принципиальной возможности использовать инвариант. new/delete я думаю просто добавляет некоторое дополнительное поведение поверх обычной аллокации, мне кажется даже если вызвать free для памяти выделенной через new ничего фатального не произойдет, хотя понятно что такое делать не нужно. Я могу ошибаться, но мне кажется new никак не влияет на стурктуру блока.

Переопределение это можно использовать только если везде брать ее за основу, если имеем уже реализованый код на alloc/free, new/delete то придется под него подстраиваться.

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

Фатального наверное нет, но если был выделен обьект - деструктор отрабатывать не должен. Хотя возможно в каких-то рантаймах это делают.

А откуда free() узнает, что ему подсунули указатель на класс (с деструктором)?
Там же this не будет.

Догадается? Опасное это дело.....

new/malloc могут брать памяти больше чем было запрошено, то есть malloc(x) захватит p размером x+i и вернет указатель на p+i, в p[0-i] служебная инфа.
Чтобы не делать этого для мелких блоков, можно держать несколько куч с быстрым определением из какой кучи данный блок.

Сомневаюсь правда, что такое сделают чтобы кто-то мог смешивать delete/free. :)

О том, что в смысле языка free заменит deletе речи нет, естественно никаких деструкторов никто не вызовет. Тут можно лишь говорить о целостности кучи, а если в C код попадает указатель на объект, то это уже проблема дизайна. Вообще в С/C++ чувствуется слабая проработка всех этих нюансов связанных со связыванием модулей, управление памяти во многом должно контроллироваться правильными действиями программиста и стандарт тут не помогает.

В c++0x есть что-то о сборщиках мусора, но не факт что это имеет отношение к данной проблеме.

Да, но это уже поведение более высокого уровня, непосредственно к структуре блоков памяти не должно иметь отношения.

Я проверял, вызов ядра из HeapAlloc (ntdll!RtlAllocateHeap) происходит редко.
Там реализован немаленький user-mode memory manager.

Ясно, так можно частично решить проблему именно с кучей, если все договорятся использовать эти аллокаторы. Но во-первых надо именно перейти на них, а во-вторых может это и не решить проблемы т.к. поверх "низкоуровнего" аллокатора может лежать дополнительный слой - я имею ввиду например дебажный alloc/free, который в заголовке содержит доп. инфу, следовательно освобождаться должен симметричным образом. Вобщем подход с унификацией нижнего слоя может прокатить, но это требует некой стандартизации.

>> Но при этом - нет никаких проблем слинковать в приложение сразу две эти библиотеки <<
что-то я не понял прикола.
В чём проблема ? - релизная и дебагная компиляция транслируют тот же malloc/free в разные символические имена ?
Ну, тогда тот, кто по недомыслию линкуется с двумя версиями crt, сам себе Злобный Баклан Буратино.
Если же символические имена будут одинаковые, то проблем быть не должно даже в случае линковки с двумя либами (возьмётся первая попавшаяся).

Так всегда и везде было. В той же джаве можно подсунуть две версии одного и того же класса и тоже никто не возбухнет (есть там какие-то механизмы, но ими, кажется, реально никто не пользуется. да и предназначены они для другого).

-+-
>> А, да, сухой остаток, сижу выпиливаю в LibRaw зеркальный вызов, который будет освобождать аллоцированное строго правильным освободителем. Две строчки кода, блин. <<
на мой взгляд это вообще плохая практика когда "аллоцирцет ресурс" один агент (библиотека, напр.) а освобождает его - другой (клиентский код, напр.), поелику нарушает принцип "loosely coupled" и прочие CRC.

так что вообще-то это ты не прав, а не мелкомяХкие.

Ну, тогда тот, кто по недомыслию линкуется с двумя версиями crt...

Ну а как - вот пришел 3rd-party DLL, с чем он там слинкован - вовсе неведомо...

Касаемо практики - как я тут уже написал в каментах, это именно такой вызов, который позволяет "аллоцировать ресурс", отдать его вызвавшему коду, а дальше заняться другими делами.
Скажем, с open() это была бы такая нормальная практика, хэндл открытого файла можно (в некоторых системах) и в другой процесс передать....

"нормальной практикой" это было бы сто лет назад, ещё до изобретения колеса ООП. А теперь это - вопиющаяя безграмотность, поскольку нарушается "инкапсуляция" и прочий полиморфизЬм.
Особенности реализации (и уж тем более члены класса) наружу не выставляют. это моветон и разбрасывание граблей.

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

С некоторым опозданием присоединяюсь к оратору - если некая либа аллоцирует память, то она же и должна ее освобождать.

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

И на очень большом количестве систем это не вызывает проблем, винды - исключение.

Так это и есть то самое незаряженное (вроде бы) ружъе, которое все же иногда "стреляет". То, что стрельнуло в винде - просто стечение объективных и субъективных обстоятельств.
Использование правила "кто аллоцировал, тот и освобождает" безопаснее. А уж в кросплатформенных либах безопасность кода должна быть одним из главных приоритетов - мало ли где и в каких рантаймах чего наворотили.

Кучи разные(наверняка уже кто-то ответил).
Вы видели, например у std::vector, второй шаблонный параметр?

template < class T, class Allocator = allocator > class vector;
А сам объект allocator'а, можно передавать в конструкторах.

Вот, как раз для борьбы с разными кучами при разных CRT, можно использовать одинаковые allocator'ы (в частности при использовании STL).

Но возможна также проблема с разными реализациями std::vector.
Конечно в этом случаем можно попробовать экспортировать его из библиотеки(специализацию).

STL в интерфейсах - вообще зло.

Да, зло. "Чистые" интерфейсы лучше(всё зависит от задач..), как минимум более портабельно между разными языками.
Но изобретать свои stl-подобные классы, из-за проблем с разными crt (вроде тут кто-то предлагал), в то время когда можно использовать stl (аллокаторы, экспорт специализаций, ну максимум простой врапер), не меньшее зло.

Даже если экспортировать.
Компонент1 использует MS STL и его экспортирует. Поставляется в бинарном виде...
Компонент2 использует GNU STL и его экспортирует. Поставляется и в исходниках тоже, но компилируется только gcc :)
Приложение использует STLport и готово что-то поимпортировать.....

Ужас же, нах.

C++ получился слишком сложным языком, на мой взгляд. Ну я вот лично давно отчаялся за ним даже следить, но ведь это говорят и те, кто следит.

По поводу MS STL/GNU STL/STLport - да, секас будет, но вроде со счастливым концом.

По поводу сложности C++, может оно и так, но я не вижу взаимосвязи этого высказывания с обсуждаемой проблемой.
Имхо все эти проблемы возможны и в других императивных/ОО языках.
Например, наверняка проблемы будет при смешивании Deplhi/FreePascal, FreePascal/GNUPascal, FreePascal'и разных версий.

Я затрудняюсь насчет счастливого конца. GNU+STLport - скорее всего счастливого конца не будет.
А связь со сложностью такая, что STL в интерфейсах - можно, а как обеспечить работу разных имплементаций (не строго одной с экспортом, а именно разных) - непонятно.

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

Про Дельфи плохо знаю, а смешать C, Fortran и Pascal можно без особых проблем.

спам фильтр у вас хреновый, уже неделю не пускает, попробую по-частям

"GNU+STLport - скорее всего счастливого конца не будет."
Если честно, GNU+STLport не пробовал, но судя по (stlport org ):
"STLport is a multiplatform ANSI C++ Standard Library implementation. It is free, open-source product", с GNU проблем не должно быть.

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

несовместимость, что называется calling conventions(в том числе, как передаётся this).
Разрешённый доступ к данным класса(на тот случай, если компиляторы генерируют разную структуру классов) в STL не встречал, виртуальных

функций тоже (на случай, если компиляторы их по разному реализуют).

КомпонентX вернул что-то STL'ское, для работы с ним используем только то STL, которое КомпонентX экспортировал.
Если надо передать что-то STL'ское из КомпонентаX в КомпонентY(с другим STL), берём данные через "чистые" интерфейсы STL КомпонентаX,

создаём на их основе объекты STL КомпонентаY, и передаём.
Да изврат, да как-то слишком индо, но ведь работает, если сложилась такая ситуация, то что делать. Было бы намного хуже, если бы

Компонент1 не экспортировал STL, но использовал его в интерфейсах.
Я где-то пропустил подводные камни?

"смешать C, Fortran и Pascal можно без особых проблем"
То что их можно смешать и они будут работать вместе это уже кэпство.
Но при таком смешивании может произойти точно такой же конфликт куч, или они лишены этого?
Что делать, если например FreePascal в интерфейсах экспортирует TList? Видимо придётся писать wrapper на FreePascal'е для этого

интерфейса, который будет экспортировать "чистый" интерфейс.
Что делать, если у разных версий FreePascal'а разные реализации TList? - то же индо

Может так пойдёт
pastebin.com/CksHPau7

О, блин.

И точно, фашизм какой-то. Регистрация помогает, но вообще я его повоспитываю.

Спасибо за сигнал.

Я в своих инструментальных средствах всегда для такого случая делаю свои функции для освобождения/закрытия всего, что моя библиотека/окружение выделяет/открывает для клиентского приложения. Граблей - миллион. Например - я люблю статичную линковку сишного рантайма, а там сразу будет пипец при освобождении памяти "со стороны" :) Да и отладочная версия распределителя памяти у меня есть своя :)

Конкретно с free() - это чисто виндовая подлость.

Но, да, придется тоже так делать.

Add new comment