Estoy evaluando una biblioteca cuya API pública actualmente se ve así:
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);
Tenga en cuenta que enh
es un identificador genérico, que se utiliza como un identificador para varios tipos de datos diferentes ( motores y ganchos ).
Internamente, la mayoría de estas API, por supuesto, arrojan el "identificador" a una estructura interna que han hecho malloc
:
motor.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); }
Personalmente, odio esconder cosas detrás de typedef
s, especialmente cuando compromete la seguridad del tipo. (Dado un enh
, ¿cómo sé a qué se refiere realmente?)
Así que envié una solicitud de extracción, sugiriendo el siguiente cambio de API (después de modificar toda la biblioteca para que se ajuste):
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);
Por supuesto, esto hace que las implementaciones internas de la API se vean mucho mejor, eliminando los moldes y manteniendo la seguridad del tipo desde / para la perspectiva del consumidor.
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);
}
Prefiero esto por las siguientes razones:
- Se agregó seguridad de tipo
- Mayor claridad de los tipos y su finalidad.
- Se eliminaron los moldes y
typedef
s - Sigue el patrón recomendado para tipos opacos en C
Sin embargo, el propietario del proyecto rechazó la solicitud de extracción (parafraseado):
Personalmente no me gusta la idea de exponer el
struct engine
. Sigo pensando que la forma actual es más limpia y amigable.Inicialmente, utilicé otro tipo de datos para el identificador de enlace, pero luego decidí cambiar para usar
enh
, por lo que todo tipo de identificadores comparten el mismo tipo de datos para simplificarlo. Si esto es confuso, ciertamente podemos usar otro tipo de datos.Veamos qué piensan los demás sobre este PR.
Esta biblioteca se encuentra actualmente en una etapa beta privada, por lo que no hay mucho código de consumidor de qué preocuparse (todavía). Además, he ofuscado un poco los nombres.
¿Cómo es mejor un mango opaco que una estructura opaca con nombre?
Nota: Hice esta pregunta en Code Review , donde se cerró.
fuente
Respuestas:
El mantra "simple es mejor" se ha vuelto demasiado dogmático. Simple no siempre es mejor si complica otras cosas. El ensamblaje es simple: cada comando es mucho más simple que los comandos de idiomas de nivel superior, y sin embargo, los programas de ensamblaje son más complejos que los lenguajes de nivel superior que hacen lo mismo. En su caso, el tipo de mango uniforme
enh
simplifica los tipos a costa de hacer que las funciones sean complejas. Dado que, por lo general, los tipos de proyectos tienden a crecer en una tasa sub-lineal en comparación con sus funciones, a medida que el proyecto se hace más grande, generalmente prefiere tipos más complejos si puede simplificar las funciones, por lo que su enfoque parece ser el correcto.El autor del proyecto está preocupado porque su enfoque es " exponer el
struct engine
". Les habría explicado que no está exponiendo la estructura en sí misma, solo el hecho de que hay una estructura llamadaengine
. El usuario de la biblioteca ya necesita conocer ese tipo; necesita saber, por ejemplo, que el primer argumento deen_add_hook
es de ese tipo, y el primer argumento es de un tipo diferente. Por lo tanto, en realidad hace que la API sea más compleja, porque en lugar de tener el documento de "firma" de la función de estos tipos, debe documentarse en otro lugar, y porque el compilador ya no puede verificar los tipos para el programador.Una cosa que debe tenerse en cuenta: su nueva API hace que el código de usuario sea un poco más complejo, ya que en lugar de escribir:
Ahora necesitan una sintaxis más compleja para declarar su identificador:
La solución, sin embargo, es bastante simple:
y ahora puedes escribir directamente:
fuente
struct
se desaconsejan expresamente.typedef struct engine engine;
usarengine*
: Se introdujo un nombre menos, y eso hace obvio que es un identificador similarFILE*
.Parece haber una confusión en ambos lados aquí:
struct
nombre no expone sus detalles (solo su existencia)Hay ventajas en el uso de identificadores en lugar de punteros desnudos, en un lenguaje como C, porque entregar el puntero permite la manipulación directa del puntero (incluidas las llamadas a
free
), mientras que entregar un identificador requiere que el cliente pase por la API para realizar cualquier acción. .Sin embargo, el enfoque de tener un solo tipo de identificador, definido a través de a,
typedef
no es seguro y puede causar muchos dolores.Mi sugerencia personal sería, por lo tanto, avanzar hacia mangos seguros de tipo, que creo que los satisfaría a ambos. Esto se logra de manera bastante simple:
Ahora, uno no puede pasar accidentalmente
2
como una manija ni puede pasar accidentalmente una manija a una escoba donde se espera una manija al motor.Ese es su error: antes de realizar un trabajo significativo en una biblioteca de código abierto, comuníquese con los autores / encargados del mantenimiento para analizar el cambio por adelantado . Les permitirá a ambos ponerse de acuerdo sobre qué hacer (o no hacer) y evitar el trabajo innecesario y la frustración que resulta de ello.
fuente
struct file
desde unint fd
. Esto es ciertamente excesivo para una IMO de biblioteca de modo de usuario.3
se libera un objeto (índice ), luego se crea un nuevo objeto y desafortunadamente se le asigna un índice3
nuevamente. En pocas palabras, es difícil tener un mecanismo seguro de vida útil de los objetos en C a menos que el recuento de referencias (junto con las convenciones sobre la propiedad compartida de los objetos) se convierta en una parte explícita del diseño de la API.Aquí hay una situación en la que se necesita un asa opaca;
Cuando la biblioteca tiene dos o más tipos de estructura que tienen una misma parte de encabezado de campos, como "tipo" en lo anterior, estos tipos de estructura pueden considerarse que tienen una estructura principal común (como una clase base en C ++).
Puede definir la parte del encabezado como "motor de estructura", de esta manera;
Pero es una decisión opcional porque los tipos de conversión son necesarios independientemente del uso del motor de estructura.
Conclusión
En algunos casos, hay razones por las que se utilizan controladores opacos en lugar de estructuras con nombre opacas.
fuente
switch
uso de "funciones virtuales" es probablemente ideal y resuelve todo el problema.El beneficio más obvio del enfoque de controladores es que puede modificar las estructuras internas sin romper la API externa. De acuerdo, todavía tiene que modificar el software del cliente, pero al menos no está cambiando la interfaz.
La otra cosa que hace es proporcionar la capacidad de elegir entre muchos tipos diferentes posibles en tiempo de ejecución, sin tener que proporcionar una interfaz API explícita para cada uno. Algunas aplicaciones, como las lecturas de sensores de varios tipos de sensores diferentes donde cada sensor es ligeramente diferente y genera datos ligeramente diferentes, responden bien a este enfoque.
Como de todos modos proporcionaría las estructuras a sus clientes, sacrifica un poco la seguridad de los tipos (que aún se puede verificar en tiempo de ejecución) para obtener una API mucho más simple, aunque una que requiere conversión.
fuente
Deja Vu
Encontré exactamente el mismo escenario, solo que con algunas diferencias sutiles. Teníamos, en nuestro SDK, muchas cosas como esta:
Mi mera propuesta fue hacer que coincida con nuestros tipos internos:
Para terceros que usan el SDK, no debería hacer ninguna diferencia. Es un tipo opaco. ¿A quien le importa? No tiene ningún efecto sobre ABI * o la compatibilidad de fuente, y el uso de nuevas versiones del SDK requiere que el complemento se vuelva a compilar de todos modos.
* Tenga en cuenta que, como señala Gnasher, en realidad puede haber casos en los que el tamaño de algo como puntero para estructurar y anular * en realidad puede ser un tamaño diferente, en cuyo caso afectaría a ABI. Como él, nunca lo he encontrado en la práctica. Pero desde ese punto de vista, el segundo en realidad podría mejorar la portabilidad en un contexto oscuro, por lo que esa es otra razón para favorecer al segundo, aunque probablemente sea discutible para la mayoría de las personas.
Errores de terceros
Además, tenía incluso más causas que la seguridad de tipo para el desarrollo / depuración interna. Ya teníamos una serie de desarrolladores de complementos que tenían errores en su código porque dos identificadores similares (
Panel
yPanelNew
, es decir, ambos) usaban unvoid*
typedef para sus identificadores, y accidentalmente pasaban los identificadores incorrectos a los lugares incorrectos como resultado de simplemente usarvoid*
para todo. Por lo tanto, en realidad estaba causando errores en el lado de aquellos que usaban SDK. Sus errores también le costaron al equipo de desarrollo interno un tiempo enorme, ya que enviarían informes de errores quejándose de errores en nuestro SDK, y tendríamos que depurar el complemento y descubrir que en realidad fue causado por un error en el complemento que pasa por los controladores incorrectos a los lugares equivocados (que se permite fácilmente sin siquiera una advertencia cuando cada identificador es un alias paravoid*
osize_t
) Así que estábamos desperdiciando innecesariamente nuestro tiempo proporcionando un servicio de depuración para terceros debido a los errores causados por su parte por nuestro deseo de pureza conceptual al ocultar toda la información interna, incluso los simples nombres de nuestro internostructs
.Manteniendo el Typedef
La diferencia es que estaba proponiendo que nos atenemos a la imagen
typedef
fija, para que los clientes no escriban,struct SomeVertex
lo que afectaría la compatibilidad de origen para futuras versiones de complementos. Si bien personalmente me gusta la idea de no escribirstruct
en C, desde la perspectiva del SDK,typedef
puede ayudar, ya que todo el punto es la opacidad. Entonces, sugiero que se relaje este estándar solo para la API expuesta públicamente. Para los clientes que usan el SDK, no debería importar si un identificador es un puntero a una estructura, un entero, etc. Lo único que les importa es que dos identificadores diferentes no alias el mismo tipo de datos para que no Pase incorrectamente el asa incorrecta al lugar equivocado.Clasificar información
Donde más importa evitar el casting es para ti, los desarrolladores internos. Este tipo de estética de ocultar todos los nombres internos del SDK es una estética conceptual que conlleva el costo significativo de perder toda la información de tipo, y que requiere que rocíemos innecesariamente los moldes en nuestros depuradores para obtener información crítica. Si bien un programador en C debería estar acostumbrado a esto en C, exigirlo innecesariamente es solo pedir problemas.
Ideales conceptuales
En general, debe tener cuidado con los tipos de desarrolladores que ponen alguna idea conceptual de la pureza muy por encima de todas las necesidades prácticas diarias. Eso impulsará la mantenibilidad de su base de código en el terreno al buscar algún ideal utópico, haciendo que todo el equipo evite la loción bronceadora en un desierto por temor a que no sea natural y pueda causar una deficiencia de vitamina D mientras la mitad del equipo muere de cáncer de piel.
Preferencia de usuario final
Incluso desde el punto de vista estricto del usuario final de aquellos que usan la API, ¿preferirían una API defectuosa o una API que funcione bien pero exponga algún nombre que difícilmente podrían importarles a cambio? Porque esa es la compensación práctica. Perder información de tipo innecesariamente fuera de un contexto genérico aumenta el riesgo de errores, y desde una base de código a gran escala en un entorno de todo el equipo durante varios años, la ley de Murphy tiende a ser bastante aplicable. Si aumenta excesivamente el riesgo de errores, es probable que al menos obtenga algunos errores más. En un entorno de equipo grande, no lleva mucho tiempo descubrir que todo tipo de error humano imaginable eventualmente pasará de ser potencial a ser realidad.
Entonces, tal vez sea una pregunta para los usuarios. "¿Preferiría un SDK con errores o uno que exponga algunos nombres opacos internos que nunca le interesarán?" Y si esa pregunta parece presentar una falsa dicotomía, diría que se necesita más experiencia en todo el equipo en un entorno de gran escala para apreciar el hecho de que un mayor riesgo de errores finalmente manifestará errores reales a largo plazo. Poco importa cuánto confía el desarrollador en evitar errores. En un entorno de todo el equipo, es más útil pensar en los enlaces más débiles y, al menos, en las formas más fáciles y rápidas para evitar que se tropiecen.
Propuesta
Así que sugeriría un compromiso aquí que aún le dará la capacidad de retener todos los beneficios de depuración:
... incluso a costa de descartar
struct
, ¿eso realmente nos matará? Probablemente no, por lo que también recomiendo un poco de pragmatismo de su parte, pero más aún para el desarrollador que preferiría hacer la depuración exponencialmente más difícil al usarsize_t
aquí y enviar a / desde entero sin una buena razón, excepto para ocultar aún más la información que ya es 99 % oculto para el usuario y no puede hacer más daño quesize_t
.fuente
void*
osize_t
ida y vuelta como otra razón para evitar la proyección de manera superflua. Lo omití un poco, ya que tampoco lo he visto en la práctica, dadas las plataformas a las que apuntamos (que siempre fueron plataformas de escritorio: Linux, OSX, Windows).typedef struct uc_struct uc_engine;
Sospecho que la verdadera razón es la inercia, eso es lo que siempre han hecho y funciona, ¿por qué cambiarlo?
La razón principal por la que puedo ver es que el mango opaco le permite al diseñador poner algo detrás, no solo una estructura. Si la API regresa y acepta múltiples tipos opacos, todos se ven iguales para la persona que llama y nunca hay problemas de compilación o recompilaciones si la letra pequeña cambia. Si en_NewFlidgetTwiddler (handle ** newTwiddler) cambia para devolver un puntero al Twiddler en lugar de un manejador, la API no cambia y cualquier código nuevo usará silenciosamente un puntero donde antes estaba usando un manejador. Además, no hay peligro de que el sistema operativo o cualquier otra cosa "repare" silenciosamente el puntero si cruza los límites.
La desventaja de eso, por supuesto, es que la persona que llama puede alimentar cualquier cosa. ¿Tienes una cosa de 64 bits? Empújelo en la ranura de 64 bits en la llamada API y vea qué sucede.
Ambos compilan pero apuesto a que solo uno de ellos hace lo que quieres.
fuente
Creo que la actitud se deriva de una filosofía de mucho tiempo para defender una API de biblioteca C del abuso por parte de principiantes.
En particular,
memcpy
los datos opacos o aumentarán los bytes o palabras dentro de la estructura. Ve a hackear.La contramedida tradicional desde hace mucho tiempo es:
void*
struct engine* peng = (struct engine*)((size_t)enh ^ enh_magic_number);
Esto es solo para decir que tiene una larga tradición; No tenía una opinión personal sobre si era correcto o incorrecto.
fuente