Au coeur de l’environnement d’exécution d’Objective-C
Note de l’éditeur — Dans ce premier opus d’une série à deux volets pour développeurs confirmés, Ezra Epstein dévoile les différentes manières d’accéder aux fonctions basiques de l’environnement d’exécution d’Objective-C. Il nous fait aussi jeter un oeil sur les catégories de classes. Dans le second article à paraître la semaine prochaine, il va un peu plus loin et traite de la manière dont l’environnement d’exécution a été implémentée. Cette étude nous ménera à une meilleure compréhension des catégories et elle nous révelera aussi des détails qui pourraient ne pas être présents dans les fichiers entêtes (.h). Le point culminant de cette petite incursion sera l’étude de RuntimeBrowser, un outil qui permet de naviguer dans les classes à la JavaBrowser. L’auteur l’a trouvé très utile et pense qu’il pourrait vous l’être aussi.
Introduction
Il était une fois un débat sur le dynamisme dans les langages de programmation (surtout les langages OO). Et le dynamisme a gagné : “reflection” s’est vu ajouté à Java et l’information sur le type lors de l’éxecution (ou Run-Time Type Information) a été ajouté au C++. Et le meilleur langage dynamique, Objective-C, ou ObjC en abrégé, fut utilisé pour la conception des frameworks de l’interface utilisateur de Mac OS X, dit “Cocoa”.
Avec le dynamisme, il est possible de coder des fonctionnalités qui seront configurées pendant l’exécution du programme. Ça peut être extrèmement utile dans de nombreux cas, par exemple lors de l’écriture de code générique d’accès aux bases de données (comme dans EOF), de l’écriture de requêtes/réponses HTTP génériques ou dans les systèmes de Services Web (comme dans WebObjects), ou encore lors de la conception d’interfaces utilisateurs dynamiques (i.e. avec InterfaceBuilder).
Une des raisons — oui, il y en a d’autres — de la grande popularité des langages de script, tels que Perl, pour la création de sites Web dynamiques, est qu’ils ont un dynamisme inhérent. L’interprétation (plutôt que la compilation) fait que le temps requis par l’environnement d’exécution comprend le “temps de compilation”. Bien sûr, il y a une diminution des performances (et une absence de vérification des types — que certains pourraient considérer comme une fonction) parmi d’autres problèmes. C’est alors que des langages dynamiques compilés tels qu’Objective-C font leur entrée.
La plupart des compilateurs sont conçus dans un seul but : convertir la logique codée dans un format utilisable par des humains (langage de programmation) en un format qui dit à l’ordinateur ce qu’il doit faire (langage machine). Une petite quantité d’information “supplémentaire” est rajoutée pour permettre à l’éditeur de lien de relier les différentes parties du code (machine) exécutable (ou pour permettre à un programmeur de débugger son code).
Ce qui signifie que la plupart des compilateurs perdent beaucoup d’informations. Le compilateur Objective-C est “intelligent”. Par là, je veux dire qu’il n’”oublie” pas toute l’intelligence que vous avez mise dans la conception de votre code. Les informations contenus dans la structure de votre code (tels que les noms de classes et de méthodes) sont extraits par le compilateur et rangés dans des structures de données (C structs). Ces structures de données sont disponibles pendant l’exécution du programme et, avec les fonctions qui permettent d’accèder et de mettre à jour ces informations, forment l’environnement d’exécution d’Objective-C.
Les fonctions de base de l’environnement d’exécution d’Objective-C
Objective-C fournit un dynamisme complet. C’est pourquoi vous n’aurez pas, entre autres choses, à mettre la “main à la pâte”. L’accès à l’environnement d’exécution d’ObjC via les fonctions et les classes ObjC (Cocoa) se font grâce à la Foundation framework. NSObjCRuntime.h définit plusieurs de ces fonctions. De plus, toutes les classes qui implémentent le protocole NSObject (c’est à dire toutes les sous-classes de NSObject) en ont plus et par défaut. La Foundation framework se trouve sur tout ordinateur sur lequel Mac OS X est installé. Toutefois, si vous voulez écrire du code, vous aurez à intaller le paquetage Developer qui va installer à cette fin les fichiers entêtes, ainsi que d’autres frameworks.
Objective-C est bien évidement orienté objets. C’est pourquoi nous commençerons notre étude avec un objet, ou plutôt une classe (dont l’objet est une instance). Puisque nous sommes en train d’étudier le dynamisme, supposons que nous avons un texte ou un fichier de configuration en XML (peut-être un fichier “property-list”, un .plist, dans Mac OS X). Ce fichier de configuration contient des instructions, et commence par le nom de la classe à utiliser. Avec un accès à l’environnement d’exécution, nous pouvons transformer le nom d’une classe (une chaine de caractères) en la Classe elle même :
#import <Foundation/NSObjCRuntime.h> NSString *aClassName; // lu a partir du fichier de configuration Class namedClass = NSClassFromString(aClassName);
Si le nom de la classe n’existe pas dans l’environnement d’exécution (i.e. la classe n’a pas été chargée), NSClassFromString() retourne Nil.
NB : Nous avons importé précisément le fichier .h que nous allions utiliser pour des raisons de clareté. Normalement vous auriez fait un #import <Foundation/Foundation.h> pour tirer partie de la vitesse des fichiers entêtes précompilés.
Une fois que nous avons la classe, nous voulons pouvoir invoquer ses méthodes. Pour ce faire, nous allons utiliser un “selector” dont le type est SEL.
NSString *aMethodName; // Existe. i.e., provient du fichier de "config" SEL aSelector = NSSelectorFromString(aMethodName);
Si aMethodName ne contient pas le nom d’une méthode d’une des classes actuellement chargées dans l’environnement d’exécution, alors aSelector sera NULL.
(Plus en détails : les sélecteurs (”selector“) sont utilisé pour l’invocation de méthodes. ObjC fait la distinction entre l’invocation d’une méthode et son implémentation. Cela ressemble beaucoup à avoir des pointeurs sur des fonctions en C, avec l’avantage que toutes les fonctions sont nommées et peuvent être référencées par leur nom. Les fonctions qui portent le même nom peuvent être résolus en (différentes) implémentations suivant la classe de l’objet. Parfois, celà semble compliqué, mais quand vous l’aurez bien compris, vous réaliserez que cela donne beaucoup de pouvoir au développeur.)
En interne, un sélecteur (SEL) est un const char*. Vérifez le par vous-même :
printf("%sn", aSelector);
(Vous aurez peut-être besoin de faire un cast sur aSelector vers un const char*pour éviter l’avertissement du compilateur). Les SELs ont une fonction supplémentaire : ils sont uniques dans l’environnement d’exécution de sorte que deux SELs de la même méthode montrent l’égalité des pointeurs.
Le protocole NSObject définit une méthode qui permet d’invoquer un sélecteur sur une classe ou une instance. Pour une classe, il suffit de procéder ainsi :
SEL desc = NSSelectorFromString(@"description"); [namedClass performSelector:desc];
Dans l’invocation précédente, l’environnement d’exécution est utilisé pour relier dynamiquement le sélecteur à une implémentation sous-jacente de méthode. Dans le cas d’ObjC, les méthodes sont toujours reliées dynamiquement. Même lors des invocations “ordinaires”, les méthodes sont reliées dynamiquement à leur implémentation. (Veuillez vous reporter à Dynamic Binding dans la documentation sur Cocoa pour de plus amples détails sur le sujet).
Prenons une instance de notre classe précédente :
id anInstance = [[[namedClass alloc] init] autorelease];
(Ceci présuppose que l’initialisateur par défaut pour la classe namedClass est -init. Reportez-vous à Allocation and Initialization: The Designated Initializer pour de plus amples détails.) Nous pouvons alors envoyer un message à notre instance de namedClass (si elle hérite de la classe NSObject sinon, il faut qu’elle implémte le protocole NSObject):
[namedClass performSelector:desc];
Les choses sont sensiblement les mêmes quand il s’agit d’envoyer des messages à (ou d’invoquer) des méthodes qui prennent des paramètres. NSObject définit deux méthodes bien utiles :
- (id)performSelector:(SEL)aSelector withObject:(id)object; - (id)performSelector:(SEL)aSelector withObject:(id)object1 withObject:(id)object2;
Les méthodes désignées par aSelector doivent retourner un objet. (Si votre méthode retourne un type C, vous pouvez “enrober” le résultat dans un NSValueavant de le renvoyer. Sinon, pour envoyer un message à des méthodes qui ne retournent pas d’objet, vous aurez à utiliser NSInvocation).
Au coeur de l’environnement d’exécution d’Objective-C (suite)
Si vous voulez savoir le nombre de paramètres qu’une certaine méthode requière, il vous suffit de compter le nombre de colonnes dans le selecteur. Un selecteur (SEL) est un const char* (qui a été rendu unique). Vous pouvez donc utiliser les fonctions en C standard.
NSObject ne présente pas d’API pour appeler dynamiquement une méthode qui demande plus de deux paramètres. Il faudra donc plutôt invoquer la fonction sous jacente de l’environnement d’exécution. La fonction s’appelle objc_msgSend(), qui est définie ainsi :
id objc_msgSend(id self, SEL op, ...);
objc_msgSend() prend en arguments un objet sur lequel une méthode va être invoquée (une “cible”), un selecteur (ou action) à réaliser et ses arguments, variable en nombre.
#import <objc/objc-runtime.h>; // pour objc_msgSend() SEL fiveArgumentSelector = @selector(aMethod:that:takes:five:arguments:); id result = objc_msgSend(myCustomInstace, fiveArgumentSelector, arg1, arg2, arg3, arg4, arg5);
Une exception est levée si vous “envoyez” un message à un objet qui n’implémente pas la méthode correspondante. Remarquez que nous avons #importé un nouveau fichier entête. Sur Mac OS X, vous trouverez les entêtes ObjC dans /usr/include/objc (essayez System/Developer/Headers/objc pour les autres systèmes).
Une des nombreuses utilisations élégantes et intelligentes de la liaison dynamique (”dynamic binding”) sont les “délégués” (”delegates”, utilisés plutôt que le terme français, dans la suite de l’article). Les ‘delegates’ sont très largement utilisés dans AppKit, et permettent ainsi de personnaliser les objets standards d’AppKit en intervenant au niveau des points de décision et modifient ainsi leur comportement. Plutôt que d’enregistrer une chaine de fonction de ‘callback’, vous implémenterez les méthodes ‘delegates’ que vous voulez intercepter (tels que : -windowWillClose:).
Grâce au typage et à la liaison dynamiques, l’implémentation peut être faite sur TOUTE classe. Ce qui est vraiment cool ici, grâce au dynamisme d’ObjC, c’est qu’il n’est pas necéssaire de vous conformer à un protocole ou bien d’implémenter toutes les méthodes déléguées : chaque méthode déléguée sera appelée si et seulement si elle est implémentée.
Nous pouvons agir ainsi parce qu’il est possible de demander à l’environnement d’exécution quelles méthodes sont implémentées par un objet. Encore une fois, NSObject fournit une interface OO très pratique à l’environnement d’exécution. Notre objet implémente-t-il telle ou telle méthode ? Il suffit de le lui “demander” :
if ([aDelegate respondsTo:someSelector]) ...
En voici la déclaration:
- (BOOL)respondsToSelector:(SEL)aSelector;
Une autre fonctionalité de la liaison dynamique permet au objets conteneurs (’containers’) tels que NSArray, NSDictionay, NSSet, etc. d’envoyer régulièrement des messages aux objets qu’ils contiennent, et ce sans qu’il soit nécessaire que les objets soient à certains endroits de la hiérarchie d’héritage ou qu’ils adoptent certains protocoles formels. Ainsi NSArray déclare ceci :
- (void)makeObjectsPerformSelector:(SEL)aSelector; - (void)makeObjectsPerformSelector:(SEL)aSelector withObject:(id)argument;
-(id)performSelector:(SEL)aSelector et -(id)performSelector:(SEL)aSelector withObject:(id)object, invoquent un selecteur sur chaque objet contenu par le NSArray. Il est possible d’ajouter un makeObjectsPerform… supplémentaire qui prend 2 (ou 3 voire 4) arguments grâce aux catégories.
Catégories
Les catégories sont une fonctionnalité d’Objective-C qui permettent d’augmenter les possibilités de TOUTE classe. Vous n’avez pas besoin d’avoir le code source de la classe, et pas besoin de recompiler non plus. Il suffit de déclarer une catégorie, d’implémenter la méthode, et basta ! Toutes les instances de la classe, et toutes ses sous-classes répondent dorénavant à ces nouvelles méthodes. Une utilisation classique des catégories est d’ajouter des méthodes de commodité :
@implementation NSString (NSString-Extensions)
- (BOOL) containsString:(NSString *)aString;
/*" Retourne YES si le receveur contient aString, NO sinon. "*/
{ return ([self rangeOfString:aString].length != 0); }
@end
Mais vous pouvez ajouter toutes les fonctions dont vous avez besoin. Parfois, c’est la meilleure façon d’obtenir ce que l’on voulait. D’autres fois, ça ne le sera pas, et vous implémenterez de préférence des fonctions auxiliaires. Par exemple :
BOOL doesStringContainSubstring(NSString* string, NSString *substring)
{ return (string != nil && [string rangeOfString:substring].length != 0); }
Avec les catégories, vous pouvez ajouter des méthodes d’instance ou de classe, mais vous ne pourrez pas ajouter des variables d’instance.
Une autre utilité des catégories est de grouper les fonctionnalités et les responsabilités de l’écriture du code au sein d’un groupe de développeurs. Séparer les catégories dans d’autres fichiers sources (.h et .m) peut être une bonne façon d’organiser le développement de vos propres classes.
Le MiscKit fait une utilisation intensive des catégories et fournit des exemples de son bon usage ainsi que du code que vous pourrez trouver utile de réutiliser.
Modifier le comportement
Les catégories vous permettent aussi de réécrire et de personnaliser le comportement des méthodes existentes. Bien que j’ai rencontré des cas où cette façon de faire m’a permis de réaliser des choses que je n’aurais pas pu obtenir autrement, l’utilisation de cette fonctionnalité demande de précautions. Si vous réécrivez une méthode de bas niveau (tel qu’une méthode de dispatch sur NSObject), il peut y avoir de sérieux effets secondaires. Une telle utilisation est généralement DÉCONSEILLÉE. C’est toujours une bonnde idée de tester votre code minucieusement, en effet, modifier les méthodes d’un objet grâce aux catégories requièreune bonne phase de tests.
La réécriture d’une méthode grâce à une catégorie ne requière pas de code spécifique, il suffit juste de déclarer et d’implémenter la méthode.
@implementation NSObject (ObjectOverride)
- description {
return [NSString stringWithFormat:@"<objc-object>%@</objc-object>", [self class]];
}
@end
Notez que, comme mentionné précédemment, cette utilisation est déconseillée et mène souvent vers des effets secondaires indésirables. Dans les cas suivants, utilisez les catégories pour étendre une classe :
@implementation NSObject (XMLDescription)
- xmlDescription {
return [NSString stringWithFormat:@"<objc-object>%@</objc-object>", [self class]];
}
@end
Vous pouvez implémenter aussi bien des méthodes d’instance que des méthodes de classe dans les catégories.
Si vous cherchez une formation plus poussée sur Objective-C et ses fonctionnalités, une excellente source d’information est disponible sur le site Web d’Apple ici : http://developer.apple.com/techpubs/macosx/Cocoa/ObjectiveC/ObjC.pdf
Dernières réflexions
Maintenant que vous avez commencé l’exploration de l’environnement d’exécution, j’espère que vous allez jeter un oeil à l’article de la semaine prochaine qui va plus loin et étudie la manière dont il a été implémenté. De plus, je vais vous présenter le navigateur de l’environnement d’exécution, que vous trouverez très utile à mon avis. On se revoit à ce moment là !
Liens
Un autre tutoriel sur le langage Objective-C, écrit par Pejvan, est disponible à l’adresse suivante : Apprendre l’Objective-C (pdf).
Le site de l’auteur de ce tutoriel : www.prajna.com.

Textes originaux en anglais sur O’Reilly : Inside the Objective-C Runtime, Part one par Ezra Epstein
Chargement
Commentaires récents