Давеча столкнулся с неприятной особенностью при разработке библиотеки на языке Си.
Есть в ней функции, возвращающие указатели на динамически выделенные массивы стандартного типа с помощью malloc(), по большей части это обычные Си-строки. Я рассчитывал, что освобождать эти данные из кода основной программы нужно стандартной функцией free(). Это разумно для компонентной системы, библиотека и программа, ее использующая, динамически подключаются к одной и той же стандартной библиотеке, в результате чего используют согласованные средства для выделения и освобождения памяти.
Основная разработка ведется в Linux, и в ней нарушения этого принципа не наблюдалось. Для любых доступных комбинаций компиляторов (gcc, clang, tcc, tendracc) для библиотеки и приложения никаких накладок не было замечено. Собранная библиотека оказалась переносима между разными версиями дистрибутива.
А вот в Windows меня ждало небольшое разочарование. Библиотеку для нее собирали средствами mingw, а приложение - с помощью Visual Studio. Несмотря на заверения о том, что mingw использует родной для Windows c-runtime, какую-то несовместимость он вносит. При попытке средствами программы освободить память, выделенную в библиотеке, происходит крах.
Есть категоричное мнение, что при правильной архитектуре освобождать нужно теми же средствами, что использовались для выделения. То есть в пару к функции, возвращающей указатель на динамически созданный объект, должна быть создана функция его освобождения. Тогда проблемы рассогласованности не возникнет.
Разумно, тем не менее, я считаю такой подход не всегда адекватным.
Он выглядит логично для сложных структур, но абсурдно для массива литер. Согласно нему создателям функций вроде strdup и asprintf, нужно было создать и strdupfree, asprintfree. Это может приводить к лишним накладным расходам как по времени выполнению, так и по написанию кода, а также дополнительным ошибкам. К примеру, если была получена выделенная строка, которую нужно передать в структуру - ее владельца, то туда же надо передать и информацию о способе освобождения строки, или сделать копию, а оригинал соответственно освободить. Сколько интересных возможностей вместо простой передачи указателя.
Проблема не исчерпывается только выделением памяти, есть ведь и другие ресурсы. Можно ли читать файл в библиотеке, открытый средствами программы? Или нужно предоставить библиотечную обертку над системной функцией для открытия файла? Что делать, если файл недоступен по имени?
Получается, что библиотека плохо встраивается в остальной код, и некоторые задачи могут выродиться в сплошное согласование вызовов функций, что может поставить вопрос об их целесообразности.
Поэтому мне больше по душе расчет на согласованность runtime`ов. Но что делать, если это нельзя гарантировать? Я нашел для себя приемлемое решение.
Нужна функция инициализации, заключающейся в том, чтобы передать из основной программы указатели на спорные функции, используемые в библиотеке. Именно их и будет вызывать библиотека вместо системных, что позволит ей тесно встроиться в работу основной программы. Работа библиотеки без инициализации должна сопровождаться крахом с выводом доходчивого сообщения о причине падения.
Дополнительным плюсом является повышенная гибкость для пользователя библиотеки, предоставляющая ему больше контроля.
Неприятность заключается в том, что функции вроде strdup(), уже не заставить использовать чужой malloc(), о чем можно забыть при написании библиотеки. Так что все еще остается простор для ошибок.
Есть в ней функции, возвращающие указатели на динамически выделенные массивы стандартного типа с помощью malloc(), по большей части это обычные Си-строки. Я рассчитывал, что освобождать эти данные из кода основной программы нужно стандартной функцией free(). Это разумно для компонентной системы, библиотека и программа, ее использующая, динамически подключаются к одной и той же стандартной библиотеке, в результате чего используют согласованные средства для выделения и освобождения памяти.
Основная разработка ведется в Linux, и в ней нарушения этого принципа не наблюдалось. Для любых доступных комбинаций компиляторов (gcc, clang, tcc, tendracc) для библиотеки и приложения никаких накладок не было замечено. Собранная библиотека оказалась переносима между разными версиями дистрибутива.
А вот в Windows меня ждало небольшое разочарование. Библиотеку для нее собирали средствами mingw, а приложение - с помощью Visual Studio. Несмотря на заверения о том, что mingw использует родной для Windows c-runtime, какую-то несовместимость он вносит. При попытке средствами программы освободить память, выделенную в библиотеке, происходит крах.
Есть категоричное мнение, что при правильной архитектуре освобождать нужно теми же средствами, что использовались для выделения. То есть в пару к функции, возвращающей указатель на динамически созданный объект, должна быть создана функция его освобождения. Тогда проблемы рассогласованности не возникнет.
Разумно, тем не менее, я считаю такой подход не всегда адекватным.
Он выглядит логично для сложных структур, но абсурдно для массива литер. Согласно нему создателям функций вроде strdup и asprintf, нужно было создать и strdupfree, asprintfree. Это может приводить к лишним накладным расходам как по времени выполнению, так и по написанию кода, а также дополнительным ошибкам. К примеру, если была получена выделенная строка, которую нужно передать в структуру - ее владельца, то туда же надо передать и информацию о способе освобождения строки, или сделать копию, а оригинал соответственно освободить. Сколько интересных возможностей вместо простой передачи указателя.
Проблема не исчерпывается только выделением памяти, есть ведь и другие ресурсы. Можно ли читать файл в библиотеке, открытый средствами программы? Или нужно предоставить библиотечную обертку над системной функцией для открытия файла? Что делать, если файл недоступен по имени?
Получается, что библиотека плохо встраивается в остальной код, и некоторые задачи могут выродиться в сплошное согласование вызовов функций, что может поставить вопрос об их целесообразности.
Поэтому мне больше по душе расчет на согласованность runtime`ов. Но что делать, если это нельзя гарантировать? Я нашел для себя приемлемое решение.
Нужна функция инициализации, заключающейся в том, чтобы передать из основной программы указатели на спорные функции, используемые в библиотеке. Именно их и будет вызывать библиотека вместо системных, что позволит ей тесно встроиться в работу основной программы. Работа библиотеки без инициализации должна сопровождаться крахом с выводом доходчивого сообщения о причине падения.
Дополнительным плюсом является повышенная гибкость для пользователя библиотеки, предоставляющая ему больше контроля.
Неприятность заключается в том, что функции вроде strdup(), уже не заставить использовать чужой malloc(), о чем можно забыть при написании библиотеки. Так что все еще остается простор для ошибок.
Комментариев нет:
Отправить комментарий