Développer un composant WinRT en C++ : Part I

Un composant WinRT, c'est un composant développé en C++, C# ou VB.NET et qui peut être ré-utilisé par une application Windows Store développée en Javascript, en C++, en VB.NET ou en C#.

Pourquoi développer un composant WinRT en C++ ?

  1. Pour réutiliser de l'existant.
    Vous avez depuis des années un composant, qui rempli sont rôle, que vous avez éprouvé, testé, et approuvé, pourquoi réinventer la roue ?
  2. Pour palier à des manques
    La plate-forme WinRT a été conçue, pour être la plus légère qui soit. Il est donc possible qu'un certain nombre de "fonctionnalités" soient manquantes.
  3. Impossibilité technique, ou difficulté accrue dans un langage.
    Si vous souhaitez manipuler des APIs du système qui sont difficile, voir même impossible à utiliser dans un langage.
  4. Performance
    Désormais, développer une application Windows 8, c'est cibler des "appareils" qui n'ont pas forcement le même niveau de performances, que votre PC de développement.
    Pas de SSD, moins de RAM, un processeur différent que le core I7-3770 à 3.4 GHz, comme les processeurs ARM par exemple (même si les prochaines générations de Tegra et Snapdragon qui arrivent, vont relever la barre.)

Pour développer un Composant WinRT en C++, vous avez à votre disposition deux modèles.
Le modèle pur Windows Store, c'est à dire en utilisant l'extension C++/CX du langage C++ du compilateur, qui permet de manipuler les types WinRT de la manière la plus simple qui soit.

Pour en savoir plus sur le comment du pourquoi de C++/CX Inside the C++/CX Design

 

Ou le modèle Windows Runtime Library (WRL)

L’un ou l’autre des modèles  permettent de facilement écrire du code pour le Windows Runtime, mais avec WRL vous avez un accès plus bas niveau pour consommer des composants COM.

D’ailleurs WRL étant inspiré par ATL il sera plus facile d’interagir entre les deux. Si vous souhaitez en savoir plus sur ce dernier je vous encourage à aller voir Windows Runtime C++ Template Library (WRL) sur MSDN.

Si vous préférez ne pas manipuler C++/CX pour des raisons qui vous incombe, un Modèle Visual Studio de création d’un composant WRL est à votre disposition ici : https://visualstudiogallery.msdn.microsoft.com/346e6fbc-6508-43c8-af7f-9a922bb57128

 

Dans ces différents billets, pour illustrer le développement d'un composant WinRT en C++, j'ai utilisé pour ma part les extensions C++/CX, en conjonction avec une API Win32 pure et dure assez méconnue, Extensible Storage Engine (cf. article Wikipédia https://en.wikipedia.org/wiki/Extensible_Storage_Engine)

J’ai choisi cette API Win32 de base de données, au départ pour un projet interne et pouvoir créer un cache interne, qui bénéficie de toute l’infrastructure d’une base de données ISAM. Avec des indexes primaires, sur plusieurs colonnes, des champs multi-valués etc..

Bien évidement, j’aurais pu utiliser SQLite qui est plus aboutit, mais comme ESE est disponible sur toutes les plate-formes (ARM, X86 et X64) c’était l’occasion de démontrer la manière de développer un composant WinRT compatible sur toutes ces plate-formes sans le moindre effort.

 

Note : Le composant est une ébauche, et loin d’être aboutit à ce stade, tout n’est pas encore implémenté. Je l’utilise essentiellement pour de la consultation. L’API ESE étant très vaste, cela prendra sans doute un certain temps.

Pour en savoir plus sur les APIs ESE (Jet Blue)  (https://msdn.microsoft.com/en-US/library/windows/apps/br205753 )

 

Développer un composant WinRT, (non graphique je précise), impose un certain nombre de règles, que nous allons détailler tout au long de ces différents billets.

  • La frontière des langages (ABI)
  • Les types commun, le boxing
  • Les exceptions
  • Les performances (astuce) i.e. StringReference, ArrayReference
  • Asynchronisme
  • Levée d'évènement et synchronisation de thread
  • Ajouter de l’aide aux différentes APIs

 

Création d’un composant WinRT en C++/CX

  1. Choisir le projet Visual C++ | Windows Store | Windows Runtime Component

    Classe Activable

    namespace WindowsRuntimeComponent2
    {
         public ref class Class1 sealed
         {
         public:
             Class1();
         };
    }

    Le modèle crée une classe dite “Activable”:

    1. C’est à dire que les autres langages, peuvent l’activer par leur opérateur new (ou ref new pour C++/CX).

    2. Plus précisément, c’est une référence à une classe, préfixée par le mot clé ref class, qui indique au compilateur de créer toute la plomberie pour pouvoir l’activer.
      Ne pas oublier que le type ref class Class1, est une classe COM (Common Object Model) qui dérive de IInspectable, qui dérive elle même de IUnknown, mais que le compilateur nous cache.

    3. Pour que notre classe Class1, soit ”Activable” par d’autres langages, il faut un mécanisme neutre d’activation. Il faut passer par le modèle Fabrique de classe (Class Factory).
      Cela tombe bien, car le compilateur, crée pour nous une autre classe (en règle générale il la nomme du style __Class1ActivationFactory , qui implémente la méthode CreateFactory () pour créer la fabrique et qui dérive de l’interface IActivationFactory qui possède une méthode ActivateInstance(), ceci afin que le Windows Runtime  active notre classe.      

    4. Imaginons que nous instancions notre objet en C#
      WindowsRuntimeComponent1.Class1 c=new WindowsRuntimeComponent1.Class1

       

      1. Avant que le Constructeur de la classe soit appelé, Le CLR invoque l’API du système RoGetActivationFactory() , en lui passant entre autre  le nom de la classe “WindowsRuntimeComponent2.Class1”, et un pointeur sur une interface IActivationFactory

      2. Le runtime charge la DLL, et invoque sa méthode DllGetActivationFactory() qui elle même invoque la méthode Microsoft::WRL::Details::GetActivationFactory().

      3. GetActivationFactory(), retrouve entre autre un pointeur sur la méthode CreateFactory(), afin de créer la fabrique, c’est à dire obtenir une instance de la classe __Class1ActivationFactory.

      4. A partir de l’instance de la classe __Class1ActivationFactory (qui dérive de IActivationFactory) le Windows Runtime, invoque la méthode ActivateInstance() qui instancie cette fois-ci notre classe Class1.
        Le constructeur de Class1() est invoqué, et le compteur de référence est incrémenté de 1.

            
        Pour une 2ieme instance de la classe la méthode ActivateInstance() de l’interface IActivationFactory est appelée directement, qui incrémente également le compteur de référence.
        Si vous souhaitez à la compilation voir le code généré par le compilateur, vous pouvez ajoutez l’option : /d1ZWtokens 
        Ces deux options, imprime l’arbre des méthodes pour Class1 et __Class1ActivationFactory
        /d1reportSingleClassLayoutClass1
        /d1reportSingleClassLayout__Class1ActivationFactory
        Exemple pour la classe __Class1ActivationFactory, ou on observe bien qu’elle dérive de IActivationFactory

        class __Class1ActivationFactory    size(16):
        1>      +---
        1>      | +--- (base class IActivationFactory)
        1>      | | +--- (base class Object)
        1>   0    | | | {vfptr}
        1>      | | +---
        1>      | +---
        1>      | +--- (base class Object)
        1>   4    | | {vfptr}
        1>      | +---
        1>   8    | __abi_FTMWeakRefData __abi_reference_count
        1>      +---
        1> 
        1>  __Class1ActivationFactory::$vftable@IActivationFactory@:
        1>      | &__Class1ActivationFactory_meta
        1>      |  0
        1>   0    | &__Class1ActivationFactory::__abi_QueryInterface
        1>   1    | &__Class1ActivationFactory::__abi_AddRef
        1>   2    | &__Class1ActivationFactory::__abi_Release
        1>   3    | &__Class1ActivationFactory::__abi_GetIids
        1>   4    | &__Class1ActivationFactory::__abi_GetRuntimeClassName
        1>   5    | &__Class1ActivationFactory::__abi_GetTrustLevel
        1>   6    | &__Class1ActivationFactory::__abi_Platform_Details_IActivationFactory____abi_ActivateInstance
        1>   7    | &__Class1ActivationFactory::ActivateInstance
        1> 
        1>  __Class1ActivationFactory::$vftable@Object@:
        1>      | -4
        1>   0    | &thunk: this-=4; goto __Class1ActivationFactory::__abi_QueryInterface
        1>   1    | &thunk: this-=4; goto __Class1ActivationFactory::__abi_AddRef
        1>   2    | &thunk: this-=4; goto __Class1ActivationFactory::__abi_Release
        1>   3    | &thunk: this-=4; goto __Class1ActivationFactory::__abi_GetIids
        1>   4    | &thunk: this-=4; goto __Class1ActivationFactory::__abi_GetRuntimeClassName
        1>   5    | &thunk: this-=4; goto __Class1ActivationFactory::__abi_GetTrustLevel
        1> 
        1>  __Class1ActivationFactory::ActivateInstance this adjustor: 0
        1>  __Class1ActivationFactory::__abi_QueryInterface this adjustor: 0
        1>  __Class1ActivationFactory::__abi_AddRef this adjustor: 0
        1>  __Class1ActivationFactory::__abi_Release this adjustor: 0
        1>  __Class1ActivationFactory::__abi_GetIids this adjustor: 0
        1>  __Class1ActivationFactory::__abi_GetRuntimeClassName this adjustor: 0
        1>  __Class1ActivationFactory::__abi_GetTrustLevel this adjustor: 0
        1>  __Class1ActivationFactory::__abi_Platform_Details_IActivationFactory____abi_ActivateInstance this adjustor: 0
        1> 

        /d1reportAllClassLayout, imprime tous les arbres.

    1. La classe est également marquée sealed (scellée),  cela veut dire qu’il ne sera pas possible d’en hériter. Ce que l’on peut lire dans les différentes discutions, et ce qui me semble le plus pertinent, c’est pour éviter la tentation qu’un développeur en hérite dans un autre langage. En effet, à la différence de .NET ou l’on peut hériter d’une classe développée dans un langage, dans une classe développée avec un autre langage, il ne faut pas oublier que notre composant est développé en Natif donc avec une ABI différente de celle de .NET.

    2. Lors de la compilation, un fichier avec une extension .winmd est généré (Format CLI d’ailleurs, donc lisible avec un outil comme ILASM) et qui décrit sous forme de meta-données ce que sait faire le composant afin que les autres langages puissent y avoir accès. Le faite d’exposer à l’extérieur de son espace binaire (ABI) des informations, telles que les méthodes, les propriétés etc.. impose un certain nombre de règles que nous décrirons dans un prochain billet.

    3. Dernière petite chose, Si vous souhaitez changer l'espace de nom, il faut changer le nom du fichier winmd avec la même nomenclature que l’espace de nom.
      Par exemple si vous choisissez une espace de nom du type FRDPE.WINRT.ESE.
      namespace FRDPE { namespace WINRT { namespace ESE {   
      ….
      }}}

      Il faut changer le nom du fichier winmd en FRDPE.WINRT.ESE, sinon vous obtiendrez l’erreur suivante :

      error APPX1706: The .winmd file 'WindowsRuntimeComponent2.winmd' contains type 'FRDPE.WINRT.ESE.__IJetBlueEnginePublicNonVirtuals' outside its root namespace 'WindowsRuntimeComponent2'. Make sure that all public types appear under a common root namespace that matches the output file name.

      Pour ce faire, Affichez les propriétés du projet | Linker | Windows Metadata
      image

     

    Dans la prochaine partie, nous détaillerons au travers de notre exemple

    • La frontière des langages (ABI), c’est à dire ce que nous pouvons ou ne pouvons pas exposer aux autres langages.
    • Les types commun, le boxing, comment transtyper des types
    • Les exceptions

     

    Eric Vernié