Pourquoi utiliser une «poignée» opaque qui nécessite la conversion dans une API publique plutôt qu'un pointeur de structure typesafe?


27

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 enhs'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 typedefs, 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:

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.


1
J'ai modifié le titre pour quelque chose qui, je crois, exprime plus clairement le cœur de votre question. N'hésitez pas à revenir si je l'ai mal interprété.
Ixrec

1
@Ixrec C'est mieux, merci. Après avoir rédigé toute la question, j'ai manqué de capacité mentale pour trouver un bon titre.
Jonathon Reinhart

Réponses:


33

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 enhrend 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 lestruct engine ". Je leur aurais expliqué que cela n'expose pas la structure elle-même - seulement le fait qu'il existe une structure nommée engine. L'utilisateur de la bibliothèque doit déjà être conscient de ce type - il doit savoir, par exemple, que le premier argument de en_add_hookest 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:

enh en;
en_open(ENGINE_MODE_1, &en);

Ils ont désormais besoin d'une syntaxe plus complexe pour déclarer leur handle:

struct engine* en;
en_open(ENGINE_MODE_1, &en);

La solution est cependant assez simple:

struct _engine;
typedef struct _engine* engine

et maintenant vous pouvez simplement écrire:

engine en;
en_open(ENGINE_MODE_1, &en);

J'ai oublié de mentionner que la bibliothèque prétend suivre le style de codage Linux , qui est également ce que je suis. Là-bas, vous verrez que les structures typographiques pour éviter d'écrire structsont expressément déconseillées.
Jonathon Reinhart

@JonathonReinhart, il a tapé le pointeur sur struct et non sur la structure elle-même.
ratchet freak

@JonathonReinhart et en lisant ce lien, je vois que pour les "objets totalement opaques", c'est autorisé. (Chapitre 5, règle A)
Ratchet Freak

Oui, mais seulement dans les cas exceptionnellement rares. Je crois honnêtement que cela a été ajouté pour éviter de réécrire tout le code mm pour traiter les typedefs pte. Regardez le code de verrouillage de rotation. Il est entièrement spécifique à l'archive (pas de données communes) mais ils n'utilisent jamais de typedef.
Jonathon Reinhart

8
Je préférerais typedef struct engine engine;et j'utiliserais engine*: Un nom de moins introduit, et cela rend évident que c'est une poignée comme FILE*.
Déduplicateur

16

Il semble y avoir une confusion des deux côtés ici:

  • l'utilisation d'une approche de poignée ne nécessite pas l'utilisation d'un seul type de poignée pour toutes les poignées
  • exposer le structnom 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 typedefest 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:

typedef struct {
    size_t id;
} enh;

typedef struct {
    size_t id;
} oth;

Maintenant, on ne peut pas accidentellement passer 2comme une poignée ni passer accidentellement une poignée à un manche à balai où une poignée pour le moteur est attendue.


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 se conformer)

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.


1
Merci. Vous ne savez pas quoi faire avec les poignées. J'ai implémenté une véritable API basée sur un handle , où les pointeurs ne sont jamais exposés, même si via un typedef. Cela impliquait une recherche coûteuse des données à l'entrée de chaque appel d'API - un peu comme la façon dont Linux regarde les struct filedepuis un int fd. C'est certainement exagéré pour une bibliothèque en mode utilisateur IMO.
Jonathon Reinhart

@JonathonReinhart: Eh bien, comme la bibliothèque fournit déjà des poignées, je n'ai pas ressenti le besoin de m'étendre. En effet, il existe plusieurs approches, de la simple conversion du pointeur en entier à la création d'un "pool" et l'utilisation des ID comme clés. Vous pouvez même changer d' approche entre Debug (ID + recherche, pour la validation) et Release (pointeur juste converti, pour la vitesse).
Matthieu M.

La réutilisation de l'index de table entier souffrira réellement du problème ABA , où un objet (index 3) est libéré, puis un nouvel objet est créé et est malheureusement affecté à 3nouveau à 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.
rwong

2
@rwong: Ce n'est qu'un problème dans un schéma naïf; vous pouvez facilement intégrer un compteur d'époque, par exemple, de sorte que lorsqu'une ancienne poignée est spécifiée, vous obtiendrez un décalage d'époque.
Matthieu M.

1
@JonathonReinhart suggestion: vous pouvez mentionner "règle d'aliasing stricte" dans votre question pour aider à orienter la discussion vers les aspects les plus importants.
rwong

3

Voici une situation où une poignée opaque est nécessaire;

struct SimpleEngine {
    int type;  // always SimpleEngine.type = 1
    int a;
};

struct ComplexEngine {
    int type;  // always ComplexEngine.type = 2
    int a, b, c;
};

int en_start(enh handle) {
    switch(*(int*)handle) {
    case 1:
        // treat handle as SimpleEngine
        return start_simple_engine(handle);
    case 2:
        // treat handle as ComplexEngine
        return start_complex_engine(handle);
    }
}

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;

struct engine {
    int type;
};

struct SimpleEngine {
    struct engine base;
    int a;
};

struct ComplexEngine {
    struct engine base;
    int a, b, c;
};

int en_start(struct engine *en) { ... }

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.


Je pense que l'utilisation d'un syndicat rend cela plus sûr au lieu de lancer des lancers dangereux vers des champs qui pourraient être déplacés. Découvrez cet élément essentiel que j'ai mis en place montrant un exemple complet.
Jonathon Reinhart

Mais en fait, éviter le switchpremier, en utilisant des "fonctions virtuelles" est probablement idéal et résout tout le problème.
Jonathon Reinhart

Votre conception chez gist est plus complexe que je ne le suggérais. Sûrement, cela rend le casting moins, sûr et intelligent, mais introduit plus de code et de types. À mon avis, il semble devenir trop délicat de se mettre en sécurité. Moi, et peut-être l'auteur de la bibliothèque décidons de suivre KISS plutôt que de taper la sécurité.
Akio Takahashi

Eh bien, si vous voulez que ce soit très simple, vous pouvez aussi complètement omettre la vérification des erreurs!
Jonathon Reinhart

À mon avis, la simplicité de conception est préférable à une certaine quantité de vérifications d'erreurs. Dans ce cas, ces vérifications d'erreur n'existent que dans les fonctions API. En outre, vous pouvez supprimer les transtypages à l'aide de l'union, mais n'oubliez pas que l'union est naturellement non sécurisée.
Akio Takahashi

2

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.


5
"Vous pouvez modifier les structures internes sans .." - vous pouvez également le faire avec l'approche de déclaration directe.
user253751

L'approche "déclaration avant" ne vous oblige-t-elle pas à déclarer les signatures de type? Et ces signatures de type ne changent-elles pas encore si vous modifiez les structures?
Robert Harvey

La déclaration directe vous oblige seulement à déclarer le nom du type - sa structure reste cachée.
Idan Arye

Quel serait alors l'avantage de la déclaration directe si elle n'applique même pas la structure de type?
Robert Harvey

6
@RobertHarvey Rappelez-vous - c'est C dont nous parlons. Il n'y a pas de méthodes, donc à part le nom et la structure, il n'y a rien d'autre au type. Si elle avait appliqué la structure, elle aurait été identique à la déclaration régulière. Le point d'exposer le nom sans appliquer la structure est que vous pouvez utiliser ce type dans les signatures de fonction. Bien sûr, sans la structure, vous ne pouvez utiliser que des pointeurs vers le type, car le compilateur ne peut pas connaître sa taille, mais comme il n'y a pas de transtypage de pointeur implicite en C, l'utilisation de pointeurs est suffisante pour que le typage statique vous protège.
Idan Arye

2

Déjà vu

Comment une poignée opaque est-elle meilleure qu'une structure opaque nommée?

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:

typedef void* SomeHandle;

Ma simple proposition était de l'adapter à nos types internes:

typedef struct SomeVertex* SomeHandle;

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 ( Panelet PanelNew, 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 de void*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 pour void*ou size_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 internes structs.

Garder le Typedef

La différence est que je proposais de nous en tenir à l' typedefarrêt, de ne pas laisser les clients écrire, struct SomeVertexce 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 texte structen C, du point de vue du SDK, cela typedefpeut 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:

typedef struct engine* enh;

... 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 utilisant size_tici 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 que size_t.


1
C'est une toute petite différence: selon la norme C, tous les "pointeurs vers struct" ont une représentation identique, tout comme "pointeur vers union", tout comme "void *" et "char *", mais un void * et un "pointeur" to struct "peut avoir une taille différente de () et / ou une représentation différente. En pratique, je n'ai jamais vu ça.
gnasher729

@ gnasher729 Pareil, je devrais peut-être qualifier cette partie en ce qui concerne la perte potentielle de portabilité lors de la coulée vers et void*/ size_tet 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).

1
Nous nous sommes retrouvés avectypedef struct uc_struct uc_engine;
Jonathon Reinhart

1

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.

en_TwiddleFlidget(engine, twiddler, flidget)
en_TwiddleFlidget(engine, flidget, twiddler)

Les deux compilent mais je parie qu'un seul d'entre eux fait ce que vous voulez.


1

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,

  • Les auteurs de bibliothèque savent que c'est un pointeur vers la structure, et les détails de la structure sont visibles pour le code de la bibliothèque.
  • Tous les programmeurs expérimentés qui utilisent la bibliothèque savent également que c'est un pointeur vers des structures opaques;
    • Ils ont eu suffisamment d' expérience pénible pour savoir ne pas jouer avec les octets stockés dans ces structures.
  • Les programmeurs inexpérimentés ne connaissent ni l'un ni l'autre.
    • Ils essaieront memcpyles 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 à:

  • Masquer le fait qu'une poignée opaque est en fait un pointeur vers une structure opaque qui existe dans le même espace de mémoire de processus.
    • Pour ce faire, en prétendant qu'il s'agit d'une valeur entière ayant le même nombre de bits qu'un void*
    • Pour être extrêmement circonspect, masquer également les bits du pointeur, par exemple
      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.


3
Sauf pour le xor ridicule, ma solution offre également cette sécurité. Le client ignore la taille ou le contenu de la structure, avec l'avantage supplémentaire de la sécurité du type. Je ne vois pas comment abuser d'un size_t pour tenir un pointeur est mieux.
Jonathon Reinhart

@JonathonReinhart, il est extrêmement improbable que le client ignore réellement la structure. La question est plus: peuvent-ils obtenir la structure et peuvent-ils retourner une version modifiée à votre bibliothèque. Pas seulement avec l'open source, mais plus généralement. La solution est un partitionnement de mémoire moderne, pas le stupide XOR.
Móż

Qu'est-ce que tu racontes? Tout ce que je dis, c'est que vous ne pouvez pas compiler de code qui tente de déréférencer un pointeur sur ladite structure, ou faire quoi que ce soit qui nécessite la connaissance de sa taille. Bien sûr, vous pouvez définir (, 0,) sur le tas du processus entier, si vous le souhaitez vraiment.
Jonathon Reinhart

6
Cet argument ressemble beaucoup à se prémunir contre Machiavel . Si l'utilisateur souhaite transmettre des ordures à mon API, je ne peux pas les arrêter. L'introduction d'une interface non sécurisée de type comme celle-ci n'y aide guère, car elle facilite en fait une mauvaise utilisation accidentelle de l'API.
ComicSansMS

@ComicSansMS merci d'avoir mentionné "accidentel", car c'est ce que j'essaie vraiment d'empêcher ici.
Jonathon Reinhart
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.