J'évalue une bibliothèque dont l'API publique ressemble actuellement à ceci:
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);
Notez qu'il enh
s'agit d'un descripteur générique, utilisé comme descripteur pour plusieurs types de données différents ( moteurs et hooks ).
En interne, la plupart de ces API jettent bien entendu le "handle" sur une structure interne qu'elles auraient 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); }
Personnellement, je déteste cacher des choses derrière typedef
s, surtout quand cela compromet la sécurité du type. (Étant donné un enh
, comment puis-je savoir de quoi il s'agit réellement?)
J'ai donc soumis une demande d'extraction, suggérant le changement d'API suivant (après avoir modifié la bibliothèque entière pour qu'elle soit conforme):
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);
Bien sûr, cela améliore considérablement la mise en œuvre des API internes, en éliminant les transtypages et en maintenant la sécurité des types vers / du point de vue du consommateur.
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);
}
Je préfère cela pour les raisons suivantes:
- Sécurité de type supplémentaire
- Amélioration de la clarté des types et de leur objectif
- Moulages et
typedef
s supprimés - Il suit le modèle recommandé pour les types opaques en C
Cependant, le propriétaire du projet a reculé à la demande de retrait (paraphrasé):
Personnellement, je n'aime pas l'idée d'exposer le
struct engine
. Je pense toujours que la voie actuelle est plus propre et plus conviviale.Au départ, j'ai utilisé un autre type de données pour le hook hook, mais j'ai ensuite décidé de passer à l'utilisation
enh
, donc toutes sortes de poignées partagent le même type de données pour rester simple. Si cela prête à confusion, nous pouvons certainement utiliser un autre type de données.Voyons ce que les autres pensent de ce PR.
Cette bibliothèque est actuellement en phase bêta privée, il n'y a donc pas (encore) beaucoup de code consommateur. De plus, j'ai un peu obscurci les noms.
Comment une poignée opaque est-elle meilleure qu'une structure opaque nommée?
Remarque: J'ai posé cette question lors de la révision du code , où elle a été fermée.
la source
Réponses:
Le mantra «simple, c'est mieux» est devenu trop dogmatique. La simplicité n'est pas toujours meilleure si elle complique d'autres choses. L'assemblage est simple - chaque commande est beaucoup plus simple que les commandes de langues de niveau supérieur - et pourtant les programmes d'assemblage sont plus complexes que les langages de niveau supérieur qui font la même chose. Dans votre cas, le type de poignée uniforme
enh
rend les types plus simples au prix de rendre les fonctions complexes. Étant donné que les types de projets ont généralement tendance à augmenter en débit sub-linéaire par rapport à ses fonctions, à mesure que le projet devient plus grand, vous préférez généralement des types plus complexes si cela peut rendre les fonctions plus simples - à cet égard, votre approche semble être la bonne.L'auteur du projet est préoccupé par le fait que votre approche " expose le
struct engine
". Je leur aurais expliqué que cela n'expose pas la structure elle-même - seulement le fait qu'il existe une structure nomméeengine
. L'utilisateur de la bibliothèque doit déjà être conscient de ce type - il doit savoir, par exemple, que le premier argument deen_add_hook
est de ce type et le premier argument est d'un type différent. Cela rend donc l'API plus complexe, car au lieu d'avoir la "signature" de la fonction documenter ces types, elle doit être documentée ailleurs, et parce que le compilateur ne peut plus vérifier les types pour le programmeur.Une chose à noter - votre nouvelle API rend le code utilisateur un peu plus complexe, car au lieu d'écrire:
Ils ont désormais besoin d'une syntaxe plus complexe pour déclarer leur handle:
La solution est cependant assez simple:
et maintenant vous pouvez simplement écrire:
la source
struct
sont expressément déconseillées.typedef struct engine engine;
et j'utiliseraisengine*
: Un nom de moins introduit, et cela rend évident que c'est une poignée commeFILE*
.Il semble y avoir une confusion des deux côtés ici:
struct
nom n'expose pas ses détails (seulement son existence)Il y a des avantages à utiliser des poignées plutôt que des pointeurs nus, dans un langage comme C, car la remise du pointeur permet une manipulation directe de la pointe (y compris les appels à
free
) tandis que la remise d'une poignée nécessite que le client passe par l'API pour effectuer n'importe quelle action .Cependant, l'approche d'avoir un seul type de poignée, défini par un
typedef
est de type pas sûr, et peut causer beaucoup de tristesses.Ma suggestion personnelle serait donc de s'orienter vers des poignées sûres, ce qui, je pense, vous satisferait tous les deux. Cela se fait assez simplement:
Maintenant, on ne peut pas accidentellement passer
2
comme une poignée ni passer accidentellement une poignée à un manche à balai où une poignée pour le moteur est attendue.C'est votre erreur: avant d'entreprendre un travail important sur une bibliothèque open source, contactez le (s) auteur (s) / mainteneur (s) pour discuter du changement dès le départ . Cela vous permettra à tous les deux de vous mettre d'accord sur ce qu'il faut faire (ou ne pas faire) et d'éviter le travail inutile et la frustration qui en découle.
la source
struct file
depuis unint fd
. C'est certainement exagéré pour une bibliothèque en mode utilisateur IMO.3
) est libéré, puis un nouvel objet est créé et est malheureusement affecté à3
nouveau à l' index . En termes simples, il est difficile d'avoir un mécanisme de durée de vie des objets sûr en C à moins que le comptage des références (ainsi que les conventions sur les propriétés partagées des objets) ne soit explicitement intégré à la conception de l'API.Voici une situation où une poignée opaque est nécessaire;
Lorsque la bibliothèque a deux types de structure ou plus qui ont la même partie d'en-tête de champs, comme "type" dans ce qui précède, ces types de structure peuvent être considérés comme ayant une structure parent commune (comme une classe de base en C ++).
Vous pouvez définir la partie d'en-tête comme un "moteur struct", comme ceci;
Mais c'est une décision facultative car les transtypages sont nécessaires indépendamment de l'utilisation du moteur struct.
Conclusion
Dans certains cas, il existe des raisons pour lesquelles des poignées opaques sont utilisées à la place des structures nommées opaques.
la source
switch
premier, en utilisant des "fonctions virtuelles" est probablement idéal et résout tout le problème.L'avantage le plus évident de l'approche des poignées est que vous pouvez modifier les structures internes sans casser l'API externe. Certes, vous devez toujours modifier le logiciel client, mais au moins vous ne changez pas l'interface.
L'autre chose qu'il fait est de fournir la possibilité de choisir parmi de nombreux types différents possibles au moment de l'exécution, sans avoir à fournir une interface API explicite pour chacun. Certaines applications, comme les lectures de capteurs de plusieurs types de capteurs différents où chaque capteur est légèrement différent et génère des données légèrement différentes, répondent bien à cette approche.
Comme vous fourniriez les structures à vos clients de toute façon, vous sacrifiez un peu de sécurité de type (qui peut toujours être vérifiée au moment de l'exécution) pour une API beaucoup plus simple, même si elle nécessite une conversion.
la source
Déjà vu
J'ai rencontré exactement le même scénario, mais avec quelques différences subtiles. Nous avions, dans notre SDK, beaucoup de choses comme ça:
Ma simple proposition était de l'adapter à nos types internes:
Pour les tiers utilisant le SDK, cela ne devrait faire aucune différence. C'est un type opaque. On s'en fout? Cela n'a aucun effet sur ABI * ou la compatibilité des sources, et l'utilisation de nouvelles versions du SDK nécessite de toute façon que le plugin soit recompilé.
* Notez que, comme le souligne gnasher, il peut y avoir des cas où la taille de quelque chose comme un pointeur vers struct et void * peut en fait être une taille différente, auquel cas cela affecterait ABI. Comme lui, je ne l'ai jamais rencontré en pratique. Mais de ce point de vue, le second pourrait en fait améliorer la portabilité dans un contexte obscur, c'est donc une autre raison de privilégier le second, bien que probablement sans objet pour la plupart des gens.
Bogues tiers
De plus, j'avais encore plus de raisons que de sécurité de type pour le développement / débogage interne. Nous avions déjà un certain nombre de développeurs de plugins qui avaient des bogues dans leur code parce que deux poignées similaires (
Panel
etPanelNew
, c.-à-d.)void*
Utilisaient toutes les deux un typedef pour leurs poignées, et elles passaient accidentellement les mauvaises poignées aux mauvais endroits à la suite de l'utilisation devoid*
pour tout. Donc, cela causait des bugs du côté de ceux qui utilisaientle SDK. Leurs bogues ont également coûté énormément de temps à l'équipe de développement interne, car ils envoyaient des rapports de bogues se plaignant de bogues dans notre SDK, et nous devions déboguer le plug-in et constater qu'il était en fait causé par un bogue du plug-in passant par les mauvaises poignées. aux mauvais endroits (ce qui est facilement autorisé sans même un avertissement lorsque chaque poignée est un alias pourvoid*
ousize_t
). Nous perdions donc inutilement notre temps à fournir un service de débogage à des tiers en raison d'erreurs causées par leur désir de pureté conceptuelle en cachant toutes les informations internes, même les simples noms de nos internesstructs
.Garder le Typedef
La différence est que je proposais de nous en tenir à l'
typedef
arrêt, de ne pas laisser les clients écrire,struct SomeVertex
ce qui affecterait la compatibilité des sources pour les futures versions du plugin. Bien que j'aime personnellement l'idée de ne pas taper de textestruct
en C, du point de vue du SDK, celatypedef
peut aider, car le point est l'opacité. Donc, je suggère d'assouplir cette norme uniquement pour l'API exposée publiquement. Pour les clients utilisant le SDK, peu importe si un handle est un pointeur vers une structure, un entier, etc. La seule chose qui compte pour eux est que deux descripteurs différents n'aliasent pas le même type de données pour ne pas Passez incorrectement la mauvaise poignée au mauvais endroit.Informations sur le type
Là où il est le plus important d'éviter le casting, c'est pour vous, les développeurs internes. Ce type d'esthétique consistant à masquer tous les noms internes du SDK est une esthétique conceptuelle qui entraîne un coût important de perte de toutes les informations de type et nous oblige à saupoudrer inutilement les transtypages dans nos débogueurs pour obtenir des informations critiques. Alors qu'un programmeur C devrait être largement habitué à cela en C, l'exiger inutilement ne fait que poser des problèmes.
Idéaux conceptuels
En général, vous devez faire attention aux types de développeurs qui placent une idée conceptuelle de la pureté bien au-dessus de leurs besoins quotidiens pratiques. Celles-ci conduiront la maintenabilité de votre base de code au sol dans la recherche d'un idéal utopique, ce qui fera que toute l'équipe évitera la lotion solaire dans un désert de peur qu'elle ne soit naturelle et puisse provoquer une carence en vitamine D tandis que la moitié de l'équipage meurt d'un cancer de la peau.
Préférence utilisateur
Même du point de vue strict de l'utilisateur de ceux qui utilisent l'API, préféreraient-ils une API buggy ou une API qui fonctionne bien mais expose un nom dont ils pourraient difficilement se soucier en échange? Parce que c'est le compromis pratique. Perdre inutilement des informations de type en dehors d'un contexte générique augmente le risque de bogues, et à partir d'une base de code à grande échelle dans un cadre à l'échelle de l'équipe sur un certain nombre d'années, la loi de Murphy a tendance à être tout à fait applicable. Si vous augmentez le risque de bogues de manière superflue, il est probable que vous obtiendrez au moins quelques bogues supplémentaires. Il ne faut pas trop de temps dans une grande équipe pour découvrir que chaque type d'erreur humaine imaginable finira par passer d'un potentiel à une réalité.
C'est peut-être une question à poser aux utilisateurs. "Préféreriez-vous un SDK plus bogué ou qui expose certains noms internes opaques dont vous ne vous soucierez même plus?" Et si cette question semble présenter une fausse dichotomie, je dirais qu'une expérience plus large à l'échelle de l'équipe dans un cadre à très grande échelle est nécessaire pour apprécier le fait qu'un risque plus élevé de bogues se traduira finalement par de vrais bogues à long terme. Peu importe à quel point le développeur est confiant pour éviter les bogues. Dans une équipe, cela aide davantage à réfléchir aux maillons les plus faibles et au moins aux moyens les plus simples et les plus rapides de les empêcher de trébucher.
Proposition
Je suggère donc un compromis ici qui vous donnera toujours la possibilité de conserver tous les avantages du débogage:
... même au prix de la dactylographie
struct
, cela nous tuera-t-il vraiment? Probablement pas, donc je recommande également un peu de pragmatisme de votre part, mais plus encore au développeur qui préférerait rendre le débogage exponentiellement plus difficile en utilisantsize_t
ici et en effectuant un cast vers / depuis un entier sans raison valable, sauf pour masquer davantage les informations qui sont déjà 99 % caché à l'utilisateur et ne peut pas faire plus de mal quesize_t
.la source
void*
/size_t
et en arrière comme une autre raison pour éviter la coulée superflue. Je l'ai un peu omis car je ne l'ai jamais vu dans la pratique, étant donné les plates-formes que nous ciblions (qui étaient toujours des plates-formes de bureau: linux, OSX, Windows).typedef struct uc_struct uc_engine;
Je soupçonne que la vraie raison est l'inertie, c'est ce qu'ils ont toujours fait et cela fonctionne, alors pourquoi le changer?
La principale raison pour laquelle je peux voir est que la poignée opaque permet au concepteur de mettre n'importe quoi derrière elle, pas seulement une structure. Si l'API renvoie et accepte plusieurs types opaques, ils ont tous la même apparence pour l'appelant et il n'y a jamais de problème de compilation ou de recompilation nécessaire si les petits caractères changent. Si en_NewFlidgetTwiddler (handle ** newTwiddler) change pour renvoyer un pointeur vers le Twiddler au lieu d'un handle, l'API ne change pas et tout nouveau code utilisera silencieusement un pointeur là où il utilisait auparavant un handle. De plus, il n'y a aucun danger que le système d'exploitation ou quoi que ce soit d'autre «répare» tranquillement le pointeur s'il passe à travers les frontières.
L'inconvénient de cela, bien sûr, est que l'appelant peut y introduire quoi que ce soit. Vous avez un truc 64 bits? Placez-le dans l'emplacement 64 bits de l'appel d'API et voyez ce qui se passe.
Les deux compilent mais je parie qu'un seul d'entre eux fait ce que vous voulez.
la source
Je crois que l'attitude découle d'une philosophie de longue date pour défendre une API de bibliothèque C contre les abus des débutants.
En particulier,
memcpy
les données opaques ou incrémenteront les octets ou les mots à l'intérieur de la structure. Allez pirater.La contre-mesure traditionnelle de longue date consiste à:
void*
struct engine* peng = (struct engine*)((size_t)enh ^ enh_magic_number);
C'est juste pour dire qu'il a de longues traditions; Je n'avais aucune opinion personnelle sur le bien ou le mal.
la source