Quelle est la meilleure façon de résoudre une collision d'espace de noms Objective-C?

174

Objective-C n'a pas d'espaces de noms; c'est un peu comme C, tout est dans un espace de noms global. La pratique courante est de préfixer les classes avec des initiales, par exemple si vous travaillez chez IBM, vous pouvez les préfixer avec "IBM"; si vous travaillez pour Microsoft, vous pouvez utiliser "MS"; etc. Parfois, les initiales font référence au projet, par exemple Adium préfixe les classes avec "AI" (car il n'y a pas de société derrière cela, vous pouvez prendre les initiales). Apple préfixe les classes avec NS et indique que ce préfixe est réservé à Apple uniquement.

Jusqu'ici tout va bien. Mais ajouter 2 à 4 lettres à un nom de classe devant est un espace de noms très, très limité. Par exemple, MS ou AI pourrait avoir une signification entièrement différente (l'IA pourrait être une intelligence artificielle par exemple) et un autre développeur pourrait décider de les utiliser et de créer une classe également nommée. Bang , collision d'espace de noms.

D'accord, s'il s'agit d'une collision entre l'une de vos propres classes et l'un des frameworks externes que vous utilisez, vous pouvez facilement changer le nom de votre classe, ce n'est pas grave. Mais que faire si vous utilisez deux frameworks externes, les deux frameworks dont vous n'avez pas la source et que vous ne pouvez pas changer? Votre application est liée aux deux et vous obtenez des conflits de noms. Comment allez-vous résoudre ces problèmes? Quelle est la meilleure façon de les contourner de manière à pouvoir toujours utiliser les deux classes?

En C, vous pouvez contourner ces problèmes en ne liant pas directement à la bibliothèque, à la place vous chargez la bibliothèque au moment de l'exécution, en utilisant dlopen (), puis trouvez le symbole que vous recherchez en utilisant dlsym () et affectez-le à un symbole global (que vous pouvez nommer comme vous le souhaitez), puis accédez-y via ce symbole global. Par exemple, si vous avez un conflit parce qu'une bibliothèque C a une fonction nommée open (), vous pouvez définir une variable nommée myOpen et la faire pointer vers la fonction open () de la bibliothèque, donc lorsque vous souhaitez utiliser le système open () , il vous suffit d'utiliser open () et lorsque vous souhaitez utiliser l'autre, vous y accédez via l'identifiant myOpen.

Quelque chose de similaire est-il possible en Objective-C et sinon, existe-t-il une autre solution intelligente et délicate que vous pouvez utiliser pour résoudre les conflits d'espace de noms? Des idées?


Mettre à jour:

Juste pour clarifier ceci: les réponses qui suggèrent comment éviter les collisions d'espace de noms à l'avance ou comment créer un meilleur espace de noms sont certainement les bienvenues; cependant, je ne les accepterai pas comme réponse car ils ne résolvent pas mon problème. J'ai deux bibliothèques et leurs noms de classe se heurtent. Je ne peux pas les changer; Je n'ai pas la source de l'un ou l'autre. La collision est déjà là et des conseils sur la façon dont elle aurait pu être évitée à l'avance ne seront plus utiles. Je peux les transmettre aux développeurs de ces frameworks et espérer qu'ils choisiront un meilleur espace de noms à l'avenir, mais pour le moment, je recherche une solution pour travailler avec les frameworks dès maintenant dans une seule application. Des solutions pour rendre cela possible?

Mecki
la source
7
Vous avez une bonne question (que faire si vous avez besoin de deux frameworks qui ont une collision de nom) mais elle est enfouie dans le texte. Révisez pour le rendre plus clair et vous éviterez des réponses simplistes comme celle que vous avez actuellement.
benzado
4
C'est mon plus gros reproche avec la conception actuelle du langage Objective-C. Regardez les réponses ci-dessous; ceux qui répondent réellement à la question (déchargement de NSBundle, utilisation de DO, etc.) sont des hacks hideux qui ne devraient tout simplement pas être nécessaires pour quelque chose d'aussi trivial que d'éviter un conflit d'espace de noms.
erikprice
@erikprice: Amen. J'apprends obj-c et je touche ce problème. Je suis venu ici à la recherche d'une solution simple .... boiteux.
Dave Mateer
1
Pour mémoire, techniquement, C et Objective-C prennent en charge plusieurs espaces de noms - pas exactement ce que l'OP recherche, cependant. Voir objectivistc.tumblr.com/post/3340816080/…
Hmm, je ne savais pas ça. Une sorte de décision de conception terrible non?
Nico

Réponses:

47

Si vous n'avez pas besoin d'utiliser des classes des deux frameworks en même temps et que vous ciblez des plates-formes qui prennent en charge le déchargement NSBundle (OS X 10.4 ou version ultérieure, pas de support GNUStep), et que les performances ne sont vraiment pas un problème pour vous, je crois que vous pouvez charger un framework à chaque fois que vous devez utiliser une classe à partir de celui-ci, puis le décharger et charger l'autre lorsque vous devez utiliser l'autre framework.

Mon idée initiale était d'utiliser NSBundle pour charger l'un des frameworks, puis copier ou renommer les classes à l'intérieur de ce framework, puis charger l'autre framework. Il y a deux problèmes avec ceci. Premièrement, je n'ai pas trouvé de fonction pour copier les données pointées pour renommer ou copier une classe, et toutes les autres classes de ce premier cadre qui référencent la classe renommée feraient désormais référence à la classe de l'autre cadre.

Vous n'auriez pas besoin de copier ou de renommer une classe s'il y avait un moyen de copier les données pointées par un IMP. Vous pouvez créer une nouvelle classe, puis copier sur les ivars, méthodes, propriétés et catégories. Beaucoup plus de travail, mais c'est possible. Cependant, vous auriez toujours un problème avec les autres classes du framework référençant la mauvaise classe.

EDIT: La différence fondamentale entre les environnements d'exécution C et Objective-C est, si je comprends bien, lorsque les bibliothèques sont chargées, les fonctions de ces bibliothèques contiennent des pointeurs vers tous les symboles auxquels elles font référence, alors qu'en Objective-C, elles contiennent des représentations sous forme de chaîne du noms de ces symboles. Ainsi, dans votre exemple, vous pouvez utiliser dlsym pour obtenir l'adresse du symbole en mémoire et l'attacher à un autre symbole. L'autre code de la bibliothèque fonctionne toujours car vous ne modifiez pas l'adresse du symbole d'origine. Objective-C utilise une table de recherche pour mapper les noms de classe aux adresses, et c'est un mappage 1-1, vous ne pouvez donc pas avoir deux classes avec le même nom. Ainsi, pour charger les deux classes, l'une d'elles doit avoir son nom changé. Cependant, lorsque d'autres classes doivent accéder à l'une des classes portant ce nom,

Michael Buckley
la source
5
Je crois que le déchargement de paquets n'était pas pris en charge jusqu'à 10.5 ou plus tard.
Quinn Taylor
93

Préfixer vos classes avec un préfixe unique est fondamentalement la seule option, mais il existe plusieurs façons de rendre cela moins onéreux et moins laid. Il y a une longue discussion sur les options ici . Mon préféré est la @compatibility_aliasdirective du compilateur Objective-C (décrite ici ). Vous pouvez utiliser @compatibility_aliaspour "renommer" une classe, vous permettant de nommer votre classe en utilisant le FQDN ou un tel préfixe:

@interface COM_WHATEVER_ClassName : NSObject
@end

@compatibility_alias ClassName COM_WHATEVER_ClassName
// now ClassName is an alias for COM_WHATEVER_ClassName

@implementation ClassName //OK
//blah
@end

ClassName *myClass; //OK

Dans le cadre d'une stratégie complète, vous pouvez préfixer toutes vos classes avec un préfixe unique tel que le FQDN, puis créer un en-tête avec tous les @compatibility_alias(j'imagine que vous pouvez générer automatiquement ledit en-tête).

L'inconvénient d'un préfixage comme celui-ci est que vous devez entrer le vrai nom de classe (par exemple COM_WHATEVER_ClassNameci-dessus) dans tout ce qui nécessite le nom de classe d'une chaîne en plus du compilateur. Notamment, @compatibility_aliasest une directive de compilateur, pas une fonction d'exécution, donc NSClassFromString(ClassName)échouera (retour nil) - vous devrez utiliser NSClassFromString(COM_WHATERVER_ClassName). Vous pouvez utiliser ibtoolvia la phase de construction pour modifier les noms de classe dans un nib / xib Interface Builder afin de ne pas avoir à écrire le COM_WHATEVER _... complet dans Interface Builder.

Dernière mise en garde: comme il s'agit d'une directive de compilateur (et obscure en plus), elle peut ne pas être portable entre les compilateurs. En particulier, je ne sais pas si cela fonctionne avec le frontend Clang du projet LLVM, bien qu'il devrait fonctionner avec LLVM-GCC (LLVM utilisant le frontend GCC).

Barry Wark
la source
4
Vote positif pour la compatibilité_alias, je ne connaissais pas celui-là! Merci. Mais cela ne résout pas mon problème. Je ne suis pas en mesure de modifier les préfixes des deux frameworks que j'utilise, je les ai uniquement sous forme binaire et ils se heurtent. Que dois-je faire à ce sujet?
Mecki
Dans quel fichier (.h ou .m) la ligne @compatibility_alias doit-elle aller?
Alex Basson
1
@Alex_Basson @compatibility_alias devrait probablement aller dans l'en-tête pour qu'il soit visible partout où la déclaration @interface est visible.
Barry Wark
Je me demande si vous pouvez utiliser @compatibility_alias avec des noms de protocole, ou des définitions de typedef, ou autre chose d'ailleurs?
Ali
Bonnes pensées ici. Le swizzling de méthode pourrait-il être utilisé pour empêcher NSClassFromString (ClassName) de renvoyer nil pour les classes aliasées? Il serait probablement difficile de trouver toutes les méthodes qui prennent un nom de classe.
Dickey Singh
12

Plusieurs personnes ont déjà partagé un code délicat et intelligent qui pourrait aider à résoudre le problème. Certaines des suggestions peuvent fonctionner, mais toutes sont loin d'être idéales et certaines d'entre elles sont carrément désagréables à mettre en œuvre. (Parfois, les hacks laids sont inévitables, mais j'essaie de les éviter chaque fois que je le peux.) D'un point de vue pratique, voici mes suggestions.

  1. Dans tous les cas, informez les développeurs des deux cadres du conflit et indiquez clairement que leur incapacité à l'éviter et / ou à le gérer vous pose de réels problèmes commerciaux, qui pourraient se traduire par une perte de revenus commerciaux s'ils ne sont pas résolus. Insistez sur le fait que si la résolution des conflits existants par classe est une solution moins intrusive, changer complètement leur préfixe (ou en utiliser un s'ils ne le sont pas actuellement, et honte à eux!) Est le meilleur moyen de s'assurer qu'ils ne le feront pas. voir à nouveau le même problème.
  2. Si les conflits de dénomination sont limités à un ensemble raisonnablement petit de classes, voyez si vous pouvez contourner uniquement ces classes, en particulier si l'une des classes en conflit n'est pas utilisée par votre code, directement ou indirectement. Si tel est le cas, vérifiez si le fournisseur fournira une version personnalisée du framework qui n'inclut pas les classes en conflit. Sinon, soyez franc sur le fait que leur rigidité réduit votre retour sur investissement en utilisant leur cadre. Ne vous sentez pas mal à l'idée d'être insistant dans les limites du raisonnable - le client a toujours raison. ;-)
  3. Si un framework est plus "dispensable", vous pouvez envisager de le remplacer par un autre framework (ou combinaison de code), tiers ou homebrew. (Ce dernier cas est le pire des cas indésirables, car il entraînera certainement des coûts commerciaux supplémentaires, à la fois pour le développement et la maintenance.) Si vous le faites, informez le fournisseur de ce cadre exactement pourquoi vous avez décidé de ne pas utiliser son cadre.
  4. Si les deux frameworks sont jugés également indispensables à votre application, explorez les moyens de factoriser l'utilisation de l'un d'entre eux dans un ou plusieurs processus séparés, peut-être en communiquant via DO comme l'a suggéré Louis Gerbarg. Selon le degré de communication, cela peut ne pas être aussi mauvais que vous le pensez. Plusieurs programmes (y compris QuickTime, je crois) utilisent cette approche pour fournir une sécurité plus granulaire fournie en utilisant les profils de sandbox Seatbelt dans Leopard , de sorte que seul un sous-ensemble spécifique de votre code est autorisé à effectuer des opérations critiques ou sensibles. La performance sera un compromis, mais peut être votre seule option

Je suppose que les frais de licence, les conditions et les durées peuvent empêcher une action instantanée sur l'un de ces points. J'espère que vous serez en mesure de résoudre le conflit le plus rapidement possible. Bonne chance!

Quinn Taylor
la source
8

C'est grossier, mais vous pouvez utiliser des objets distribués afin de ne conserver l'une des classes que dans une adresse de programmes subordonnés et RPC. Cela deviendra compliqué si vous passez une tonne de choses dans les deux sens (et peut ne pas être possible si les deux classes manipulent directement les vues, etc.).

Il existe d'autres solutions potentielles, mais beaucoup dépendent de la situation exacte. En particulier, utilisez-vous les environnements d'exécution modernes ou hérités, êtes-vous une architecture lourde ou unique, 32 ou 64 bits, quelles versions de système d'exploitation ciblez-vous, créez-vous des liaisons dynamiques, des liaisons statiques, ou avez-vous le choix, et est-ce potentiellement ok pour faire quelque chose qui pourrait nécessiter une maintenance pour les nouvelles mises à jour logicielles.

Si vous êtes vraiment désespéré, ce que vous pouvez faire est:

  1. Pas de lien direct avec l'une des bibliothèques
  2. Implémentez une version alternative des routines d'exécution objc qui change le nom au moment du chargement ( vérifiez le projet objc4 , ce que vous devez faire exactement dépend d'un certain nombre de questions que j'ai posées ci-dessus, mais cela devrait être possible quelles que soient les réponses ).
  3. Utilisez quelque chose comme mach_override pour injecter votre nouvelle implémentation
  4. Chargez la nouvelle bibliothèque en utilisant des méthodes normales, elle passera par la routine de l'éditeur de liens patché et obtiendra son nom de classe modifié

Ce qui précède va demander beaucoup de travail, et si vous devez l'implémenter contre plusieurs archs et différentes versions d'exécution, ce sera très désagréable, mais cela peut certainement fonctionner.

Louis Gerbarg
la source
4

Avez-vous envisagé d'utiliser les fonctions d'exécution (/usr/include/objc/runtime.h) pour cloner l'une des classes en conflit dans une classe sans collision, puis charger le cadre de classe en collision? (cela nécessiterait que les frameworks en collision soient chargés à des moments différents pour fonctionner.)

Vous pouvez inspecter les classes ivars, les méthodes (avec les noms et les adresses d'implémentation) et les noms avec le runtime, et créer les vôtres de manière dynamique pour avoir la même présentation ivar, les mêmes noms de méthodes / adresses d'implémentation, et ne différer que par le nom (pour éviter les collision)

xtophyr
la source
3

Les situations désespérées appellent des mesures désespérées. Avez-vous envisagé de pirater le code objet (ou le fichier de bibliothèque) de l'une des bibliothèques, en changeant le symbole de collision en un autre nom - de même longueur mais une orthographe différente (mais, recommandation, la même longueur de nom)? Intrinsèquement méchant.

Il n'est pas clair si votre code appelle directement les deux fonctions avec le même nom mais des implémentations différentes ou si le conflit est indirect (il n'est pas non plus clair si cela fait une différence). Cependant, il y a au moins une chance extérieure que le changement de nom fonctionne. Cela pourrait également être une idée de minimiser la différence d'orthographe, de sorte que si les symboles sont dans un ordre trié dans une table, le changement de nom ne déplace pas les choses dans le désordre. Des choses comme la recherche binaire sont perturbées si le tableau qu'ils recherchent n'est pas dans l'ordre de tri comme prévu.

Jonathan Leffler
la source
Il n'est pas question de changer la bibliothèque sur disque, car la licence ne le permet pas. Changer les symboles en mémoire serait possible, mais je ne vois pas comment cela pourrait être fait (charger la bibliothèque en mémoire, la modifier puis la passer à l'éditeur de liens dynamique ... voir aucune méthode pour cela).
Mecki le
3
OK - il est temps de décider laquelle des deux bibliothèques est la moins importante et de trouver un remplaçant. Et expliquez à celui pour lequel vous payez mais que vous abandonnez que la raison pour laquelle vous changez est à cause de cette collision et qu'ils peuvent conserver votre entreprise en réglant votre problème.
Jonathan Leffler
2

@compatibility_alias sera capable de résoudre les conflits d'espace de noms de classe, par exemple

@compatibility_alias NewAliasClass OriginalClass;

Cependant, cela ne résoudra aucun des enums, typedefs ou conflits d'espace de noms de protocole . De plus, il ne joue pas bien avec @classles déclinaisons avant de la classe d'origine. Étant donné que la plupart des frameworks seront livrés avec ces éléments non-classe tels que les typedefs, vous ne seriez probablement pas en mesure de résoudre le problème d'espacement de noms avec uniquement Compatibility_alias.

J'ai examiné un problème similaire au vôtre , mais j'avais accès aux sources et je construisais les frameworks. La meilleure solution que j'ai trouvée pour cela était d'utiliser @compatibility_aliasconditionnellement avec #defines pour prendre en charge les enums / typedefs / protocoles / etc. Vous pouvez le faire de manière conditionnelle sur l'unité de compilation pour l'en-tête en question afin de minimiser le risque d'étendre des éléments dans l'autre cadre de collision.

Michael Chinen
la source
1

Il semble que le problème soit que vous ne pouvez pas référencer les fichiers d'en-têtes des deux systèmes dans la même unité de traduction (fichier source). Si vous créez des wrappers objective-c autour des bibliothèques (les rendant plus utilisables dans le processus), et que #incluez uniquement les en-têtes de chaque bibliothèque dans l'implémentation des classes wrapper, cela séparerait efficacement les collisions de noms.

Je n'ai pas assez d'expérience avec cela dans objective-c (je ne fais que commencer), mais je pense que c'est ce que je ferais en C.

chrish
la source
1
Mais ne seriez-vous pas encore confronté à des collisions si vous essayiez d'inclure les deux en-têtes de wrapper dans le même fichier (puisqu'ils auraient chacun besoin d'inclure eux-mêmes les en-têtes de leur framework respectif)?
Wilco
0

Préfixer les fichiers est la solution la plus simple que je connaisse. Cocoadev a une page d'espace de noms qui est un effort de la communauté pour éviter les collisions d'espaces de noms. N'hésitez pas à ajouter le vôtre à cette liste, je pense que c'est à cela qu'il sert.

http://www.cocoadev.com/index.pl?ChooseYourOwnPrefix

Ryan Townshend
la source
Comment le préfixe des fichiers aide-t-il si les objets définis dans les fichiers provoquent le problème? Je peux sûrement manipuler les fichiers h, mais alors l'éditeur de liens ne trouvera plus du tout les objets lorsqu'il aime contre le framework: - /
Mecki
Je crois que ce serait plutôt une meilleure pratique qu'une solution au problème initial.
Ali
Cela ne répond pas du tout à la question
Madbreaks
0

Si vous avez une collision, je vous suggère de réfléchir sérieusement à la façon dont vous pourriez refactoriser l'un des frameworks hors de votre application. Une collision suggère que les deux font des choses similaires, et vous pourriez probablement vous déplacer en utilisant un framework supplémentaire simplement en refactorisant votre application. Non seulement cela résoudrait votre problème d'espace de noms, mais cela rendrait votre code plus robuste, plus facile à maintenir et plus efficace.

Sur une solution plus technique, si j'étais à votre place, ce serait mon choix.

Allyn
la source
0

Si la collision se situe uniquement au niveau de la liaison statique, vous pouvez choisir la bibliothèque utilisée pour résoudre les symboles:

cc foo.o -ldog bar.o -lcat

Si foo.oet les bar.odeux font référence au symbole, ratalorslibdog résoudra foo.o« s ratet libcatrésoudra bar.o» s rat.

wcochran
la source
0

Juste une pensée .. non testé ou prouvé et pourrait être un moyen de la marque mais avez-vous envisagé d'écrire un adaptateur pour la classe que vous utilisez à partir du plus simple des frameworks .. ou au moins leurs interfaces?

Si vous deviez écrire un wrapper autour du plus simple des frameworks (ou de celle à laquelle vous accédez le moins), il ne serait pas possible de compiler ce wrapper dans une bibliothèque. Étant donné que la bibliothèque est précompilée et que seul son têtes doivent être distribués, vous masqueriez efficacement le cadre sous-jacent et seriez libre de le combiner avec le deuxième cadre en cas de conflit.

J'apprécie bien sûr qu'il y aura probablement des moments où vous devez utiliser des classes des deux frameworks en même temps, cependant, vous pouvez fournir des usines pour d'autres adaptateurs de classe de ce framework. Sur le dos de ce point, je suppose que vous auriez besoin d'un peu de refactorisation pour extraire les interfaces que vous utilisez des deux frameworks, ce qui devrait vous fournir un bon point de départ pour créer votre wrapper.

Vous pouvez construire sur la bibliothèque au fur et à mesure que vous avez besoin de fonctionnalités supplémentaires de la bibliothèque encapsulée, et simplement la recompiler lorsque vous changez.

Encore une fois, rien de prouvé mais j'avais envie d'ajouter une perspective. J'espère que ça aide :)

marque
la source
-1

Si vous avez deux frameworks qui ont le même nom de fonction, vous pouvez essayer de charger dynamiquement les frameworks. Ce sera inélégant, mais possible. Comment le faire avec les classes Objective-C, je ne sais pas. J'imagine que la NSBundleclasse aura des méthodes qui chargeront une classe spécifique.

MaddTheSane
la source