LINUX.ORG.RU

Обработка ошибок в библиотеке на C


0

3

Есть библиотека на plain C, которая работает с неким форматом файлов. Интерфейс простой:

typedef struct mylib {
  int mylib_errno;
  ...
} *mylib_t;

mylib_t lib = mylib_open("filename", ...);
somecount = mylib_getsomecount(lib, ...);
if (somecount < 0)
  errx(1, "cannot get some count: %s\n", mylib_strerror(mylib_getlasterror(lib)));

mylib_close(lib);

Т.е. в struct mylib хранится errno в котором могут быть как положительные значения (от системных вызовов и функций из libc) так и отрицательные от самой библиотеки.

Это отлично работает, но только когда в mylib_open ничего сложного не делается (тогда если возвращается NULL, это однозначно malloc вернул ENOMEM) и когда в качестве someval можно передать невозможное значение (ну это не особо проблема, потому что если нельзя, то можно передавать через указатель на манер stat(2)). Но вот нужно в mylib_open сделать что-то нетривиальное, и потом узнать что случилось. Вопрос: как лучше построить интерфейс в общем случае?

Вариант 1:

typedef int mylib_code_t;

mylib_code_t mylib_open(mylib_t *);
mylib_code_t mylib_getsomeval(mylib_t, int *)

Т.е. выходные данные всегда передавать через указатели, а возвращать всегда код ошибки (или MYLIB_OK). Это не нравится по той причине, что так пользователю вероятнее забыть что-то освободить, т.е. вызвать mylib_open(&lib) при уже открытом lib уже открыт. Все-таки с lib = mylib_open() это легче заметить. Далее, нельзя написать в одну строку struct mylib *lib = mylib_open() и общее несогласование интерфейса с обычными библиотечными функциями, которые возвращают данные а не коды.

Вариант 2:

Хранить свой глобальный errno. Описанных выше проблем нет, но имеем проблемы с многопоточными программами. С другой стороны, если есть портабельный способ так объявить mylib_errno, чтобы при сборке с потоками он был в TLS, а без потоков просто глобальным, ИМХО это было бы самое оно.

Вариант 3:

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

Идеи? Особенно интересует второй вариант. Есть мысли про установку обработчика ошибки + setjmp, но а коде который это использует невозможно разбираться, да и jmpbuf надо передавать опять таки. Т.е. это ухудшенный вариант 1.

★★★★★

Почитал хедеры разных библиотек, такие идеи еще появились:

Вариант 4:

typedef struct mylib {
  ...
} mylib_t;

mylib_open(mylib_t *);
data = mylib_getdata(mylib_t *);

Т.е. библиотекой структуру mylib не выделяем, а пользуемся готовой, остальной интрфейс как был (errno в структуре). Тогда всегда есть куда положить код ошибки, но один из минусов первого варианта остаётся. Плюс можно вызвать функцию на неинициализированную структуру.

Вариант 5:

Устанавливать обычный системный errno. Плюс - после сисколлов не надо сохранять нигде errno (хотя если нужно, например, после провалившегося lseek закрыть файл, придется наоборот не только сохранять но и восстанавливать), ошибки будет видно и через обычный strerror (текста он, конечно, не напишет, но номер будет), ну и mylib_strerror никуда не денется.

slovazap ★★★★★
() автор топика

>С другой стороны, если есть портабельный способ так объявить mylib_errno, чтобы при сборке с потоками он был в TLS, а без потоков просто глобальным, ИМХО это было бы самое оно.

http://gcc.gnu.org/onlinedocs/gcc-4.2.4/gcc/Thread_002dLocal.html

Но это, во-первых, не переносимо дальше gcc (хотя может icc в линуксе и умеет подобное). Во-вторых, придется делать две версии библиотеки: одна с __thread int mylib_errno, а другую просто с int mylib_errno.

linuxfan
()
Ответ на: комментарий от arsi

Нет, такие компилер-спицифичные костыли в простенькой библиотеке будут выглядеть дико. Значит вариант 2 отметается.

slovazap ★★★★★
() автор топика

errno и вариации - говно.

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

См. например про HRESULT в винде: http://msdn.microsoft.com/en-us/library/ms690088%28VS.85%29.aspx

Это не нравится по той причине, что так пользователю вероятнее забыть что-то освободить, т.е. вызвать mylib_open(&lib) при уже открытом lib уже открыт. Все-таки с lib = mylib_open() это легче заметить.


Нет смысла считать пользователя дураком, и делать соответствующие попытки защиты от дурака, если предполагается, что он будет писать на Си, или, как минимум дергать Си через FFI. Потому что Си и так предоставляет миллион способов отстрелить себе ногу.

Далее, нельзя написать в одну строку struct mylib *lib = mylib_open()

И правильно. Потому что так надо писать на высокоуровневых функциональных языках, а не на Си(кстати, возвращать указатель - дико дурной тон, если что).

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

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

Love5an
()
Ответ на: комментарий от baverman

>Потому что неизвестно, нужно ли его освобождать или нет.

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

Dimanc ★★
()
Ответ на: комментарий от baverman

>Вторая причина: пользователя библиотеки не устраивает твой способ выделения памяти.

Ему не нравится динамическое выделение или что?

Dimanc ★★
()
Ответ на: комментарий от Dimanc

Ему не нравится динамическое выделение или что?

Причем тут нравиться/не нравиться? Если есть несколько способов выделить память, автор библиотеки не должен решать за пользователя. Ведь даже в куче можно выделять по разному.

baverman ★★★
()
Ответ на: комментарий от baverman

>Если есть несколько способов выделить память, автор библиотеки не должен решать за пользователя.

Ну хорошо. Вернемся к первоначальному вопросу: почему «возвращать указатель - дико дурной тон»? В частности, если это указатель на уже существующий кусок памяти?

Dimanc ★★
()
Ответ на: комментарий от Love5an

>>Далее, нельзя написать в одну строку struct mylib *lib = mylib_open()

кстати, возвращать указатель - дико дурной тон, если что.

это где тебя такому бреду учат?

Deleted
()
Ответ на: комментарий от baverman

>Потому что неизвестно, нужно ли его освобождать или нет.

еще один.

обоим двум домашнее задание - протащить через стек гигабайтный массив.

Deleted
()
Ответ на: комментарий от Dimanc

В частности, если это указатель на уже существующий кусок памяти?

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

baverman ★★★
()
Ответ на: комментарий от Deleted

протащить через стек гигабайтный массив.

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

baverman ★★★
()
Ответ на: комментарий от baverman

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

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

Deleted
()
Ответ на: комментарий от baverman

Потому что неизвестно, нужно ли его освобождать или нет.

Обычно описано в документации, да и часто интуитивно понятно.

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

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

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

hunt
()
Ответ на: комментарий от hunt

>> кстати, возвращать указатель - дико дурной тон, если что

Почему и как надо?

Тут скорее проблема: «Кто должен выделять и освобождать память общих структур?»

ИМХО оптимальный вариант для большинства случаев, когда вызывающий код выделяет память для этих структур.

pathfinder ★★★★
()
Ответ на: комментарий от hunt

>Обычно описано в документации, да и часто интуитивно понятно.

Ты серьезно думаешь, что если грабли описать в документации, то они перестанут быть граблями?

pathfinder ★★★★
()
Ответ на: комментарий от hunt

>Записывать в уже выделенную память - не всегда известно сколько нужно будет памяти...

Зато всегда можно спросить.

pathfinder ★★★★
()
Ответ на: комментарий от Deleted

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

Ну бред же. К чему этот поток сознания приурочен — не совсем понятно.

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

А это вообще перл, причем тут это уже непонятно совсем.

baverman ★★★
()
Ответ на: комментарий от hunt

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

Сестра, помни про стек.

Записывать в уже выделенную память - не всегда известно сколько нужно будет памяти.

Для этого нужна функция, которая скажет нужный размер. На winapi не писал что ле?

да и избыточные действия

Ты на Си пишешь или на чем? Развели тут институт благородных девиц.

baverman ★★★
()
Ответ на: комментарий от baverman

>Ну бред же. К чему этот поток сознания приурочен — не совсем понятно.

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

А это вообще перл, причем тут это уже непонятно совсем.

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

Deleted
()
Ответ на: комментарий от pathfinder

Ты серьезно думаешь, что если грабли описать в документации, то они перестанут быть граблями?

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

ИМХО оптимальный вариант для большинства случаев, когда вызывающий код выделяет память для этих структур.

Опять согласен, в общем случае это самый гибкий вариант, если известен объём требуемой памяти.

Зато всегда можно спросить.

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

hunt
()
Ответ на: комментарий от Dimanc

>А почему?

<troll face> потому, что это пишет лиспер

annulen ★★★★★
()
Ответ на: комментарий от Deleted

Если ты не понял, то у меня только один пойнт — библиотека не должна выделять память, есть возражения?

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

baverman ★★★
()
Ответ на: комментарий от baverman

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

Но вроде как остаются варианты с изначально неизвестным объёмом данных.

Сестра, помни про стек.

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

Ты на Си пишешь или на чем? Развели тут институт благородных девиц.

Да хоть на асме, чем меньше код и структуризированней, тем лучше.

hunt
()
Ответ на: комментарий от hunt

Но вроде как остаются варианты с изначально неизвестным объёмом данных.

Да, есть нюансы, кто спорит?

baverman ★★★
()
Ответ на: комментарий от hunt

чем меньше код и структуризированней, тем лучше

Предоставление участка памяти клиентом для записи библиотечной функцией — что может быть структуризованней? Явность в таких делах превыше всего.

baverman ★★★
()

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

Corey
()
Ответ на: комментарий от Deleted

Что за бред? Какой гигабайт через стек? Зачем??

//инициализирует buf int init(struct *buf, options options);

Память выделятся пользователем, как ему удобно, и освобождается им же

Но это блин все равно не отменяет необходимость деструктора! Так что про «непонятно освобождать или нет» это точно такой же бред. Даже если ф-ция возвращает int есть огромная вероятность что надо вызвать какой-то close на этот int чтобы она освободила ресурсы.

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

2 Love5an разработчики stdio (fopen) - лохи, жалко ты с ними вместе не работаешь.

OxiD ★★★★
()

Делай как в lib pthread, возвращай код ошибки. Главное везде все делай в одном стиле. А юзер в любом случае может забыть что-то освободить.

Лучше добавь проверку переменной окружения DEBUG и при ней выводи сообщения.

OxiD ★★★★
()
Ответ на: комментарий от OxiD

>2 Love5an разработчики stdio (fopen) - лохи, жалко ты с ними вместе не работаешь.

Нашел пример, блеать.

FILE* вполне можно считать не за «указатель на структуру FILE», а за файловый дескриптор. Типа как HANDLE виндовый.

либо юзать ЯП где деструктор автоматически вызывается когда переменная из области видимости выходит.


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

Love5an
()
Ответ на: комментарий от hunt

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

Love5an
()
Ответ на: комментарий от hunt

>Но вроде как остаются варианты с изначально неизвестным объёмом данных.

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

Love5an
()
Ответ на: комментарий от Love5an

>В Windows API в этом плане хорошо сделано - там либо существует отдельная функция, вызов которой возвращает размер необходимого куска памяти, либо же его возвращает сама основная функция при определенной комбинации аргументов(передаваемый указатель - NULL, например).

Вам уже сказали, что это может быть как невозможно, так и требовать большого количества ресурсов. //я не знаю, что там в winapi

Dimanc ★★
()
Ответ на: комментарий от Dimanc

>//я не знаю, что там в winapi
А стоило бы. Т.е. поизучать нормальные сишные api.

Love5an
()
Ответ на: комментарий от Dimanc

>Например, в протоколе передачи данных не предусмотрен размер пакета

И что тут?
Читаем в буфер по кусочкам. Буфер заранее известного размера.

Love5an
()

> И правильно. Потому что так надо писать на высокоуровневых функциональных языках, а не на Си(кстати, возвращать указатель - дико дурной тон, если что).

Я знаю. У меня не возвращаются указатели - это я сначала хотел упростить пример кода, потом передумал и сделал как у меня, но одну строчку забыл. Везде возвращается opaque тип mylib_t или mylib_чтото_t, который на деле указатель, но пользователь этого не видит.

Так сделай у всех функций возврат параметров через указатели.

Обломно как-то один int возвращать через указатель. Кроме того, так возникает проблема с приведением типов - может пользователю он нужен во float или int64_t, а не int.

Это хороший стиль.

Имхо, хороший стиль у POSIX. Там где можно вернуть данные, они возвращаются сразу (open), там где нет, заполняется превыделенная структура (stat). Ошибки сбоку, кому надо - тот пользуется.

Пользователя библиотеки не устраивает твой способ выделения памяти

Ну это имхо можно уже сделать указателями на функции в глобальных переменных, если приспичит. Пока это нахрен никому не упёрлось и я просто использую malloc и free.

Кстати, в библиотеках я чаще вижу стиль #1. Поэтому (ну и из-за других фичей типа поддержки пользовательского аллокатора) ими пользоваться вообще невозможно - можно начать с «супергибких» png и jpeg.

библиотека не должна выделять память, есть возражения

К реальному миру никакого отношения не имеет.

slovazap ★★★★★
() автор топика

> В Windows API в этом плане хорошо сделано - там либо существует отдельная функция, вызов которой возвращает размер необходимого куска памяти, либо же его возвращает сама основная функция при определенной комбинации аргументов(передаваемый указатель - NULL, например).

А вот это, кстати, всегда racy.

slovazap ★★★★★
() автор топика
Ответ на: комментарий от Love5an

Читаем в буфер по кусочкам. Буфер заранее известного размера.

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

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

hunt
()
Ответ на: комментарий от hunt

>Понятно, что всё равно придётся читать данные, по мере надобности увеличивая буфер

Вот увеличивать буфер или один и тот же реюзать, это уже должно быть на стороне пользователя.

если не инкапсулировать это в библиотеке, то получится очень неудобный и нестройный интерфейс.


Пользователю должна быть доступна как раз только операция чтения в некий буфер.

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


Множество вызовов функций аллокации на производительности скажется еще хуже.

А проблему с выбором способа выделения памяти решать регистрацией аллокатора.


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

Love5an
()
Ответ на: комментарий от Love5an

Пользователю должна быть доступна как раз только операция чтения в некий буфер.

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

Множество вызовов функций аллокации на производительности скажется еще хуже.

Вызовы для выделения памяти всё равно будут (у вызывающего или вызываемого).

Вот увеличивать буфер или один и тот же реюзать, это уже должно быть на стороне пользователя.

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

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

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

hunt
()

я когда таким занимался делал так:
lib_open принимает какой-то идентификатор (скажем, имя файла + параметры), возвращает целое число - дескриптор либы. Если ноль - ошибка.
все остальные ф-ии (их много было) возрващают enum с отрицательными значениями констант. Это код ошибки, или ноль, если всё хорошо. Принимают дескриптор либы + параметры (указатели всякие там, или другая хрень. для разных ф-ий по разному).
lib_close принимает дескриптор либы, возвращает void.
get_error_string ещё была. ты ей enum с ошибкой, а она тебе строку конктретного размера с текстовым описанием ошибки (в стеке, с malloc/free не заморачивался).

+: схема проста и понятня. очень похоже на то как open/read/write/close/... работают. много либов можно открыть одновременно (если запрограмированно соответствующим образом).
-: непонятно, почему lib_open неотработал (в случае ошибки).

nanoo_linux
()
Вы не можете добавлять комментарии в эту тему. Тема перемещена в архив.