Проектирование библиотек
В этом разделе термин клиентский код относится к приложениям и другим библиотекам, использующим данную библиотеку.
Управление состоянием
Глобальное состояние
Глобального состояния следует избегать.
Если это невозможно, глобальное состояние должно быть защищено блокировкой. В C/C++ можно использовать функции pthread_mutex_lock и pthread_mutex_unlock без компоновки с -lpthread, поскольку система предоставляет заглушки для непоточных процессов.
Для совместимости с fork эти блокировки должны захватываться и освобождаться во вспомогательных функциях, зарегистрированных с помощью pthread_atfork. Эта функция недоступна без -lpthread, поэтому вам нужно использовать dlsym или слабый символ для получения её адреса.
Если вам требуется защита от fork по другим причинам, следует сохранять идентификатор процесса и сравнивать его со значением, возвращаемым getpid, при каждом доступе к глобальному состоянию. (getpid не реализован как системный вызов и работает быстро.) Если значение изменилось, вы знаете, что вам необходимо пересоздать объект состояния. (Это, разумеется, должно сочетаться с блокировкой.)
Дескрипторы
Состояние библиотеки должно быть скрыто за завесой. Клиентский код должен получать только дескриптор. В C дескриптором может быть указатель на незавершённую struct. В C++ дескриптором может быть указатель на абстрактный базовый класс, или он может быть скрыт с помощью идиомы «указатель на реализацию».
Библиотека должна предоставлять функции для создания и уничтожения дескрипторов. (В C++ для уничтожения можно использовать виртуальные деструкторы.) Настоятельно рекомендуется соблюдать согласованность между созданием и уничтожением дескрипторов: если клиентский код создал дескриптор, он же несёт ответственность за его уничтожение. (Это не всегда возможно или удобно, поэтому иногда требуется передача владения.)
Использование дескрипторов гарантирует, что можно изменить способ представления состояния библиотекой прозрачно для клиентского кода. Это важно для упрощения обновлений безопасности и многих других изменений кода.
Не всегда необходимо защищать состояние за дескриптором с помощью блокировки. Это зависит от уровня потокобезопасности, который обеспечивает библиотека.
Объектная ориентированность
Классы должны быть либо спроектированы как базовые, либо должна быть исключена возможность их использования в качестве базовых (как final-классы в Java). Классы, которые не предназначены для наследования, но используются в качестве базовых, создают потенциальные проблемы с обслуживанием, поскольку трудно предсказать, как клиентский код отреагирует на добавление, переупорядочивание или удаление вызовов виртуальных методов.
Виртуальные функции-члены могут использоваться в качестве обратных вызовов. См. Обратные вызовы о некоторых связанных с этим сложностях.
Обратные вызовы
Код высшего порядка трудно анализировать как людям, так и компьютерам, поэтому его следует избегать. Часто интерфейс на основе итераторов (функция библиотеки, которая вызывается клиентским кодом многократно и возвращает поток событий) приводит к лучшему дизайну, который проще документировать и использовать.
Если обратных вызовов избежать невозможно, следуйте некоторым рекомендациям.
В современном коде C++ для обратных вызовов следует использовать объекты std::function.
В старом коде C++ и в коде C все обратные вызовы должны иметь дополнительный параметр замыкания типа void *, значение которого может быть указано клиентским кодом. По возможности значение параметра замыкания должно предоставляться клиентским кодом одновременно с регистрацией конкретного обратного вызова (или указываться как аргумент функции). Если один параметр замыкания используется для нескольких обратных вызовов, гибкость значительно снижается, и конфликты между различными частями клиентского кода, использующими один и тот же объект библиотеки, могут быть неразрешимы. В некоторых случаях имеет смысл предоставить обратный вызов для отмены регистрации, который можно использовать для уничтожения параметра замыкания, когда обратный вызов больше не используется.
Обратные вызовы могут генерировать исключения или вызывать longjmp. По возможности все объекты библиотеки должны оставаться в допустимом состоянии. (Все дальнейшие операции над ними могут завершаться ошибкой, но должна быть возможность освободить их без утечек ресурсов.)
Наличие обратных вызовов ставит вопрос о том, являются ли функции, предоставляемые библиотекой, реентерабельными. Если библиотека не была спроектирована для такого использования, возникнут проблемы, если функция обратного вызова использует функции той же библиотеки (особенно если они вызываются для одних и тех же объектов и манипулируют одним и тем же состоянием). При вызове обратного вызова библиотека может находиться в несогласованном состоянии. Реентерабельные функции писать сложнее, чем потокобезопасные (по определению, простая блокировка немедленно приведёт к взаимным блокировкам). Также трудно решить, что делать, когда запрашивается уничтожение объекта, который в данный момент обрабатывает обратный вызов.
Атрибуты процесса
Некоторые атрибуты являются глобальными и влияют на весь код в процессе, а не только на библиотеку, которая ими манипулирует.
-
переменные окружения (см. Доступ к переменным окружения)
-
umask
-
идентификаторы пользователей, групп и возможности
-
текущий рабочий каталог
-
обработчики сигналов, маски сигналов и доставка сигналов
-
блокировки файлов (особенно блокировки
fcntlведут себя неожиданно, и не только в многопоточной среде)
Код библиотеки должен избегать манипулирования этими глобальными атрибутами процесса. Он не должен полагаться на переменные окружения, umask, текущий рабочий каталог и маски сигналов, поскольку эти атрибуты могут быть унаследованы от недоверенного источника.
Кроме того, существуют очевидные общепроцессные аспекты, такие как макет виртуальной памяти, набор открытых файлов и динамически загружаемые объекты, но за исключением разделяемых объектов, ими можно манипулировать относительно изолированно.
Want to help? Learn how to contribute to Fedora Docs ›