Pièges de conception d'API en C [fermé]

10

Quelles sont les failles qui vous rendent fou dans les API C (y compris les bibliothèques standard, les bibliothèques tierces et les en-têtes à l'intérieur d'un projet)? L'objectif est d'identifier les pièges de conception d'API en C, afin que les personnes qui écrivent de nouvelles bibliothèques C puissent apprendre des erreurs du passé.

Expliquez pourquoi la faille est mauvaise (de préférence avec un exemple) et essayez de suggérer une amélioration. Bien que votre solution puisse ne pas être pratique dans la vie réelle (il est trop tard pour la réparer strncpy), elle devrait donner un coup de pouce aux futurs rédacteurs de bibliothèques.

Bien que l'objectif de cette question soit les API C, les problèmes qui affectent votre capacité à les utiliser dans d'autres langues sont les bienvenus.

Veuillez donner un défaut par réponse, afin que la démocratie puisse trier les réponses.

Joey Adams
la source
3
Joey, cette question est sur le point de ne pas être constructive en demandant de dresser une liste de choses que les gens détestent. Il est possible que la question soit utile si les réponses expliquent pourquoi les pratiques qu'elles signalent sont mauvaises et fournissent des informations détaillées sur la façon de les améliorer. À cette fin, veuillez déplacer votre exemple de la question dans une réponse qui lui est propre et expliquer pourquoi c'est un problème / comment une mallocchaîne pourrait le résoudre. Je pense que donner un bon exemple avec la première réponse pourrait vraiment aider cette question à prospérer. Merci!
Adam Lear
1
@Anna Lear: Merci de m'avoir expliqué pourquoi ma question posait problème. J'essayais de rester constructif en demandant un exemple et une alternative suggérée. Je suppose que j'avais vraiment besoin d'exemples pour indiquer ce que j'avais en tête.
Joey Adams
@Joey Adams Regardez les choses de cette façon. Vous posez une question censée résoudre "automatiquement" les problèmes de l'API C de manière générale. Où des sites comme StackOverflow ont été conçus pour fonctionner de telle sorte que les problèmes de programmation les plus courants sont facilement détectés ET résolus. StackOverflow se traduira naturellement par une liste de réponses à votre question, mais d'une manière plus structurée et facilement consultable.
Andrew T Finnell
J'ai voté pour clore ma propre question. Mon objectif était d'avoir une collection de réponses qui pourrait servir de liste de contrôle pour les nouvelles bibliothèques C. Jusqu'à présent, les trois réponses utilisent toutes des mots comme "incohérent", "illogique" ou "déroutant". On ne peut pas déterminer objectivement si une API viole ou non l'une de ces réponses.
Joey Adams

Réponses:

5

Fonctions avec des valeurs de retour incohérentes ou illogiques. Deux bons exemples:

1) Certaines fonctions Windows qui renvoient un HANDLE utilisent NULL / 0 pour une erreur (CreateThread), certaines utilisent INVALID_HANDLE_VALUE / -1 pour une erreur (CreateFile).

2) La fonction POSIX 'time' renvoie '(time_t) -1' en cas d'erreur, ce qui est vraiment illogique car 'time_t' peut être soit un type signé soit non signé.

David Schwartz
la source
2
En fait, time_t est (généralement) signé. Cependant, qualifier le 31 décembre 1969 d '«invalide» est plutôt illogique. Je suppose que les années 60 étaient approximatives :-) Sérieusement, une solution serait de renvoyer un code d'erreur et de passer le résultat à travers un pointeur, comme dans: int time(time_t *out);et BOOL CreateFile(LPCTSTR lpFileName, ..., HANDLE *out);.
Joey Adams
Exactement. C'est bizarre si time_t n'est pas signé, et si time_t est signé, cela rend une fois invalide au milieu d'un océan de valides.
David Schwartz
4

Fonctions ou paramètres avec des noms non descriptifs ou affirmativement déroutants. Par exemple:

1) CreateFile, dans l'API Windows, ne crée pas réellement de fichier, il crée un descripteur de fichier. Il peut créer un fichier, tout comme 'open' peut, si on le lui demande via un paramètre. Ce paramètre a des valeurs appelées «CREATE_ALWAYS» et «CREATE_NEW» dont les noms ne font même pas allusion à leur sémantique. ('CREATE_ALWAYS' signifie-t-il qu'il échoue si le fichier existe? Ou crée-t-il un nouveau fichier par-dessus? Est-ce que 'CREATE_NEW' signifie qu'il crée toujours un nouveau fichier et échoue si le fichier existe déjà? Ou crée-t-il un nouveau fichier par-dessus?)

2) pthread_cond_wait dans l'API POSIX pthreads, qui malgré son nom, est une attente inconditionnelle .

David Schwartz
la source
1
Le cond in pthread_cond_waitne signifie pas "conditionnellement attendre". Cela fait référence au fait que vous attendez une variable de condition .
Jonathon Reinhart
C'est une attente inconditionnelle pour une condition.
David Schwartz
3

Types opaques transmis via l'interface en tant que poignées de type supprimé. Le problème est, bien sûr, que le compilateur ne peut pas vérifier le code utilisateur pour les types d'arguments corrects.

Cela se présente sous diverses formes et saveurs, y compris, mais sans s'y limiter:

  • void* abuser de

  • utilisation intcomme ressource (exemple: la bibliothèque CDI)

  • arguments typés en chaîne

Les types les plus distincts (= ne peuvent pas être utilisés de manière totalement interchangeable) sont mappés au même type supprimé, le pire. Bien sûr, le remède consiste simplement à fournir des pointeurs opaques de type sécurisé le long de (exemple C):

typedef struct Foo Foo;
typedef struct Bar Bar;

Foo* createFoo();
Bar* createBar();

int doSomething(Foo* foo);
void somethingElse(Foo* foo, Bar* bar);

void destroyFoo(Foo* foo);
void destroyBar(Bar* bar);
cmaster - réintégrer monica
la source
2

Fonctions avec des conventions de retour de chaîne incohérentes et souvent encombrantes.

Par exemple, getcwd demande un tampon fourni par l'utilisateur et sa taille. Cela signifie qu'une application doit soit fixer une limite arbitraire sur la longueur actuelle du répertoire, soit faire quelque chose comme ça ( depuis CCAN ):

 /* *This* is why people hate C. */
len = 32;
cwd = talloc_array(ctx, char, len);
while (!getcwd(cwd, len)) {
    if (errno != ERANGE) {
        talloc_free(cwd);
        return NULL;
    }
    cwd = talloc_realloc(ctx, cwd, char, len *= 2);
}

Ma solution: renvoyer une mallocchaîne ed. C'est simple, robuste et non moins efficace. À l'exception des plates-formes intégrées et des anciens systèmes, mallocest en fait assez rapide.

Joey Adams
la source
4
Je n'appellerais pas cette mauvaise pratique, j'appellerais cette bonne pratique. 1) Il est tellement courant qu'aucun programmeur ne devrait en être surpris. 2) Il laisse l'allocation à l'appelant, ce qui exclut de nombreuses possibilités de bugs de fuite de mémoire. 3) Il est compatible avec les tampons alloués statiquement. 4) Cela rend l'implémentation de la fonction plus propre, une fonction calculant une formule mathématique ne devrait pas se préoccuper de quelque chose de totalement indépendant, comme l'allocation dynamique de mémoire. Vous pensez que main devient plus propre mais la fonction devient plus désordonnée. 5) malloc n'est même pas autorisé sur de nombreux systèmes.
@Lundin: Le problème est que cela conduit les programmeurs à créer des limites codées en dur inutiles, et ils doivent vraiment essayer de ne pas le faire (voir l'exemple ci-dessus). C'est bien pour des choses comme snprintf(buf, 32, "%d", n), où la longueur de sortie est prévisible (certainement pas plus de 30, à moins qu'elle ne intsoit vraiment énorme sur votre système). En effet, malloc n'est pas disponible sur de nombreux systèmes, mais pour les environnements de bureau et de serveur, il l'est, et il fonctionne très bien.
Joey Adams
Mais le problème est que la fonction de votre exemple ne définit aucune limite codée en dur. Un code comme celui-ci n'est pas une pratique courante. Ici, main sait quelque chose sur la longueur du tampon que la fonction aurait dû connaître. Tout cela suggère une mauvaise conception du programme. Main ne semble même pas savoir ce que fait la fonction getcwd, donc il utilise une allocation de "force brute" pour le découvrir. Quelque part, l'interface entre le module dans lequel réside getcwd et l'appelant est confuse. Cela ne signifie pas que cette façon d'appeler des fonctions est mauvaise, au contraire l'expérience montre qu'elle est bonne pour les raisons que j'ai déjà énumérées.
1

Fonctions qui prennent / retournent des types de données composés par valeur, ou qui utilisent des rappels.

Pire encore si ledit type est une union ou contient des champs binaires.

Du point de vue d'un appelant C, ceux-ci sont en fait OK, mais je n'écris pas en C ou C ++ à moins d'y être obligé, donc j'appelle généralement via un FFI. La plupart des FFI ne prennent pas en charge les unions ou les champs de bits, et certains (tels que Haskell et MLton) ne peuvent pas prendre en charge les structures passées par valeur. Pour ceux qui peuvent gérer les structures par valeur, au moins Common Lisp et LuaJIT sont forcés sur des chemins lents - L'interface Common Foreign Function de Lisp doit effectuer un appel lent via libffi, et LuaJIT refuse de compiler JIT le chemin de code contenant l'appel. Les fonctions qui peuvent rappeler dans les hôtes déclenchent également des chemins lents sur LuaJIT, Java et Haskell, LuaJIT ne pouvant pas compiler un tel appel.

Demi
la source