D/Objective-C: frappé un mur, nouveau départ
Il y a environ trois ans, je travaillais sur un petit projet à moi qui consistait à utiliser les capacités de « template » du langage de programmation D pour créer un pont entièrement transparent avec la runtime Objective-C. Ça fonctionnait plutôt bien : les objets pouvait passer sans soucis entre les deux monde et c’était plutôt simple d’écrire des bindings pour les classes Objective-C. Ça n’a pas pris beaucoup de temps avant que quelque failles apparaissent…
Les failles
Le premier problème était que c’était atrocement lent de compiler quoi que ce soit avec le pont. Bien que j’ai réussi à réduire le temps de compilation par un facteur 2 ou 3, ce n’est pas encore qualifiable de rapide. Mais ce n’est pas un défaut fatal.
Un deuxième problème était les petites inefficacités un peu partout. Normalement, compiler un objet Objective-C construit un fichier objet avec un format particulier que le linker dynamique et la runtime Objective-C se chargent d’initialiser ce qui le doit. Mais c’est impossible à faire en D, je doit donc tout initialiser à l’exécution du programme, et ceci à la demande (lazily) pour éviter de créer des dépendances circulaires entre les modules ayant des constructeurs statiques. Il y a beaucoup de code à cet effet dans le pont.
En conséquence de cette initialisation à la demande, les bindings doivent parfois être initialisées manuellement. C’est le cas par exemple avant de charger un fichier nib. C’est pas vraiment idéal.
Mais le véritable blocage et la taille du code généré par les bindings. Le système j’ai créé doit générer beaucoup de code pour chacune des fonctions et classes pour lesquelles on crée des bindings. Ceci gonfle de beaucoup la taille de l’exécutable final (et pourrait en partie expliquer la lenteur de compilation). La petite application de test incluse avec le pont utilise environ 60 Mo de code (compilé), la même chose écrite en Objective-C prendrait quelque kilooctets.
Ceci n’était pas le cas au début. Tout ce bagage est apparut au fur et à mesure que j’ai ajouté des bindings pour différentes classes de Cocoa. C’est un véritable problème parce que personne ne veut distribuer un exécutable avec 60 Mo de code qui restera inutilisé la plupart du temps. Une solution pourrait être que chacun écrive des bindings contenant uniquement les classes et méthodes don’t il se sert, mais c’est d’un intérêt plutôt limité.
Une nouvelle approche
Alors comment rendre les bindings plus légers et éviter le non-sens qu’est initialisation à la demande ? Il y a une façon : inclure le modèle d’objet d’Objective-C dans le compilateur. Si le compilateur comprend ce qu’est un objet Objective-C, comment appeler ses méthodes et comment générer le fichier objet, il deviendra aussi bon qu’un compilateur Objective-C avec comme seule différence la syntaxe qui sera celle du D.
Alors c’est donc ce que j’ai entrepris ce dernier mois. J’ai joué avec le code source de DMD et tenté de lui faire générer du code à la Objective-C.
Il n’y a qu’une seule chose de fonctionnelle actuellement : déclarer une interface extern (Objective-C)
et appeler des fonctions sur elle. Ça fonctionne comme ceci :
extern (Objective-C)
interface NSObject {
NSObject description();
}
Maintenant, quand vous avez une instance de NSObject
dans la variable objet
, vous pouvez écrire objet.description()
et ce sera équivalent à écrire [objet description]
en Objective-C. À l’interne, l’appel est transformé en objc_msgSend(objet, @selector(description))
. J’ai ajouté au compilateur la capacité de placer le sélecteur dans le segment __OBJC,__message_refs
où il est automatiquement enregistré par la runtime lors du chargement de l’exécutable. C’est exactement ce que fait le véritable compilateur Objective-C, et c’est beaucoup mieux que de générer tout plein de code pour enregistrer le sélecteur à la demande lors de l’exécution.
Pour les méthodes ayant plus d’un argument, le compilateur ne peut déduire le nom d’un sélecteur à partir du nom de la fonction parce que le sélecteur est généralement séparés en plusieurs parties. Donc j’ai du ajouter une syntaxe permettant de spécifier manuellement le sélecteur pour la fonction. Ça ressemble à ça :
extern (Objective-C)
interface NSString {
void getCharacters(wchar* buffer, NSRange range) [getCharacters:range:];
}
Et ça marche aussi ! De cette façon, appeler string.getCharacters(b, r)
émet le même code que l’appel Objective-C [string getCharacters:b range:r]
.
Tout ceci est bien, mais toujours incomplet. Pour l’instant vous devez trouver un pointeur vers un objet Objective-C, le « caster » dans le type de la bonne interface, puis ensuite appeler des fonctions. C’est pas si mal, mais ce n’est pas très utile en soit non plus. Il y a encore beaucoup de travail pour rendre Cocoa utilisable avec le D.
Mais le débrousaillage est fait, il sera maintenant plus facile de continuer. Ça prendra certainement plusieurs mois encore avant que ça marche bien, plus ou moins dépendant du temps que je peut dévouer à ce projet. Je veux que le résultat final permette de mélanger des objets Objective-C et D de façon transparente, et la possibilité de créer des classes dérivées de classes Objective-C.
Si vous aimeriez utiliser D avec Cocoa sur Mac, j’aimerai bien avoir vos impressions. Et si vous voulez m’encourager plus, vous pouvez faire un don : si je reçoit suffisamment de support ce sera probablement plus facile de dévouer plus de temps à ce projet.