Подводные камни дизайна API в C [закрыто]

10

Какие недостатки сводят вас с ума в C API (включая стандартные библиотеки, сторонние библиотеки и заголовки внутри проекта)? Цель состоит в том, чтобы выявить ошибки проектирования API в C, чтобы люди, пишущие новые библиотеки C, могли учиться на ошибках прошлого.

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

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

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

Джои Адамс
источник
3
Джои, этот вопрос граничит с тем, что он не конструктивен, когда просит составить список того, что люди ненавидят. Здесь есть потенциал для того, чтобы вопрос был полезен, если ответы объясняют, почему методы, на которые они указывают, являются плохими, и предоставляют подробную информацию о том, как их улучшить. Для этого, пожалуйста, перенесите свой пример с вопроса на собственный ответ и объясните, почему это проблема / как mallocее исправит строка. Я думаю, что хороший пример с первым ответом может помочь этому вопросу процветать. Спасибо!
Адам Лир
1
@ Анна Лир: Спасибо, что сказали мне, почему мой вопрос был проблематичным. Я пытался сохранить его конструктивным, попросив пример и предложил альтернативу. Я думаю, мне действительно нужно было несколько примеров, чтобы показать, что я имел в виду.
Джои Адамс
@ Джои Адамс Посмотри на это так. Вы задаете вопрос, который должен автоматически решать проблемы C API. Где сайты, такие как StackOverflow, были спроектированы так, чтобы можно было легко находить и решать наиболее распространенные проблемы с программированием. StackOverflow, естественно, приведет к списку ответов на ваш вопрос, но в более структурированном и удобном для поиска виде.
Эндрю Т Финнелл
Я проголосовал, чтобы закрыть свой вопрос. Моя цель состояла в том, чтобы получить набор ответов, которые могли бы служить контрольным списком для новых библиотек Си. Все три ответа до сих пор используют такие слова, как «противоречивый», «нелогичный» или «запутанный». Невозможно объективно определить, нарушает ли API какой-либо из этих ответов.
Джои Адамс

Ответы:

5

Функции с противоречивыми или нелогичными возвращаемыми значениями. Два хороших примера:

1) Некоторые функции Windows, которые возвращают HANDLE, используют NULL / 0 для ошибки (CreateThread), некоторые используют INVALID_HANDLE_VALUE / -1 для ошибки (CreateFile).

2) Функция POSIX 'time' возвращает '(time_t) -1' в случае ошибки, что на самом деле нелогично, поскольку time_t может иметь тип со знаком или без знака.

Дэвид Шварц
источник
2
На самом деле, time_t (обычно) подписано. Однако называть «недействительным» 31 декабря 1969 года довольно нелогично. Я думаю, что 60-е годы были грубыми :-) Если серьезно, решение было бы вернуть код ошибки и передать результат через указатель, как в: int time(time_t *out);и BOOL CreateFile(LPCTSTR lpFileName, ..., HANDLE *out);.
Джои Адамс
Точно. Это странно, если time_t не подписано, и если time_t подписано, это делает один раз недействительным посреди океана действительных.
Дэвид Шварц
4

Функции или параметры с неописательными или утвердительно запутывающими именами. Например:

1) CreateFile в Windows API фактически не создает файл, он создает дескриптор файла. Он может создать файл, так же как и «open», если его попросить через параметр. Этот параметр имеет значения с именами «CREATE_ALWAYS» и «CREATE_NEW», имена которых даже не намекают на их семантику. (Означает ли 'CREATE_ALWAYS', что он потерпит неудачу, если файл существует? Или он создает новый файл поверх него? Означает ли «CREATE_NEW», что он всегда создает новый файл и дает сбой, если файл уже существует? Или он создает новый файл? файл поверх него?)

2) pthread_cond_wait в POSIX pthreads API, который, несмотря на свое имя, является безусловным ожиданием.

Дэвид Шварц
источник
1
Конд в pthread_cond_waitне означает , что «условно ждать». Это относится к тому факту, что вы ожидаете переменную условия .
Джонатон Рейнхарт
Право, это безусловное ожидание для состояния.
Дэвид Шварц
3

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

Это входит в различные формы и ароматы, включая, но не ограничиваясь:

  • void* злоупотребление

  • использование intв качестве дескриптора ресурса (пример: библиотека CDI)

  • строковые аргументы

Чем более различные типы (= нельзя использовать полностью взаимозаменяемо) сопоставляются с удаленным типом того же типа, тем хуже. Конечно, решение состоит в том, чтобы просто обеспечить безопасные непрозрачные указатели по типу (пример C):

typedef struct Foo Foo;
typedef struct Bar Bar;

Foo* createFoo();
Bar* createBar();

int doSomething(Foo* foo);
void somethingElse(Foo* foo, Bar* bar);

void destroyFoo(Foo* foo);
void destroyBar(Bar* bar);
cmaster - восстановить монику
источник
2

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

Например, getcwd запрашивает предоставленный пользователем буфер и его размер. Это означает, что приложение должно либо установить произвольное ограничение на текущую длину каталога, либо сделать что-то вроде этого ( из CCAN ):

 /* *This* is why people hate C. */
len = 32;
cwd = talloc_array(ctx, char, len);
while (!getcwd(cwd, len)) {
    if (errno != ERANGE) {
        talloc_free(cwd);
        return NULL;
    }
    cwd = talloc_realloc(ctx, cwd, char, len *= 2);
}

Мое решение: вернуть mallocстроку ed. Это просто, надежно и не менее эффективно. За исключением встроенных платформ и старых систем, mallocна самом деле это довольно быстро.

Джои Адамс
источник
4
Я бы не назвал это плохой практикой, я бы назвал это хорошей практикой. 1) Это настолько распространено, что ни один программист не должен удивляться этому. 2) Это оставляет выделение вызывающей стороне, что исключает многочисленные возможности ошибок утечки памяти. 3) Совместимо со статически размещенными буферами. 4) Это делает реализацию функции более чистой, функция, вычисляющая некоторую математическую формулу, не должна касаться чего-то совершенно не связанного, такого как динамическое распределение памяти. Вы думаете, что main становится чище, но функция становится более грязной. 5) malloc даже не разрешен во многих системах.
@Lundin: Проблема в том, что это приводит к тому, что программисты создают ненужные жестко заданные ограничения, и им приходится изо всех сил стараться этого не делать (см. Пример выше). Это хорошо для таких вещей, как snprintf(buf, 32, "%d", n), когда длина вывода предсказуема (конечно, не более 30, если только intона не очень велика в вашей системе). Действительно, malloc недоступен во многих системах, но для настольных и серверных сред он есть, и он работает очень хорошо.
Джои Адамс
Но проблема в том, что функция в вашем примере не устанавливает жестко заданных ограничений. Подобный кодекс не является обычной практикой. Здесь main знает вещи о длине буфера, которые должна знать функция. Все это говорит о плохом дизайне программы. Кажется, что Main не знает, что вообще делает функция getcwd, поэтому он использует какое-то распределение "грубой силы", чтобы выяснить это. Где-то интерфейс между модулем, в котором находится getcwd, и вызывающей стороной запутан. Это не означает, что этот способ вызова функций плох, наоборот, опыт показывает, что он хорош по причинам, которые я уже перечислил.
1

Функции, которые принимают / возвращают составные типы данных по значению или используют обратные вызовы.

Еще хуже, если указанный тип является объединением или содержит битовые поля.

С точки зрения вызывающего абонента C, это действительно нормально, но я не пишу на C или C ++, если это не требуется, поэтому я обычно звоню через FFI. Большинство FFI не поддерживают объединения или битовые поля, а некоторые (например, Haskell и MLton) не могут поддерживать структуры, передаваемые по значению. Для тех, кто может обрабатывать структуры по значению, по крайней мере Common Lisp и LuaJIT вынуждены идти по медленным путям - Интерфейс Common Foreign Function Lisp должен делать медленный вызов через libffi, а LuaJIT отказывается JIT-компилировать путь кода, содержащий вызов. Функции, которые могут вызывать обратные вызовы хостов, также запускают медленные пути в LuaJIT, Java и Haskell, поскольку LuaJIT не может скомпилировать такой вызов.

Деми
источник