Accéder à CoreFoundation avec Swift

Si je veux que mon application obtienne les icones des volumes actuellement affichés dans la barre latérale du Finder, ou encore obtenir la liste des dix dernières applications lancées par l'utilisateur, ce genre de chose, je dois faire appel à CoreFoundation et à ses étranges API en C.

Mais accéder aux API CoreFoundation d'OS X avec Swift n'est pas vraiment intuitif.

C'est que ces API sont très différentes des API modernes, et il est souvent necessaire d'étudier la documentation en profondeur… quand elle existe.

Ensuite il faut s'adapter à la gestion de la mémoire qui est différente, ainsi qu'inspecter un peu partout pour trouver les signatures des méthodes, le type des objets…

On va étudier un exemple concret en détail, et en profiter pour lister quelques astuces.

Vraiment ?

Il faut donc accéder à de vieilles API en C pour obtenir ce genre d'informations basique, en 2015, à partir de Swift, un langage de haut niveau ?

Oui, mais… elles ne sont pas mauvaises, ces API CoreFoundation. Elles fonctionnent très bien. Elles sont juste étranges.

Ou plutôt, elles donnent l'impression de venir d'un monde ancien, alors qu'elles ne sont pas vraiment vieilles (10.5, Leopard). Mais elles sont si différentes des API récentes que c'en est déstabilisant.

Gestion de la mémoire

Dans les langages de la famille C il faut gérer la mémoire soi-même. On alloue (“alloc”, “malloc”) des blocs de mémoire de taille X pour y placer des éléments de taille X, et quand on n'a plus besoin de ces éléments, on doit retenir/libérer (“retain”, “release”) le bloc de mémoire.

Il faut faire attention à ne pas accéder à un bloc qui vient d'être libéré, à ne pas libérer un bloc qui sera utilisé plus tard par un autre process, à ne pas excéder la taille des blocs avec les données… bref, c'est chaud.

En Objective-C c'était un peu moins dur grâce à ARC, qui comptait semi-automatiquement les références aux blocs mémoire et les libérait automatiquement quand ils n'étaient plus référencés par aucun process.

En Swift il n'y a pas ce problème : à quelques exceptions près, l'allocation et la libération des blocs mémoire est automatique et le développeur n'a pas à s'en soucier.

Sauf lorsqu'on interagit avec des API en Objective-C ou en C, donc : même si l'on est en Swift, on reçoit et envoie des objets à des API qui ont besoin que la mémoire soit gérée (semi)manuellement, il faut donc gérer aussi.

Que se passe-t-il si l'on ne gère pas correctement ces allocations et libérations ?

Au mieux, et en principe, l'application va fuiter (leak) quelques bits ou octets, sans conséquence alarmante. Pourtant, ces fuites vont souvent empirer et/ou se multiplier et finir par encombrer la mémoire jusqu'à créer des problèmes pour le système d'exploitation. Et au pire, ces fuites vont créer des corruptions de données en mémoire et faire crasher l'application, créer des failles de sécurité ou même planter le système.

Concrètement, dans notre exemple, il faudra décider dans quels cas nous utilisons la retained value d'un objet provenant de CoreFoundation, et dans quels cas utiliser la unretained value.

En fait c'est simple, il y a une convention : si la méthode/fonction utilisée comporte “Copy” ou “Create” dans son nom, alors il faut utiliser la “retained value” ; sinon, utiliser “unretained value”.

Dans le premier cas on fait faire une copie des données à l'API, qui crée donc de nouvelles allocations rien que pour nous, il faut donc utiliser cette valeur “retained” gérée par ARC ; dans le dernier cas, pas de copie ou de création et donc pas besoin d'utiliser ARC, on prend “unretained”.

Exemple : icones de barre latérale du Finder

Comment obtenir les icones monochromes de volumes affichés à un instant donné dans la barre latérale du Finder ?

icones

Par exemple, disons que j'ai besoin pour mon app de pouvoir montrer une liste des disques durs et clés USB branchés, et que je veux pouvoir associer les noms de ces volumes à des icones, comme le fait le Finder donc…

En cherchant des mots-clés sur Google on finit par trouver des références à de vieux (2006) “headers” (en Objective-C et C, fichiers d'en tête contenant la déclaration des méthodes d'une classe ou famille de classes) contenant des constantes faisant référence à des icones, tel IconsCore.h, mais ce des références aux icones “normaux” du Finder, pas à ceux de la barre latérale, qui ont changé d'aspect plusieurs fois, notamment depuis Yosemite.

On découvre ensuite qu'il n'y a pas d'API pour quérir ces icones particuliers de toute façon, mais qu'ils répondent à une clé, kLSSharedFileListFavoriteVolumes, définie dans LSSharedFileList.h, un fichier qui n'est plus d'actualité depuis 2012… mais qui n'a pas été remplacé depuis, et qu'il faut donc tout de même utiliser.

Comment accéder à cette clé, étant donné que ce fichier header n'est pas exposé à Swift ?

On découvre que ces méthodes en C sont exposées à Objective-C, et comme Objective-C est exposé à Swift, on va traduire les appels Objective-C en Swift à partir de vieux exemples.

Voici une des meilleures solutions en Objective-C postée sur StackOverflow, c'est celle que nous allons adpater :

LSSharedFileListRef list = LSSharedFileListCreate(NULL, kLSSharedFileListFavoriteVolumes, NULL);
UInt32 seed;
NSArray* items = CFBridgingRelease(LSSharedFileListCopySnapshot(list, &seed));
CFRelease(list);
for (id item in items)
{
    IconRef icon = LSSharedFileListItemCopyIconRef((__bridge LSSharedFileListItemRef)item);
    NSImage* image = [[NSImage alloc] initWithIconRef:icon];

    // Do something with this image

    ReleaseIconRef(icon);
}

On va étudier ça étape par étape.

Inspecteur Gadget et Sherlock Holmes

Mettons notre chapeau à idées -et à gadgets- et commençons par la première ligne.

En Objective-C, le premier élément d'une déclaration de variable est le type de la variable ; ensuite vient le nom de la variable.

Ici donc la première ligne

LSSharedFileListRef list = LSSharedFileListCreate(NULL, kLSSharedFileListFavoriteVolumes, NULL);

crée une variable nommée list de type LSSharedFileListRef.

Ce type est défini comme ceci dans LSSharedFileList.h :

/* The shared file list API is for sharing and storing list of references to file system objects.
   The shared file list is a persistent list of objects, where each item has assigned display name, icon, and url
   as well as other optional properties.
   Each list can also have various properties attached.
*/
typedef struct OpaqueLSSharedFileListRef*  LSSharedFileListRef;
typedef struct OpaqueLSSharedFileListItemRef*  LSSharedFileListItemRef;

Très bien, donc cette “list ref” est une struct qui va contenir les propriétés demandées en fonction des clés utilisées.

On lit plus loin que la première clé est en fait celle qui nous intéresse :

/*
 *  kLSSharedFileListFavoriteVolumes
 *  
 *  Availability:
 *    Mac OS X:         in version 10.5 and later in CoreServices.framework
 *    CarbonLib:        not available
 *    Non-Carbon CFM:   not available
 */
extern CFStringRef kLSSharedFileListFavoriteVolumes __OSX_AVAILABLE_STARTING(__MAC_10_5, __IPHONE_NA);

Retournons à notre code Objective-C :

LSSharedFileListRef list = LSSharedFileListCreate(NULL, kLSSharedFileListFavoriteVolumes, NULL);

C'est donc cette fonction LSSharedFileListCreate qui va retourner les propriétés en utilisant notre clé.

Sachant que NULL est l'équivalent de nil, on va donc écrire en Swift :

let list = LSSharedFileListCreate(nil, kLSSharedFileListFavoriteVolumes, nil)

Et bien évidemment ça ne compile pas. Problème : depuis cet exemple (pourtant le plus récent sur le web) la signature de la méthode a changé. Maintenant le compilateur exige cette signature :

func LSSharedFileListCreate(inAllocator: CFAllocator!, inListType: CFString!, listOptions: AnyObject!) -> Unmanaged<LSSharedFileList>!

Ouch. Déjà, tous ces ! signifient que l'on ne va pas bénéficier des Optionnels de Swift, et donc que tous les paramètres sont obligatoires, impossible de passer nil.

Le premier paramètre désormais exigé, le CFAllocator, pourrait être un système d'allocation d'objets CoreFoundation customisé. Nous n'avons pas besoin de ces complications supplémentaires et allons donner un allocateur par défaut que l'on n'utilisera jamais ensuite, représenté par une constante (définie dans les fichies header), kCFAllocatorDefault (on va supposer que l'ancienne version de cette méthode créait un allocateur par défaut automatiquement si on passait nil).

Pour la CFString pas de souci, notre clé kLSSharedFileListFavoriteVolumes semble correspondre à ce type.

Pour la listOptions le header dit qu'il faut passer nil par défaut mais comme le compilateur n'est plus d'accord, on va utiliser la même astuce que pour l'allocateur et passer cette fois-ci un object mutable, par exemple un NSMutableDictionary vide (qui ne servira pas ensuite).

Ces opérations un peu cowboy n'ont pour but que de pouvoir utiliser une API en pleine transition, de la rendre compatible, quitte à créer, terrible crime, des objets inutilisés. :)

Résultat :

let listBase = LSSharedFileListCreate(kCFAllocatorDefault, kLSSharedFileListFavoriteVolumes, NSMutableDictionary())

Oh mais quoi alors, ça ne marche toujours pas !

C'est que notre clé kLSSharedFileListFavoriteVolumes en Swift est un “wrapper” pour la clé en Objective-C, il faut donc la “sortir de sa boite”, et utiliser pour ce faire soit “takeRetainedValue” soit “takeUnretainedValue”, c'est-à-dire soit utiliser ARC soit ne pas l'utiliser.

Pour cette clé, on voit qu'elle ne crée rien ni ne copie rien, c'est juste une clé, pas besoin qu'ARC gère la mémoire, on utilise donc “takeUnretainedValue” :

let listBase = LSSharedFileListCreate(kCFAllocatorDefault, kLSSharedFileListFavoriteVolumes.takeUnretainedValue(), NSMutableDictionary())

Et voilà notre première ligne traduite, le compilateur est content et ne produit plus de message d'erreur.

In and out

Dans l'original en Objective-C, les trois lignes suivantes servent à accéder aux données et à les mettre dans un array :

UInt32 seed;
NSArray* items = CFBridgingRelease(LSSharedFileListCopySnapshot(list, &seed));
CFRelease(list);

La variable seed est là pour des raisons de compatibilité et représente un état de départ pour le snapshot (on va préciser ces nouveautés juste après, pas de panique). Elle est déclarée comme un UInt32, c'est-à-dire un Integer non-signé (nombre entier positif) de 32 bits.

Ligne suivante est créé un pointeur (*) vers un NSArray nommé “items”, qui contiendra les propriétés que la fonction LSSharedFileListCopySnapshot va récupérer dans notre list.

Cette fonction LSSharedFileListCopySnapshot prend en argument la list et passe en inout (attribue à la variable elle-même) un flag à la seed.

Notons que nous n'avons pas besoin d'adapter CFBridgingRelease car nous gèrerons la mémoire directement sur LSSharedFileListCopySnapshot. On n'a pas besoin non plus de libérer list ensuite puisque nous n'étions pas passés par ARC pour l'utiliser.

Premier essai pour notre version en Swift :

var seed:UInt32?
let itemsCF = LSSharedFileListCopySnapshot(list, &seed)

Bleuargh, le compilateur pleure.

Tout d'abord nous avons oublié d'utiliser ARC sur le résultat de LSSharedFileListCreate quand on a fait la liste (on a sorti la valeur de la clé, mais pas celle du résultat de la fonction). Donc il faut “sortir de sa boite” la valeur de list et lui donner le bon type.

Révisons notre traduction des premières lignes :

let listBase = LSSharedFileListCreate(kCFAllocatorDefault, kLSSharedFileListFavoriteVolumes.takeUnretainedValue(), NSMutableDictionary())
let list = listBase.takeRetainedValue() as LSSharedFileList

Et recommencons notre essai :

var seed:UInt32?
let itemsCF = LSSharedFileListCopySnapshot(list, &seed)

Ca ne marche toujours pas, mais pour une autre raison : contrairement à la convention, le inout ne veut pas prendre un Optionnel, il lui faut une valeur par défaut. Allons-y :

var seed:UInt32 = 0
let itemsCF = LSSharedFileListCopySnapshot(list, &seed)

Et ça marche, le compilateur est content.

Pause

Qu'est-ce que l'on vient de faire, en fait ?

LSSharedFileListCreate va créer une liste de fichiers ou volumes qui seront compatibles avec certaines clés prédéfinies.

Ces clés représentent des données du système que l'on veut récupérer : des noms, icones, adresses, options, tags, etc.

Dans notre cas on veut les icones de nos volumes favoris (c'est comme ça qu'Apple appelle les disques durs, disques ejectables et autres volumes qui viennent se loger dans la barre latérale du Finder).

La clé qui correspond à ces propriétés est kLSSharedFileListFavoriteVolumes.

Nous utilisons ensuite LSSharedFileListCopySnapshot qui va récupérer un snapshot de ces propriétés, c'est-à-dire les données telles qu'elles sont à l'instant T de l'exécution de la fonction.

La liste des volumes

Ce snapshot nous renvoie un array de type CoreFoundation (CFArray) que l'on ne va pas utiliser tel quel pour éviter d'avoir à gérer la mémoire manuellement.

Dans l'original cet array était d'ailleurs converti en NSArray, mais nous allons à la place le convertir, car compatible, en tant qu'array Swift dont les éléments sont de type LSSharedFileListItemRef.

Comme c'est juste compatible ça n'est pas garanti, donc le typecast renvoie un Optional.

D'autre part, LSSharedFileListCopySnapshot créée manifestement une copie gérée par ARC, mais on sait désormais comment s'en servir.

Si l'on observe cette partie du code original en Objective-C :

for (id item in items)
{
    IconRef icon = LSSharedFileListItemCopyIconRef((__bridge LSSharedFileListItemRef)item);
    NSImage* image = [[NSImage alloc] initWithIconRef:icon];

    // Do something with this image

    ReleaseIconRef(icon);
}

on voit une boucle qui itère dans l'array de propriétés, et que ces propriétés ne sont pas encore typées (id est comme AnyObject).

Ensuite dans la boucle est extraite –et typée– la propriété correspondant à l'icone, puis une image en est créée.

Pour notre traduction en Swift, essayons d'abord de faire notre typecast du CFArray vers un Swift array de LSSharedFileListItemRef :

if let items = itemsCF.takeRetainedValue() as? [LSSharedFileListItemRef] {

}

Le compilateur ne râle pas, formidable.

Déclarons maintenant notre boucle à l'intérieur :

if let items = itemsCF.takeRetainedValue() as? [LSSharedFileListItemRef] {
    for item in items {

    }
}

Chaque item sera un objet correspondant à notre clé, et contenant des propriétés spécifiques, notamment une référence à l'icone utilisée par le volume (ou fichier) correspondant.

Pour extraire cette propriété de l'item il faut utiliser une fonction de CoreFoundation nommée LSSharedFileListItemCopyIconRef :

if let items = itemsCF.takeRetainedValue() as? [LSSharedFileListItemRef] {
    for item in items {
        let icon = LSSharedFileListItemCopyIconRef(item)

    }
}

Et que fait-on de cet objet “icon” de type IconRef, qui n'est pas une image ?

Et bien la bonne vieille classe NSImage sait en créer une image, ce qui va donc nous donner comme code définitif :

if let items = itemsCF.takeRetainedValue() as? [LSSharedFileListItemRef] {
    for item in items {
        let icon = LSSharedFileListItemCopyIconRef(item)
        let image = NSImage(iconRef: icon)
        // ici `image` contient une représentation de l'icone
    }
}

Icones et images

Le code au complet :

let listBase = LSSharedFileListCreate(kCFAllocatorDefault, kLSSharedFileListFavoriteVolumes.takeUnretainedValue(), NSMutableDictionary())
let list = listBase.takeRetainedValue() as LSSharedFileList
var seed:UInt32 = 0
let itemsCF = LSSharedFileListCopySnapshot(list, &seed)
if let items = itemsCF.takeRetainedValue() as? [LSSharedFileListItemRef] {
    for item in items {
        let icon = LSSharedFileListItemCopyIconRef(item)
        let image = NSImage(iconRef: icon)
        // ...
    }
}

Et voilà !

Confirmer ces connaissances

Voici juste pour le plaisir un autre exemple, totalement similaire, qui va nous permettre de confirmer tout ce que l'on a appris : disons que l'on souhaite obtenir la liste des dernières applications ouvertes par l'utilisateur.

Nous allons en fait suivre le même modèle que pour les icones, mais passer une clé différente, et utiliser des fonctions spécifiques pour récupérer les propriétés.

Notre clé sera ici kLSSharedFileListRecentApplicationItems.

L'array de propriétés contiendra des NSURL (chemin vers les applications) et des NSString (le nom des applications).

On utilisera respectivement LSSharedFileListItemCopyResolvedURL et LSSharedFileListItemCopyDisplayName pour accéder à ces propriétés.

Le code au complet :

let listBase = LSSharedFileListCreate(kCFAllocatorDefault, kLSSharedFileListRecentApplicationItems.takeUnretainedValue(), NSMutableDictionary())
let list = listBase.takeUnretainedValue() as LSSharedFileListRef
var seed:UInt32 = 0
let itemsCF = LSSharedFileListCopySnapshot(list, &seed)
if let items = itemsCF.takeUnretainedValue() as? [LSSharedFileListItemRef] {
    for item in items {
        let listItemURL = LSSharedFileListItemCopyResolvedURL(item, 0, nil)
        let urlBase = listItemURL.takeUnretainedValue()
        let url = urlBase as NSURL
        let listItemName = LSSharedFileListItemCopyDisplayName(item)
        let name = listItemName.takeUnretainedValue() as String
        println(url)
        println(name)
    }
}

Conclusion

Notre langage de haut niveau, Swift, nous offre un pont vers un autre langage, Objective-C, qui lui nous offre un pont vers les API en C de Mac OS X, nommées “CoreFoundation”.

Dans notre exemple ça prend la forme de clés (des constantes définies dans des fichiers header) qui correspondent à des éléments du système que l'on peut lister en utilisant des fonctions dédiées.

Ces fonctions retournent des listes d'objets que l'on peut typer et gérer pour accéder aux propriétés que l'on cherche.

Il faut au passage penser à utiliser la gestion de la mémoire semi-automatique ARC quand nécessaire, et ne pas hésiter à contourner bugs et limitations par des astuces un peu vaudou.

Au final, on traverse les mondes et les années, utilisant de nombreuses bibliothèques aux éléments disparates qui correspondent aux modes et technologies de leurs époques respectives.

Etant donné le récent regain de dynamisme chez les ingénieurs d'Apple, on se dit qu'un jour pas si lointain des API modernes inspirées des efforts produits pour iOS remplaceront ces frameworks OS X de plus en plus dépréciés… ce sera l'occasion de faire de nouveau tutoriels. :)

Auteur: Eric Dejonckheere