Я оцениваю библиотеку, публичный API которой в настоящее время выглядит следующим образом:
libengine.h
/* Handle, used for all APIs */ typedef size_t enh; /* Create new engine instance; result returned in handle */ int en_open(int mode, enh *handle); /* Start an engine */ int en_start(enh handle); /* Add a new hook to the engine; hook handle returned in h2 */ int en_add_hook(enh handle, int hooknum, enh *h2);
Обратите внимание, что enh
это универсальный дескриптор, используемый в качестве дескриптора для нескольких различных типов данных ( движки и хуки ).
Внутренне, большинство из этих API, конечно, приводят «дескриптор» к внутренней структуре, которую они сделали malloc
:
engine.c
struct engine { // ... implementation details ... }; int en_open(int mode, *enh handle) { struct engine *en; en = malloc(sizeof(*en)); if (!en) return -1; // ...initialization... *handle = (enh)en; return 0; } int en_start(enh handle) { struct engine *en = (struct engine*)handle; return en->start(en); }
Лично я ненавижу прятать вещи за typedef
s, особенно когда это ставит под угрозу безопасность типов. (Учитывая enh
, как я узнаю, к чему это на самом деле относится?)
Поэтому я отправил запрос на извлечение, предложив следующее изменение API (после изменения всей библиотеки для соответствия):
libengine.h
struct engine; /* Forward declaration */
typedef size_t hook_h; /* Still a handle, for other reasons */
/* Create new engine instance, result returned in en */
int en_open(int mode, struct engine **en);
/* Start an engine */
int en_start(struct engine *en);
/* Add a new hook to the engine; hook handle returned in hh */
int en_add_hook(struct engine *en, int hooknum, hook_h *hh);
Конечно, это заставляет внутренние реализации API выглядеть намного лучше, устраняя приведения и поддерживая безопасность типов с точки зрения потребителя.
libengine.c
struct engine
{
// ... implementation details ...
};
int en_open(int mode, struct engine **en)
{
struct engine *_e;
_e = malloc(sizeof(*_e));
if (!_e)
return -1;
// ...initialization...
*en = _e;
return 0;
}
int en_start(struct engine *en)
{
return en->start(en);
}
Я предпочитаю это по следующим причинам:
- Добавлена безопасность типов
- Улучшена ясность типов и их назначения
- Удалены слепки и
typedef
с - Это следует за рекомендуемым образцом для непрозрачных типов в C
Тем не менее, владелец проекта отказался от запроса на извлечение (перефразировал):
Лично мне не нравится идея разоблачения
struct engine
. Я все еще думаю, что нынешний путь чище и дружелюбнее.Первоначально я использовал другой тип данных для дескриптора хука, но затем решил переключиться на использование
enh
, чтобы все виды дескрипторов использовали один и тот же тип данных, чтобы было проще. Если это сбивает с толку, мы, безусловно, можем использовать другой тип данных.Посмотрим, что другие подумают об этом пиаре.
Эта библиотека в настоящее время находится в стадии приватного бета-тестирования, поэтому не нужно беспокоиться о коде пользователя (пока). Кроме того, я немного запутал имена.
Чем непрозрачный дескриптор лучше именованной непрозрачной структуры?
Примечание: я задал этот вопрос в Code Review , где он был закрыт.
источник
Ответы:
Мантра «просто лучше» стала слишком догматичной. Простое не всегда лучше, если оно усложняет другие вещи. Сборка проста - каждая команда намного проще, чем команды языков более высокого уровня - и все же программы сборки более сложны, чем языки более высокого уровня, которые делают то же самое. В вашем случае унифицированный тип дескриптора
enh
упрощает типы за счет усложнения функций. Так как обычно типы проектов имеют тенденцию расти сублинейной скоростью по сравнению с его функциями, по мере того, как проект становится больше, вы обычно предпочитаете более сложные типы, если это может упростить функции - поэтому в этом отношении ваш подход кажется правильным.Автор проекта обеспокоен тем, что ваш подход - « разоблачение
struct engine
». Я бы объяснил им, что это не разоблачает саму структуру - только тот факт, что есть структура с именемengine
. Пользователь библиотеки уже должен знать об этом типе - он должен знать, например, что первый аргументen_add_hook
этого типа, а первый аргумент другого типа. Таким образом, это на самом деле делает API более сложным, потому что вместо наличия «сигнатурной» функции этих документов необходимо документировать ее где-то еще, и потому что компилятор больше не может проверять типы для программиста.Следует отметить одну вещь - ваш новый API делает пользовательский код немного более сложным, поскольку вместо написания:
Теперь им нужен более сложный синтаксис для объявления своего дескриптора:
Решение, однако, довольно простое:
и теперь вы можете прямо написать:
источник
struct
явно не приветствуются.typedef struct engine engine;
и использовалengine*
: введено наименьшее имя, и это делает очевидным, что он похож на ручкуFILE*
.Здесь, кажется, с обеих сторон путаница:
struct
имени не раскрывает его детали (только его существование)Преимущества использования дескрипторов, а не простых указателей, в языке, подобном C, заключаются в том, что передача указателя позволяет напрямую манипулировать указателем (включая вызовы
free
), тогда как передача дескриптора требует, чтобы клиент прошел через API для выполнения любого действия. ,Тем не менее, подход с использованием единственного типа дескриптора, определенного через a
typedef
, не является безопасным по типу и может вызвать много неприятностей.Таким образом, мое личное предложение состояло бы в том, чтобы перейти к безопасным ручкам, которые, я думаю, удовлетворили бы вас обоих. Это достигается довольно просто:
Теперь нельзя случайно пройти
2
как ручку и нельзя случайно передать ручку метле, где ожидается ручка для двигателя.Это ваша ошибка: прежде чем приступить к серьезной работе над библиотекой с открытым исходным кодом, свяжитесь с автором (-ами) / сопровождающим (-ями) для обсуждения изменений заранее . Это позволит вам обоим договориться о том, что делать (или не делать), и избегать ненужной работы и разочарований, которые в результате этого возникают.
источник
struct file
изint fd
. Это, конечно, излишне для пользовательской библиотеки IMO.3
) освобождается, затем создается новый объект и, к сожалению, ему3
снова назначается индекс . Проще говоря, трудно создать безопасный механизм жизни объектов в C, если подсчет ссылок (вместе с соглашениями об общих правах собственности на объекты) не включен в явную часть разработки API.Вот ситуация, когда нужна непрозрачная ручка;
Когда библиотека имеет два или более структурных типа, которые имеют одинаковую часть заголовка полей, как, например, «тип» в приведенном выше примере, можно считать, что эти структурные типы имеют общую родительскую структуру (например, базовый класс в C ++).
Вы можете определить часть заголовка как «struct engine», вот так;
Но это необязательное решение, потому что приведение типов необходимо независимо от использования движка struct.
Вывод
В некоторых случаях есть причины, по которым непрозрачные дескрипторы используются вместо непрозрачных именованных структур.
источник
switch
во-первых, использования «виртуальных функций», вероятно, идеально, и решает всю проблему.Наиболее очевидное преимущество подхода с ручками заключается в том, что вы можете изменять внутренние структуры, не нарушая внешний API. Конечно, вам все еще нужно изменить клиентское программное обеспечение, но, по крайней мере, вы не меняете интерфейс.
Другая вещь, которую он делает, - это возможность выбирать из множества возможных типов во время выполнения без необходимости предоставления явного интерфейса API для каждого из них. Некоторые приложения, такие как показания датчиков от нескольких различных типов датчиков, где каждый датчик немного отличается и генерирует немного разные данные, хорошо реагируют на этот подход.
Так как вы все равно будете предоставлять структуры своим клиентам, вы жертвуете немного безопасностью типов (которую все еще можно проверить во время выполнения) для намного более простого API, хотя и того, которое требует приведения.
источник
Дежавю
Я столкнулся с точно таким же сценарием, только с некоторыми тонкими различиями. В нашем SDK было много таких вещей:
Мое простое предложение состояло в том, чтобы он соответствовал нашим внутренним типам:
Для третьих лиц, использующих SDK, это не должно иметь никакого значения. Это непрозрачный тип. Какая разница? Он не влияет на совместимость с ABI * или исходными текстами, и для использования новых выпусков SDK требуется перекомпиляция плагина в любом случае.
* Обратите внимание, что, как указывает gnasher, на самом деле могут быть случаи, когда размер чего-то вроде указателя на struct и void * может фактически иметь другой размер, и в этом случае это повлияет на ABI. Как и он, я никогда не сталкивался с этим на практике. Но с этой точки зрения, то второй может реально улучшить переносимость в какой-то неясной контексте, так что это еще одна причина, в пользу второго, хотя, возможно, спорный вопрос для большинства людей.
Ошибки третьих сторон
Кроме того, у меня было даже больше причин, чем безопасность типов для внутренней разработки / отладки. У нас уже было несколько разработчиков плагинов, у которых были ошибки в их коде, потому что два одинаковых дескриптора (
Panel
иPanelNew
, т.е.) оба использовалиvoid*
typedef для своих дескрипторов, и они случайно передавали неправильные дескрипторы в неправильные места в результате простого использования SDK. Их ошибки также стоят огромного времени для внутренней команды разработчиков, так как они отправляют отчеты об ошибках с жалобами на ошибки в нашем SDK, и нам придется отлаживать плагин и обнаруживать, что это на самом деле было вызвано ошибкой в плагине, передающей неправильные дескрипторы. в неправильные места (что легко разрешается даже без предупреждения, когда каждый дескриптор является псевдонимом илиvoid*
для всего. Так что это на самом деле вызывает ошибки на стороне тех, кто используетvoid*
size_t
). Таким образом, мы без необходимости тратили свое время на предоставление услуг по устранению неполадок третьим сторонам из-за ошибок, вызванных с их стороны нашим стремлением к концептуальной чистоте в сокрытии всей внутренней информации, даже простых имен нашей внутреннейstructs
.Хранение Typedef
Разница заключается в том, что я предлагаю , что мы делаем стики до
typedef
сих пор, не иметь клиент запись ,struct SomeVertex
которые будут влиять на совместимость источника для будущих плагин релизов. Хотя мне лично нравится идея не определять типstruct
в C, с точки зрения SDK, этоtypedef
может помочь, поскольку все дело в непрозрачности. Так что я бы предложил ослабить этот стандарт только для публичного API. Для клиентов, использующих SDK, не должно иметь значения, является ли дескриптор указателем на структуру, целое число и т. Д. Для них важно только то, что два разных дескриптора не имеют псевдонимов одного типа данных, так что они не неправильно передать в неправильную ручку в неправильное место.Тип информации
Больше всего важно избегать кастинга для вас, внутренних разработчиков. Этот вид эстетики сокрытия всех внутренних имен от SDK - это концептуальная эстетика, которая сопряжена со значительными затратами на потерю всей информации о типах и требует от нас ненужного разбрасывания бросков в наши отладчики для получения критической информации. В то время как программист на C должен в значительной степени привыкнуть к этому на C, требовать этого без необходимости просто напрашивается на неприятности.
Концептуальные Идеалы
В общем, вы хотите остерегаться тех разработчиков, которые ставят какую-то концептуальную идею чистоты намного выше практических, повседневных потребностей. Это приведет к тому, что ваша кодовая база станет проще в поиске утопического идеала, заставляя всю команду избегать лосьона для загара в пустыне из-за страха, что это неестественно и может вызвать дефицит витамина D, в то время как половина команды умирает от рака кожи.
Пользовательские настройки
Даже со строгой точки зрения конечных пользователей, использующих API, они предпочли бы ошибочный API или API, который работает хорошо, но предоставляет какое-то имя, о котором они вряд ли могли бы заботиться взамен? Потому что это практический компромисс. Потеря информации о типах без необходимости вне общего контекста увеличивает риск ошибок, и из-за крупномасштабной кодовой базы в команде в течение нескольких лет закон Мерфи, как правило, вполне применим. Если вы чрезмерно увеличите риск ошибок, скорее всего, вы, по крайней мере, получите еще несколько ошибок. Это не займет много времени в большой команде, чтобы обнаружить, что все мыслимые человеческие ошибки в конечном итоге превратятся из потенциала в реальность.
Так что, возможно, это вопрос к пользователям. «Вы бы предпочли более шумный SDK или тот, который предоставляет некоторые внутренние непрозрачные имена, которые вам никогда не понадобятся?» И если кажется, что этот вопрос выдвигает ложную дихотомию, я бы сказал, что требуется больше опыта для всей команды в очень крупномасштабной обстановке, чтобы оценить тот факт, что более высокий риск ошибок в конечном итоге в конечном итоге приведет к появлению реальных ошибок. Не имеет значения, насколько разработчик уверен в том, чтобы избежать ошибок. В общекорпоративных условиях это помогает больше думать о самых слабых звеньях и, по крайней мере, о самых простых и быстрых способах предотвращения их срабатывания.
Предложение
Поэтому я бы предложил здесь компромисс, который все же даст вам возможность сохранить все преимущества отладки:
... даже ценой уничтожения шрифтов
struct
, это действительно нас убьет? Вероятно, нет, поэтому я рекомендую также немного прагматизма с вашей стороны, но в большей степени разработчику, который предпочел бы сделать экспоненциальную сложность отладки, используяsize_t
здесь, и приведение к / от целочисленного значения без веской причины, за исключением дальнейшего скрытия информации, которая уже % скрыт от пользователя и не может причинить больше вреда, чемsize_t
.источник
void*
илиsize_t
обратно как еще одну причину, чтобы избежать излишнего приведения. Я как бы пропустил это, поскольку никогда не видел его на практике, учитывая платформы, на которые мы нацеливались (которые всегда были настольными платформами: linux, OSX, Windows).typedef struct uc_struct uc_engine;
Я подозреваю, что настоящая причина в инерции, это то, что они всегда делали, и это работает, так зачем это менять?
Основная причина, которую я вижу, заключается в том, что непрозрачный дескриптор позволяет дизайнеру поместить за ним что угодно, не только структуру. Если API возвращает и принимает несколько непрозрачных типов, они все выглядят одинаково для вызывающей стороны, и никогда не возникает проблем компиляции или перекомпиляции, если изменяется мелкий шрифт. Если en_NewFlidgetTwiddler (handle ** newTwiddler) изменится, чтобы вернуть указатель на Twiddler вместо дескриптора, API не изменится, и любой новый код будет молча использовать указатель там, где раньше он использовал дескриптор. Кроме того, нет никакой опасности, что ОС или что-либо еще тихо «исправит» указатель, если он проходит через границы.
Недостатком этого, конечно, является то, что вызывающий абонент может вводить в него все что угодно. У вас есть 64-битная вещь? Вставьте его в 64-битный слот в вызове API и посмотрите, что произойдет.
Оба компилируются, но держу пари, только один из них делает то, что вы хотите.
источник
Я считаю, что это связано с давней философией защиты API библиотеки C от злоупотреблений со стороны новичков.
Особенно,
memcpy
изменить непрозрачные данные или увеличить байты или слова внутри структуры. Иди взломать.Давняя традиционная контрмера заключается в том, чтобы:
void*
struct engine* peng = (struct engine*)((size_t)enh ^ enh_magic_number);
Это просто сказать, что у него есть давние традиции; У меня не было личного мнения о том, правильно это или неправильно.
источник