Mitsuru Furuta
Interopérabilité COM/.Net : extension du shell windows et appel à Messenger
Extension du shell windows pour ajouter au menu contextuel de l'explorateur de fichier un lien direct vers l'envoie de fichiers par Messenger
Par Mitsuru Furuta publié le 08/10/2004 à 17:29, lu 28156 fois, 1 pages
Présentation
Bonjour à tous, cet article a pour but de vous exposer les différentes techniques d'interopérabilité possibles entre le monde .Net et le monde COM. J'ai choisi de développer pour support de ce sujet une extension du shell de windows qui consiste à ajouter une entrée au menu contextuel de l'explorateur de fichier afin de permettre l'envoi facile d'un fichier à un contact en ligne de Windows Messenger.



Définissons dans un premier temps les différents éléments dont nous allons avoir besoin.
  1. Pour programmer notre extension du shell, il faut implémenter deux interfaces du système qui sont IShellExtInit et IContextMenu. Il nous faudra donc développer un serveur COM dont l'interface nous est imposée et signée par un GUID système. Il nous sera donc impossible d'utiliser le wrapper automatique que propose classiquement Visual Studio .Net via l'option «Register for COM interop ».
  2. Lors de l'implémentation de ces interfaces, nous utiliserons des structures de l'API Win32 afin d'étendre le menu contextuel. Nous serons donc obligés de convertir ces structures systèmes en structures .Net compatibles au niveau mémoire. Nous importerons également quelques méthodes de l'API Win32 relatives à la gestion des menus via des imports classiques de dll (DllImport).
  3. L'enregistrement d'un tel module se fait grâce à l'utilitaire regasm.exe. Le référencement de notre serveur auprès du shell de windows se fait entièrement dans la base de registre. Nous couplerons cette action avec l'enregistrement du serveur COM.
  4. Nous aurons ensuite besoin de faire appel à Messenger pour récupérer la liste des utilisateurs connectés puis pour lancer le transfert du fichier sélectionné. J'ai pris le choix de ne pas créer de session Messenger ni de lancer l'AutoSign mais d'utiliser la session ouverte en cours. Afin d'implémenter tout cela, nous appellerons via COM mais cette fois en tant que client, l'API MessengerAPI fournie par Windows Messenger 5. MSN Messenger expose également cette API mais uniquement si Windows Messenger 5 est installé. Si vous trouvez cela bizarre, je suis d'accord avec vous !
  5. Enfin, j'ai packagé le tout dans une projet d'installation qui vous le verrez présente quelques écueils.
  6. Nous aurons enfin besoin bien évidemment du framework .Net installé en version 1.1 mais cela, je pense que vous l'avez tous.
  7. Attention, que vous utilisiez Windows Messenger 5 ou Msn Messenger, pour que ce programme fonctionne, il faut que impérativement que Windows Messenger 5 soit installé.
Le projet
1. Signer notre assemblée

Afin d'enregistrer notre assemblée en tant que serveur COM, nous devons la signer tel que le framework .Net nous l'impose. L'utilisation de l'utilitaire sn.exe nous permet de créer un fichier contenant la paire de clés (privée et publique) nécessaire à la signature. Dans le fichier AssemblyInfo.cs nous référençons ce fichier via l'attribut « assembly: AssemblyKeyFile ».



2. Référencer l'API MessengerAPI : utilisation de TlbImp

Une des premières difficultés a été curieusement de référencer l'API de Messenger. L'utilisation de l'assistant de Visual Studio .Net depuis « Ajouter une référence » suffit normalement à faire ce référencement. Hors notre assemblée est signée et ne peut référencer que des assemblées signées. L'assistant compile automatique une assemblée qui wrappe le serveur COM de Messenger sans nous laisser le temps de la signer.

La solution est simple, nous devons utiliser manuellement l'utilitaire tlbimp.exe afin de lui spécifier l'option de signature. Nous ferons à nouveau appel à sn.exe pour générer le fichier de clés nécessaire.



3. Définitions des interfaces COM

Je ne vais pas m'attarder sur les spécifications de ces interfaces qui sont largement décrites sur le site msdn, cela dit afin de vous éviter de vous perdre dans l'océan de ce site, je vous joins les liens ci-dessous.

L'interface IContextMenu :

http://msdn.microsoft.com/library/default.asp?url=/library/en-us/shellcc/platform/shell/reference/ifaces/icontextmenu/icontextmenu.asp

L'interface IShellExtInit :

http://msdn.microsoft.com/library/default.asp?url=/library/en-us/shellcc/platform/shell/reference/ifaces/ishellextinit/ishellextinit.asp

Ces interfaces permettent globalement de :
  1. Initialiser le contexte
  2. Etre appelé lors de la création du menu contextuel de l'explorateur de fichiers afin de tester les fichiers sélectionnés et d'ajouter ses propres entrés de menu
  3. Etre appelé lors du lancement des menus personnalisés. Il faudra mettre en place un système qui permette de conserver les ids de menu afin de s'y retrouver entre le moment où l'on crée les menus et le moment où ils sont appelés
Afin de spécifier que ces interfaces sont bien fournies par un serveur COM externe à notre programme, nous allons utiliser un certain nombre d'attributs. Sans cela, le compilateur créerait une interface pure .Net qui bien qu'ayant la même définition que celle de windows, ne serait pas du tout identifiée comme identique.
  1. L'attribut « ComImport » permet de spécifier que ce type a déjà été défini dans un serveur COM. Au niveau mémoire le compilateur cherchera donc à le mapper et non à le redéfinir. Il faudra bien sûr que leurs méthodes et leurs paramètres soient compatibles à l'octet prêt avec la définition originale et respecte également le même alignement mémoire. Nous venons d'entrer dans la partie Interop pure de .Net.
  2. Un serveur COM peut exposer plusieurs types d'interfaces, Dual, IDispatch (permet l'invocation dynamique) ou IUnknown (accessible uniquement en redéfinissant le type localement). L'attribut « InterfaceType » nous permet de spécifier cette valeur. Il nous faut reprendre le type IUnknown telle que l'interface de windows le spécifie.
  3. Les serveurs COM sont enregistrés et identifiés sur le système grâce à un Global Unique Identifier (GUID). Les interfaces qu'ils exposent sont également enregistrées et identifiées par un mécanisme identique. L'attribut GuidAttribute nous permet de faire le lien absolu et unique avec cette interface ; il nous faut bien évidemment reprendre exactement la même valeur telle qu'elle est définie dans l'API de windows. On peut également retrouver cette valeur dans la base de registre.
[ComImport(), 
InterfaceType(ComInterfaceType.InterfaceIsIUnknown), 
GuidAttribute(000214e8-0000-0000-c000-000000000046)]
public interface IShellExtInit
{  }

[ComImport(), 
InterfaceType(ComInterfaceType.InterfaceIsIUnknown), 
GuidAttribute(000214e4-0000-0000-c000-000000000046)]
public interface IContextMenu
{  }
4. Les structures nécessaires

Nous aurons besoin de quelques fonctions classiques de l'API de windows pour manipuler les sous-menus à insérer. Pour cela, il nous faut reprendre les structures nécessaires en .Net. Il suffit juste de transformer quelques types et d'ajouter l'attribut StructLayout afin d'imposer l'ordre des champs en mémoire.

Afin de gagner de l'espace et du temps, les compilateurs modernes, par défaut, tentent de réorganiser et d'aligner les champs des structures de manière optimum (en général avec un alignement de la taille des registres du processeur – donc 32 bits quasiment pour tout le monde). Nous ne voulons pas de cette optimisation car nous voulons encore une fois une compatibilité mémoire avec les structures de l'API windows.

Pour plus de détails, sachez que le framework .Net vous fournit également un attribut FieldOffsetAttribute qui vous permet de gérer manuellement l'alignement mémoire de vos champs. Cela peut être très intéressant si vous relisez en .Net un fichier à accès direct que vous avez écrit en Pascal par exemple.
[StructLayout(LayoutKind.Sequential)]
public struct MENUITEMINFO
{
    public uint cbSize;
    public uint fMask;
    public uint fType;
    public uint fState;
    public int  wID;
    public int  hSubMenu;
    public int  hbmpChecked;
    public int  hbmpUnchecked;
    public int  dwItemData;
    public string dwTypeData;
    public uint cch;
    public int  hbmpItem;
}

[StructLayout(LayoutKind.Sequential)]
public struct MENUINFO 
{
    public uint cbSize;
    public uint fMask;
    public uint dwStyle;
    public uint cyMax;
    public int hbrBack;
    public uint dwContextHelpID;
    public IntPtr dwMenuData;
} 
5. Les fonctions de l'API Win32

Pour ce qui est de l'API windows, nous n'aurons besoin que de quelques fonctions afin de manipuler le fichier, les entrées de menu du popup ainsi que les images associées.
[DllImport("shell32")]
static public extern uint DragQueryFile(uint hDrop,uint iFile, StringBuilder buffer, int cch);

[DllImport("user32")]
static public extern int InsertMenuItem(uint hmenu, uint uposition, uint uflags, ref MENUITEMINFO mii);

[DllImport("user32")]
static public extern int CreateMenu();

[DllImport("user32")]
public static extern int CreatePopupMenu();

[DllImport("user32")]
public static extern int SetMenuItemBitmaps(uint hMenu, 
                                            int nPosition, 
                                            int wFlags,
                                            uint hBitmapUnchecked, 
                                            uint hBitmapChecked);
6. La classe Messenger Helper

Afin de faciliter l'utilisation de l'API de Messenger, nous isolerons le code qui lui est relatif dans une classe qui instancie et libère l'objet COM après utilisation. L'utilisation de l'API MessengerAPI est relativement simple mais un piège subsiste. Tout client d'un serveur COM doit appeler les fonctions _AddRef et _Release de l'interface IUnknown à chaque fois qu'une référence à cette interface est ajoutée ou supprimée.

Le wrapper .Net d'objets COM implémente bien évidemment cette fonctionnalité mais il subsiste un problème lors de la libération de l'objet. Dans des langages « classiques », il suffit en général de mettre la référence à « null » ou « Empty » en VB pour que le compilateur libère la référence. Hors en .Net nous ne manipulons pas directement le pointeur de l'interface puisque nous y accédons via une classe dite « proxy » générée par notre wrapper. Donc si nous lui affectons la valeur « null », c'est le proxy que nous libérons et non l'interface COM. Nous pouvons penser que lorsque la classe proxy est détruite par le garbage collector, la référence à l'objet COM le soit aussi, mais cette solution n'est pas satisfaisante car très peu maîtrisée, dû au fonctionnement indépendant du garbage collector.

La solution est apportée par une fonction statique ReleaseComObject de la classe Marshal.
Cette libération étant essentielle au bon fonctionnement de notre programme, j'ai décidé de l'implémenter dans la méthode Dispose de l'interface IDisposable. Ceci nous apporte le confort d'écriture et la sécurité du mot clé « using () {} »

7. L'implémentation des interfaces : le code est là !

Tout d'abord, Il faut faire de notre objet principal un objet COM ou en tout cas, il faut le déclarer comme étant à enregistrer dans le mécanisme COM de l'interopérabilité .Net.
Pour cela, il faut marquer notre classe de l'attribut « ComVisible(true) ».
Nous utiliserons également, l'attribut « Guid » afin d'associer à notre objet COM un Guid que nous allons générer. Je rappelle que Visual Studio .Net fournit dans le menu outils un générateur de Guids.
[Guid("458F56C9-ABF5-4b45-B7C4-9A0CCB11E34E"), ComVisible(true)]
public class SendFileToMessenger : IContextMenu, IShellExtInit
{  }
Par sécurité, nous pouvons appliquer également l'attribut « ComVisible(false) » à la l'assemblée entière dans le fichier AssemblyInfo.cs. Ceci à pour effet de ne pas exporter par défaut toutes les classes présentes dans notre module mais uniquement celles qui sont marquées par « ComVisible(true) ».
[assembly:ComVisible(false)]
Je vous laisse découvrir le code fonctionnel à travers le source. Les principales actions sont les suivantes :
  1. Dans QueryContextMenu, nous insérons une entrée de sous-menu au menu contextuelle dont le handler nous est fourni. A cette entrée de menu, nous insérons autant de sous-menus que d'utilisateurs messenger en lignes sont trouvés. Nous insérons également dans les entrées de menu, les bitmaps associés. Enfin, pour chaque entrée de menu, nous conservons dans une hashtable le lien entre sa position et l'utilisateur messenger afin de le retrouver ultérieurement.
  2. InvokeCommand est appelé lorsqu'une entrée du menu est validée. Nous devons donc, retrouver l'utilisateur Messenger associé à cette entrée de menu depuis la position du menu, et ce, grâce à notre hashtable définie ci-dessus.
8. Associer l'inscription dans la base de registre à l'enregistrement du serveur

Tout d'abord, comment fonctionne l'enregistrement d'un serveur COM écrit en .NET ? L'astuce est assez simple. Votre assemblée .Net bien qu'exportant des interfaces COM n'est tout simplement pas un serveur COM. Impossible donc de l'enregistrer via le fameux « regsvr32.exe ». L'astuce apportée par le framework est de fournir un serveur COM unique pour toutes les interopérabilités nécessaires entre le monde .Net et le monde COM. Ce serveur est « mscoree.dll ». C'est lui qui fera office de passerelle. Le Guid du serveur COM reste celui défini dans l'assemblée .Net mais la dll ciblée est bien « mscoree.dll » comme nous le montre la copie d'écran ci-dessous. C'est donc bien lui qui sera physiquement lancé mais il ira chercher l'implémentation des interfaces dans l'assemblée spécifiée dans la clé « CodeBase ».

La passerelle est ainsi établie. Afin de faciliter cet enregistrement, le framework .Net fournit un utilitaire du nom de « regasm.exe ».



Il est possible de brancher des actions personnalisées au moment de l'enregistrement et/ou du désenregistrement de notre assemblée via « regasm.exe ». Il suffit pour cela de définir des méthodes statiques marquées respectivement des attributs « ComRegisterFunctionAttribute » et « ComUnregisterFunctionAttribute ».
[System.Runtime.InteropServices.ComRegisterFunctionAttribute()]
public static void RegisterServer(Type t) 
{  }
[System.Runtime.InteropServices.ComUnregisterFunctionAttribute()]
public static void UnregisterServer(Type t) 
{  }
8. Gestion des images

Je ne m'étends pas sur le sujet. Il suffit d'ajouter l'image au projet et de la compiler en tant que ressource. On la charge ensuite via les fonctions managées de l'espace de nom « System.Resources »



Le projet d'installation
Je tenais à partager un dernier point avec vous sur le projet d'installation car j'ai rencontré une difficulté qui risque d'être courante si vous voulez enregistrer votre propre objet COM écrit en .Net lors de la phase d'installation.

Les projets d'installation de Visual Studio .Net permettent d'inscrire un objet COM mais uniquement à travers « regsrv32.exe » hors si vous avez suivi l'article jusqu'ici, vous savez que les objets COM écrits en .Net s'enregistrent via « regasm.exe ». Le seul moyen que j'ai trouvé a donc été d'ajouter « regasm.exe » au projet d'installation puis de faire une action personnalisée afin de le lancer en passant en paramètre l'assemblée de notre projet. Je n'ai pas trouvé de solution plus esthétique avec cette version de Visual Studio mais en tout cas, elle fonctionne.

Conclusion
Nous avons donc pu voir principalement deux choses : comment implémenter une interface COM déjà existante et comment accéder via COM également à l'API de Messenger. Même si la technologie .Net se prête naturellement moins à ce genre d'exercice, nous avons vu que cela reste tout à fait faisable via les fonctionnalités d'interopérabilité du framework .Net. Là encore l'utilisation d'attributs masque pas mal de difficultés et permet à notre code de conserver la lisibilité que nous connaissons au C#.

Un dernier mot pour dire que c'est mon premier article sur le site Tech Head Brothers et que je remercie Laurent Kempé de son accueil.

A bientôt pour un nouvel article...
 
» Démarrer une discussion
 
Discussion démarée par panda31 le 07/05/2007 à 12:19, 2 commentaire(s).