Partager via



Juillet 2015

Volume 30, numéro 7

Windows avec C++ - composants d'exécution de Windows

Par Kenny Kerr | Juillet 2015

Kenny KerrAu cours des prochains mois, je vais à la découverte de l'essentiel de l'exécution de Windows. Le but est de briser les abstractions de niveau supérieur que les développeurs utilisent dans les différentes projections de langage et outils afin d'examiner comment le Runtime Windows fonctionne à l'interface binaire d'application (ABI) — la limite entre les applications et les composants binaires, elles s'appuient sur les services d'accès OS.

À certains égards, le Runtime Windows est simplement l'évolution de la COM, qui était en fait une norme binaire pour la réutilisation du code et continue d'être une manière populaire de construire des applications complexes et composants de système d'exploitation. Cependant, contrairement à COM, le Runtime Windows se concentre plus étroitement et est principalement utilisé comme fondement pour l'API Windows. Les développeurs d'applications seront plus enclins à utiliser le Runtime Windows en tant que consommateur de composants de système d'exploitation et moins enclins à écrire des composants eux-mêmes. Néanmoins, une bonne compréhension de comment toutes les abstractions chics différentes sont mis en œuvre et projetés dans différents langages de programmation peut seulement vous aider à écrire des applications plus efficaces et mieux diagnostiquer les problèmes d'interopérabilité et de performance.

Une des raisons pourquoi si peu de développeurs comprennent comment le Runtime Windows fonctionne (autre que sa documentation plutôt clairsemée) est parce que les projections de l'outillage et la langue vraiment occulter la plate-forme sous-jacente. Cela peut être naturel pour les développeurs c#, mais il certainement n'est pas rendre la vie confortable pour le développeur C++ qui veut vraiment savoir ce qui se passe en coulisses. Nous allons donc commencer en écrivant un simple composant Runtime Windows avec C++ Standard à l'aide de l'invite de commandes de developer Visual Studio 2015.

Je vais commencer par une DLL simple et traditionnelle qui exporte un couple de fonctions. Si vous voulez continuer, créez un dossier de l'échantillon et à l'intérieur qui créer quelques fichiers de source commençant à Sample.cpp :

C:\Sample>notepad Sample.cpp

La première chose que je vais faire, c'est prendre soin de déchargement de la DLL, que j'appellerai le composant d'ici sur. Le composant doit soutenir le déchargement des requêtes via un appel de fonction exportée, DllCanUnloadNow, et c'est l'application qui contrôle le déchargement à la CoFreeUnused­fonction de bibliothèques. Je ne passent beaucoup de temps là-dessus parce que c'est la façon même composants ont été déchargés dans classique COM. Parce que le composant n'est pas lié de manière statique dans l'application — avec un fichier LIB, par exemple —, mais est plutôt chargé dynamiquement via la charge­fonction de la bibliothèque, il faut avoir un moyen pour le composant pour être déchargé par la suite. Seule la composante ne sait vraiment combien de références en circulation sont détenus afin que le runtime COM puisse appeler sa fonction DllCanUnloadNow pour déterminer s'il est sécuritaire de le décharger. Les applications peuvent effectuer également ce ménage eux-mêmes en utilisant les fonctions CoFreeUnusedLibraries ou CoFreeUnusedLibrariesEx. L'implémentation du composant est simple. J'ai besoin d'un verrou qui garder trace de combien d'objets est vivants :

static long s_lock;

Chaque objet peut alors simplement incrémenter ce verrou dans son constructeur et il decrement dans son destructeur. Pour que rester simple, je vais écrire une petite classe ComponentLock :

struct ComponentLock
{
  ComponentLock() noexcept
  {
    InterlockedIncrement(&s_lock);
  }
  ~ComponentLock() noexcept
  {
    InterlockedDecrement(&s_lock);
  }
};

Tous les objets qui devraient empêcher le composant de déchargement peuvent ensuite simplement incorporer un ComponentLock comme une variable de membre. La fonction DllCanUnloadNow peut maintenant être mis en oeuvre tout simplement :

HRESULT __stdcall DllCanUnloadNow()
{
  return s_lock ? S_FALSE : S_OK;
}

Il y a vraiment deux types d'objets, vous pouvez créer au sein d'un composant — des usines d'activation, qui furent appelés usines de classe COM classique, et l'instance réelle d'une classe particulière. Je vais d'implémenter une classe simple de « Poule » et je vais commencer en définissant une interface inconnue donc la poule peut cot :

struct __declspec(uuid("28a414b9-7553-433f-aae6-a072afe5cebd")) __declspec(novtable)
IHen : IInspectable
{
  virtual HRESULT __stdcall Cluck() = 0;
};

Il s'agit d'une interface COM régulière qui arrive juste à dériver de IInspectable plutôt que directement à partir de IUnknown. Je peux ensuite utiliser le modèle de classe Implements j'ai décrit dans le numéro de décembre 2014 (msdn.com/magazine/dn879357) pour implémenter cette interface et fournir l'implémentation réelle de la classe de poule au sein de la composante :

struct Hen : Implements<IHen>
{
  ComponentLock m_lock;
  virtual HRESULT __stdcall Cluck() noexcept override
  {
    return S_OK;
  }
};

Une usine d'activation est juste une classe C++ qui implémente l'interface IActivationFactory. Cette interface de IActivationFactory fournit la méthode unique de ActivateInstance, qui est analogue à l'interface IClassFactory COM classique et sa méthode de CreateInstance. L'interface COM classique est en fait légèrement supérieur car il permet à l'appelant de demander une interface spécifique directement, alors que le IActivationFactory de Runtime Windows retourne simplement un pointeur d'interface IInspectable. L'application est ensuite chargée d'appeler la méthode IUnknown QueryInterface pour récupérer une interface plus utile à l'objet. En tout cas, il rend l'Activate­Instance de méthode assez simple à mettre en œuvre :

struct HenFactory : Implements<IActivationFactory>
{
  ComponentLock m_lock;
  virtual HRESULT __stdcall ActivateInstance(IInspectable ** instance)
    noexcept override
  {
    *instance = new (std::nothrow) Hen;
    return *instance ? S_OK : E_OUTOFMEMORY;
  }
};

Le composant permet aux applications de récupérer une usine d'activation spécifique en exportant une autre fonction, appelée DllGetActivation­usine. Ceci, encore une fois, est analogue à la fonction DllGetClassObject exportée qui prend en charge le modèle d'activation COM. La principale différence est que la classe désirée est spécifiée avec une chaîne au lieu d'un GUID :

HRESULT __stdcall DllGetActivationFactory(HSTRING classId,
   IActivationFactory ** factory) noexcept
{
}

Un HSTRING est un handle qui représente une valeur de chaîne immuable. C'est l'identificateur de classe, peut-être « Sample.Hen » et indique quelle usine d'activation doit être retourné. À ce stade, il y a plusieurs raisons pourquoi les appels vers DllGetActivationFactory peuvent échouer, alors je vais commencer par effacer la variable d'usine avec un nullptr :

*factory = nullptr;

Maintenant, j'ai besoin d'obtenir le tampon de soutien pour l'identificateur de classe HSTRING :

wchar_t const * const expected = WindowsGetStringRawBuffer(classId, nullptr);

Je peux ensuite comparer cette valeur avec toutes les classes que mon composant arrive à mettre en œuvre. Jusqu'il y a une :

if (0 == wcscmp(expected, L"Sample.Hen"))
{
  *factory = new (std::nothrow) HenFactory;
  return *factory ? S_OK : E_OUTOFMEMORY;
}

Sinon, je vais retourner un HRESULT qui indique que la classe demandée n'est pas disponible :

return CLASS_E_CLASSNOTAVAILABLE;

C'est tout le C++, j'ai besoin de ce composant simple place et faire fonctionner, mais il n'y a encore un peu plus de travail à faire afin de réellement faire une DLL pour ce composant et puis le décrire ces satanés compilateurs c# qui ne savent pas comment analyser les fichiers d'en-tête. Pour faire une DLL, j'ai besoin d'impliquer l'éditeur de liens, plus précisément sa capacité à définir les fonctions exportées de la DLL. Je pourrais utiliser le spécificateur de __declspec dllexport spécifique au compilateur Microsoft, mais c'est l'un des rares cas où je préfère parler directement à l'éditeur de liens et plutôt fournir un fichier de définition de module avec la liste des exportations. Je trouve cette approche moins erreur sujettes. Donc, c'est retour à la console pour le deuxième fichier source :

C:\Sample>notepad Sample.def

Ce fichier DEF doit simplement une section appelée exportations qui répertorie les fonctions à exporter :

EXPORTS
DllCanUnloadNow         PRIVATE
DllGetActivationFactory PRIVATE

Je peux vous donner maintenant le fichier source C++ avec ce module -­fichier de définition pour le compilateur et l'éditeur de liens pour produire la DLL et ensuite utiliser un fichier de commandes simple commodité pour générer le composant et placer tous les objets de génération dans un sous-dossier :

C:\Sample>type Build.bat
@md Build 2>nul
cl Sample.cpp /nologo /W4 /FoBuild\ /FeBuild\Sample.dll /link /dll /def:Sample.def

Je vais passer outre la magie profonde qui est le langage de script batch file et se concentrer sur les options du compilateur Visual C++. L'option /nologo supprime l'affichage de la bannière de copyright. L'option est également transmise à l'éditeur de liens. L'option/W4 indispensable indique au compilateur d'afficher des avertissements de plus pour les bugs de codage courantes. Il n'y a pas d'option /FoBuild. Le compilateur a cette convention difficile à lire par lequel les chemins de sortie suivent l'option, en l'occurrence /Fo, sans espace entre les deux. En tout cas, l'option /Fo est utilisée pour forcer le compilateur pour vider le fichier de l'objet dans le sous-dossier de Build. C'est la seule sortie de la génération qui n'est pas par défaut dans le dossier que le fichier exécutable défini avec l'option /Fe sortie. L'option /link indique au compilateur qu'arguments suivants doivent être interprétés par le linker. Cela évite d'avoir à appeler l'éditeur de liens comme une étape secondaire et, à la différence du compilateur, les options de l'éditeur de liens sont insensibles à la casse et emploient un séparateur entre le nom d'une option et n'importe quelle valeur, comme c'est le cas avec l'option /def qui indique le fichier de définition de module à utiliser.

Je peux maintenant construire mon composant tout simplement et le sous-dossier Build résultant contient un certain nombre de fichiers, seul des quelles questions. Naturellement, c'est le Sample.dll exécutable qui peut être chargé dans l'espace d'adressage de l'application. Mais cela ne suffit pas. Un développeur d'applications a besoin d'un moyen de savoir ce que contient le composant. Un développeur C++ ne serait sans doute satisfait avec un fichier d'en-tête dont l'interface inconnue, mais même cela n'est pas particulièrement pratique. Le Runtime Windows inclut le concept de projections de langue par laquelle un composant est décrit de manière que différentes langues peuvent découvrir et ses types de projets dans leurs modèles de programmation. J'explorerai projection langue dans les prochains mois, mais pour l'instant nous allons juste obtenir cet exemple pour travailler à partir d'une application c# parce que c'est le plus convaincant. Comme je le disais, les compilateurs c# ne sais pas comment analyser les fichiers d'en-tête C++, donc j'ai besoin de fournir des métadonnées avec laquelle le compilateur c# se fera un plaisir. J'ai besoin de produire un fichier WINMD qui contient les métadonnées CLR décrivant mon composant. Ce n'est aucun chose facile parce que les types natifs que je pourrais utiliser pour ABI le composant de le peuvent souvent être très différentes lorsqu'elles sont projetées en c#. Heureusement, le compilateur IDL Microsoft a été réaffecté pour produire un fichier WINMD, donné un fichier IDL qui utilise quelques nouveaux mots clés. Donc, c'est retour à la console pour notre troisième fichier source :

C:\Sample>notepad Sample.idl

Tout d'abord, j'ai besoin d'importer la définition de l'interface IInspectable Prérequis :

import "inspectable.idl";

Je peux ensuite définir un espace de noms de types du composant. Cela doit correspondre au nom du composant lui-même :

namespace Sample
{
}

Maintenant, j'ai besoin définir l'interface inconnue, que j'ai défini précédemment en C++, mais cette fois comme une interface IDL :

[version(1)]
[uuid(28a414b9-7553-433f-aae6-a072afe5cebd)]
interface IHen : IInspectable
{
  HRESULT Cluck();
}

Il s'agit de bon vieux IDL et si vous avez utilisé des IDL dans le passé pour définir des composants COM, vous ne devriez pas être surpris par tout cela. Tous les types de Windows Runtime doivent, toutefois, définir un attribut de version. Cette habitude d'être en option. Toutes les interfaces doivent également dériver de IInspectable directement. Il n'y a effectivement aucun héritage de l'interface dans l'exécution de Windows. Cela a des conséquences négatives que je vais parler dans les prochains mois.

Et, enfin, j'ai besoin de définir la classe poule elle-même en utilisant le mot clé new de runtimeclass :

[version(1)]
[activatable(1)]
runtimeclass Hen
{
  [default] interface IHen;
}

Encore une fois, l'attribut version est requis. L'attribut activable, bien que non nécessaire, indique que cette classe peut être activée. Dans ce cas, il indique que l'activation par défaut est supportée via la méthode IActivationFactory ActivateInstance. Une projection de langue qui devrait présenter comme un constructeur par défaut C++ ou c# ou tout ce qui est logique pour une langue particulière. Enfin, l'attribut par défaut avant le mot clé interface indique que l'inconnue est l'interface par défaut pour la classe de poule. L'interface par défaut est l'interface qui prend la place des paramètres et types de retour, lorsque ces types de spécifieraient la classe elle-même. Parce que l'ABI métiers uniquement dans la classe de poule et les interfaces COM, c'est pas en soi une interface, l'interface par défaut est son représentant au niveau de l'ABI.

Il y a beaucoup plus à découvrir ici, mais cela va faire pour le moment. Je peux maintenant mettre à jour mon fichier de commandes pour générer un fichier WINMD qui décrit mon composant :

@md Build 2>nul
cl Sample.cpp /nologo /W4 /FoBuild\ /FeBuild\Sample.dll /link /dll /def:Sample.def
"C:\Program Files (x86)\Windows Kits\10\bin\x86\midl.exe" /nologo /winrt /out %~dp0Build /metadata_dir "c:\Program Files (x86)\Windows Kits\10\References\Windows.Foundation.FoundationContract\1.0.0.0" Sample.idl

Je vais encore occulter la magie dans le fichier de commandes et se concentrer sur ce qui est nouveau avec les options du compilateur MIDL. L'option /winrt est la clé et indique que le fichier IDL contient Windows Runtime types plutôt que les définitions d'interface COM ou style RPC traditionnelles. L'option/out juste fait en sorte que le fichier WINMD se trouve dans le même dossier que la DLL comme cela est requis par les outils de compilation c#. L'option /metadata_dir indique au compilateur où il peut trouver les métadonnées qui a été utilisée pour construire le système d'exploitation. J'écris ceci, le Kit de développement logiciel (SDK) Windows pour Windows 10 est encore s'installer et j'ai besoin d'être prudent appeler le compilateur MIDL fourni avec le Kit de développement logiciel (SDK) Windows et pas celui fourni par le chemin d'accès dans l'invite de commande des outils Visual Studio .

Exécutez que le fichier batch produit maintenant Sample.dll tant la Sample.winmd, que je peux ensuite référencer à partir d'une application c# Windows universelle et utiliser la classe de poule comme si c'était juste un autre projet de bibliothèque CLR :

Sample.Hen h = new Sample.Hen();
h.Cluck();

Le Runtime de Windows est construit sur les fondations de COM et Standard C++. Concessions ont été faites pour soutenir le CLR et le rendent très facile pour les développeurs c# d'utiliser les nouvelles API de Windows sans avoir besoin des composants d'interopérabilité. Le Runtime de Windows est l'avenir de l'API Windows.

Plus précisément, j'ai présenté le développement d'un composant de Windows Runtime dans la perspective de COM classique et ses racines dans le compilateur C++ afin que vous puissiez comprendre d'où vient cette technologie. Toutefois, cette approche devient vite peu pratique. Le compilateur MIDL fournit en fait beaucoup plus que simplement le fichier WINMD et nous pouvons réellement l'utiliser, entre autres, pour générer la version canonique de l'interface inconnue en C++. J'espère que vous allez me rejoindre le mois prochain que nous explorons un flux de travail plus fiable pour la création de composants d'exécution de Windows et également résoudre quelques problèmes d'interopérabilité le long du chemin.


Kenny Kerr est un programmeur informatique basé au Canada, mais aussi un auteur pour Pluralsight et MVP Microsoft. Il blogs à kennykerr.ca et vous pouvez le suivre sur Twitter à twitter.com/kennykerr.

Grâce à l'expert technique Microsoft suivant d'avoir relu cet article : Larry Osterman