Идеи по усовершенствованию реализации библиотек на языке Си

Расти Рассел (Paul "Rusty" Russel), являющийся разработчиком таких систем как netfilter/iptables и lguest, в своем блоге поделился идеями по разработке библиотек на языке Си, в контексте развития своего нового проекта CCAN - аналога архива CPAN для языка Си, в котором представлены небольшие модули с реализацией определенных функций.

Поводом для написания статьи послужил анализ исходных текстов библиотеки для обработки динамических массивов разреженных данных по хеш-индексу Judy и свободного аудио-кодека codec2:

  1. Подготовка существующего кода для пакетирования в библиотеку нарушает наглядность, в основном, из-за большого объема инфраструктуры пакетной информации, требуемой для autoconf, и, даже если исходные тексты, как это часто делается, поместить в src/, порядка не добавится. Например, код Judy состоит из 29 файлов, а к этому добавляется еще 33 файла инфраструктуры - automake, Authors, Chargelog, INSTALL, COPYING и 13 файлов README.
  2. Повторяющееся переопределение стандартных типов (типа typedef long или typedef void*) ухудшает читаемость кода, вместо желаемого обеспечения безопасности типов. Так же следует избегать помещения определения структур и объединений (struct, union) в заголовочные файлы (public header files), доступные пользователю библиотеки.
  3. Не стоит выдумывать сложные наименования библиотечных вызовов, напротив, таковые должны быть как можно более короткими, но - запоминаемыми: например, если в библиотеке есть функции создания и разрушения контекста, достаточно их назвать mylib_new() и mylib_free() - в этом случае каждому будет понятно что это такое и что с ним делать.
  4. Идентично сказанному, если для работы библиотеки требуется инициализация, то вызов должен называться mylib_init() или mylib_setup().
  5. Вызовы к библиотеке должны быть реализованы так, чтобы толкование к их использованию было однозначным: например, если успешное завершение вызова mylib_init() необходимо для работы последующих вызовов к библиотеке, то любое нарушение этого правила, должно приводить к аварийному завершению программы, скажем через abort() или assert(). Если нужно, или хочется дать пользователю возможность обработки фатальных ошибок библиотеки, сделайте это по аналогии макроса NDEBUG в assert(). В любом случае, обработка пользовательских ошибок не должна тратить время разработчика библиотеки.
  6. Если единственная предполагаемая ошибка при выполнении библиотечного кода - это "нехватка памяти" (out-of-memory), оставьте обработку ошибки пользователю (при этом можно практически быть уверенным в том, что пользователи этого делать не будут, и в этом случае можно ссылаться на правило - "не проверенный тестами код всегда содержит ошибки"). При возникновении ошибок такого типа обычно возвращается NULL в качестве результата. Также можно предложить пользователю зарегистрировать свой обработчик ошибок и пользоваться функционалом setjmp(). Однако это не должно делаться в библиотечном коде.
  7. Всегда возвращайте ошибку пользователю при ее возникновении в коде разрабатываемой библиотеки. Если ошибочная функция возвращает указатель, использование NULL, как индикатора ошибки - обычная практика. Другие типы ошибок также могут возвращать специальные указатели, но чаще всего применяются более простые способы.
  8. Следует тщательно подходить к разработке интерфейса пользователя: лучше его сделать простым и понятным для 90% пользователей (оставшиеся 10% не будут пользоваться им вообще, или переделают по-своему), чем разработать сложный многоуровневый интерфейс и остаться с половиной (из возможного числа) пользователей, так как для другой половины будет проще написать свой собственный интерфейс, чем тратить время на исследование написанного вами.
  9. Имеется стандартное правило для наименования низкоуровневых функций - когда имя начинается с символа подчеркивания (например, _exit()). Следует избегать использования низкоуровневых функций в библиотечном интерфейсе, по причине описанной в предыдущем пункте. Вполне вероятно, что стоит подумать над общей архитектурой и разработать две библиотеки с простым интерфейсом (где одна будет пользоваться функциями другой), вместо одной, но с сложным, многоуровневым интерфейсом.
  10. Вместо использования пространств имен (namespaces), придумайте один префикс к всем поименованным объектам вашей библиотеки, например, opt_, judy или talloc_. Конечно, есть небольшая вероятность что имя объекта в вашей библиотеки пересечется с каким-нибудь другим объектом в пользовательской программе, но все же лучше оставить этот риск, чем давать сложные, практически уникальные, имена типа ccan_rusty_russell_opt_ prefixes, что безусловно добавит головной боли пользователю при использовании вашей библиотеки.
  11. Заголовочные файлы (headers), предоставляемые пользователю, должны быть легко читаемы. Комментарии, в этих файлах могут быть использованы системами самодументирования (как например kerneldoc или doxygen) для поддержания актуальной документации разрабатываемой библиотеки.
  12. На стадии разработки не стоит утруждаться вопросами портирования кода на другие платформы. Если вы сами не собираетесь тестировать код на платформе, отличной от той, где велась разработка, оставьте это на потом. Может быть, это сделает тот, кому это нужно.
  13. Не стоит загромождать заголовочные файлы конструкциями C++ - если вдруг ваша библиотека понадобится разработчику на C++, будет не сложно такую конструкцию добавить непосредственно в код разрабатываемой пользовательской программы, непосредственно перед #include вашего заголовочного файла.
  14. Следует помнить, что в заметке говорится о разработке на языке C, с принятыми в этом языке идентификаторами из строчных букв, с возможными разделителями из символа подчеркивания. Идентификаторы из прописных и строчных букв (BumbyCaps) в языке C не приняты.

В какой-то степени все сказанное касается CCAN и размышлений о том "как убедить людей использовать этот код". Все, что находится на CCAN, сделано на основании Золотого Правила CCAN - "наш код не должен быть уродливым" - CCAN Golden Rule: Our Code Should Not Be Ugly.

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

  • В пакете должен быть файл config.h. Файл включается во все исходные модули собираемой библиотеки, как правило, содержит некоторые специфичные для инсталляции макроопределения в виде #define HAVE_FOO
  • _info файл - на самом деле является C-кодом, в котором [в комментариях] в формате kerneldoc содержатся сведения об авторе, описание пакета и прочая мета-информация, предназначенная для чтения человеком. Этот файл также содержит некоторый исполнимый код, в котором указываются зависимости пакета.
  • "Главный" заголовочный файл, поименованный также как собираемый модуль, с суффиксом ".h", в котором содержится пользовательская документация к пакету, например, описание API для библиотеки.
  • Директория test, содержащая набор примеров, иллюстрирующих функциональность пакета в формате run-*
  • Может быть определен макрос DEBUG, предназначенный для получения отладочной информации, часто в ущерб производительности. Заметим, что это никак не касается функциональности проверок ошибок NDEBUG, о которой упоминалось выше.

Иными словами, это дает возможность сразу увидеть описание модуля при просмотре CCAN.

На CCAN также имеется небольшая утилита namespacize (весьма примитивная, нуждается в доработке), которая предназначена для модификации кода в контексте пространств имен. В настоящее время утилита добавляет префикс ccan_ к вызовам модуля, модифицируя соответственно, все зависимые модули. Таким образом можно решить проблему уникальности имен.

Дополнительная утилита ccanlint, которая выполняет семантический анализ кода, и, в идеале, должна выявить все, о чем говорилось в этом тексте. Разумеется, "интеллекта" утилиты недостаточно, чтобы проверять интерфейсы, однако она вполне хорошо справляется с выявление kerneldoc-комментариев, и даже выделяет из кода и компилирует примеры - фрагменты кода для проверки на наличие "лишних", т.е никогда не выполняемых фрагментов кода в модуле (bitrotted code pieces).

Полный текст статьи читайте на OpenNet