C++/CLI en 10 min, partie 2: Caractéristiques générales

Le langage C++/CLI est un langage qui permet de manipuler à la fois des objets managés et non managés. Cette caractéristique rend ce langage plus complexe à implémenter car il rend la plupart des mécanismes d’interoperabilité invisibles entre le code managé et le code non managé. Le fait d’ignorer ces mécanismes peut mener à des exécutions moins performantes. Cette partie liste la plupart de ces mécanismes.

Mise en oeuvre pour coder en C++/CLI

Outils nécessaires pour coder en C++/CLI

Pour implémenter en C++/CLI, il faut une version de Visual Studio avec les options d’installation du C++, par exemple Visual Studio Community 2015: https://www.visualstudio.com/.

Chaque version de Visual Studio fournit une version du compilateur C++ et du runtime C/C++. Les versions du runtime et du compilateur sont les mêmes que celles de Visual Studio. Par exemple, Visual Studio 2015 permet de produire des assemblies qui seront compatibles avec le runtime C/C++ VS140. Ce runtime est fournit avec Microsoft Visual C++ 2015 Redistributable.

Comment exécuter une assembly C++/CLI ?

Toutes les assemblies codées en C++/CLI ne nécessitent pas forcément un runtime C/C++ (voir détails des options de compilation). Toutefois, quand on souhaite déployer une assembly codée en C++/CLI qui nécessite un runtime C/C++, il faut au moins que la version du “redistributable C++” correspondant à la version du runtime soit installée sur le poste client.

Dans le cas où le “redistributable C++” n’est pas installé ou si la bonne version n’est pas installée, à l’exécution on aura une erreur du type:

"The program can't start because MSVCRXXX.dll is missing from your computer. 
Try reinstalling the program to fix the problem"

Dans le nom de la DLL MSVCRXXX.dll, XXX étant la version du runtime:

  • 100 pour Visual C++ 2010
  • 110 pour Visual C++ 2012
  • 120 pour Visual C++ 2013
  • 140 pour Visual C++ 2015 etc…

Comment vérifier si le redistributable C++ est installé ?

Pour vérifier que le “redistributable C++” est installée, il faut vérifier la présence des clés de registre suivante:

Runtime Version Architecture Clé de registre
Visual C++ 2010 Redistributable 10.0.40219.325 x86 HKLM\SOFTWARE\Classes\Installer\Products\1D5E3C0FEDA1E123187686FED06E995
x64 HKLM\SOFTWARE\Classes\Installer\Products\1926E8D15D0BCE53481466615F760A7F
Visual C++ 2012 Redistributable 11.0.61030.0 x86 HKLM\SOFTWARE\Classes\Installer\Dependencies\{33d1fd90-4274-48a1-9bc1-97e33d9c2d6f}
x64 HKLM\SOFTWARE\Classes\Installer\Dependencies\{ca67548a-5ebe-413a-b50c-4b9ceb6d66c6}
Visual C++ 2013 Redistributable 12.0.30501.0 x86 HKLM\SOFTWARE\Classes\Installer\Dependencies\{f65db027-aff3-4070-886a-0d87064aabb1}
x64 HKLM\SOFTWARE\Classes\Installer\Dependencies\{050d4fc8-5d48-4b8f-8972-47c82c46020f}
Visual C++ 2015 Redistributable 14.0.24212.0 x86 HKLM\SOFTWARE\Classes\Installer\Dependencies\{462f63a8-6347-4894-a1b3-dbfe3a4c981d}
x64 HKLM\SOFTWARE\Classes\Installer\Dependencies\{323dad84-0974-4d90-a1c1-e006c7fdbb7d}

Options de compilation /clr

Dans un projet C++, pour implémenter en C++/CLI, il faut ajouter une option de compilation /clr. Cette option posséde quelques variantes:

  • /clr: cette option permet de produire une assembly mixte contenant du code C++ natif et du code managé. L’assembly produite contient des metadatas qui lui permettent d’être consommée par une autre assembly managée.
  • /clr:pure: l’assembly produite est très proche d’une assembly C# car elle ne contient que du code managé sous forme de code MSIL. Une assembly de ce type ne contient pas de code natif.
  • /clr:safe: de même que /clr:pure, l’assembly produite ne contient pas de code natif mais seulement du code MSIL. L’assembly produite est vérifiable c’est-à-dire que le CLR peut vérifier à l’exécution qu’elle ne viole aucun critères de sécurité comme pour une assembly du CLR. A la différence des assemblies /clr:pure, une assembly de ce type est compatible avec tes les autres types d’assembly C++/CLI. Le gros intérêt des assemblies de ce type est qu’elle ne dépendent pas du tout du CRT (C/C++ Runtime).
  • Sans option /clr: pas d’utilisation du CLR, la DLL produite ne contient que du code natif et est exécutée par le CRT (C/C++ Runtime).

Détails des options de compilation

Toutes ces options ne sont pas forcément compatibles entre elles.

/clr:safe:

  • Les assemblies de ce type sont compatibles avec toutes les autres, c’est-à-dire qu’elles peuvent consommer tous les autres types d’assembly ou de DLL.
  • Les assemblies /clr:safe ne dépendent pas du runtime C/C++ mais seulement du CLR.
  • Du code C++ natif avec cette option de compilation ne peut être compilé dans une assembly managée et inversement le code source ne peut utiliser du code natif.
  • Cette option permet une compilation statique (option /MT ou /MTd).

/clr:

  • Les assemblies de ce type sont compatibles avec tous les autres types d’assembly à l’exception des assemblies /clr:pure.
  • C’est l’option la plus intéressante pour contenir du code mixte (code managé et code non managé) car le code C++ peut être compilé dans une assembly managée et du code managé peut utiliser des types natifs.
  • Certains types exposés dans une assembly de ce type peuvent être consommés par du code natif.
  • Cette option ne permet pas le chargement d’assembly dynamique avec System::Reflection::Assembly::Load() ou System::Reflection::Assembly::LoadFrom().
  • Cette option est incompatible avec la compilation statique (option /MT ou /MTd), les assemblies de ce type ne peuvent consommer que des assemblies managées ou des DLL dépendant seulement du CRT.
  • Les assemblies de ce type dépendent du CLR et des DLL du runtime C/C++.

/clr:pure:

  • Les assemblies de ce type sont compatibles seulement avec d’autres assembles /clr:pure et /clr:safe.
  • Les types sont chargés à la demande comme du code managé .NET classique.
  • Du code C++ natif peut être compilé dans une assembly managée et le code source peut utiliser des types natifs.
  • Les assemblies de ce type dépendent du CLR et des DLL du runtime C/C++.
  • Cette option est incompatible avec la compilation statique (option /MT ou /MTd).

Détails des options d’un projet C++/CLI

Pour créer un projet C++/CLI, il faut préciser quelques paramètres dans les propriétés du projet.

Runtime Library

Dans “Configuration Properties” -> “C/C++”: il faut choisir une option compatible avec l’option /clr choisie. La compilation statique (c’est-à-dire les options /MT et /MTd) sont compatibles seulement avec /clr:safe. Avec les options /clr et /clr:pure, on ne peut utiliser que les options /MD et /MDd pour produire une DLL dépendante du CRT.

Common Language Runtime Support

C’est l’option la plus importante dans “Configuration Properties” -> “General”:

  • “No Common Language Runtime Support”: cette option correspond à l’absence d’option /clr, elle permet de produire une DLL C++ native.
  • “Common Language Runtime Support (/clr)”: permet de produire une assembly /clr.
  • “Pure MSIL Common Language Runtime Support (/clr:pure)”: permet de produire une assembly /clr:pure.
  • “Safe MSIL Common Language Runtime Support (/clr:safe)”: permet de produire une assembly /clr:safe.

2e fichier d’en-têtes précompilés

Pour compiler un fichier d’en-tête précompilé avec du code managé, il faut utiliser une 2e fichier en plus du fichier “stdafx.pch” compilé sans options /clr. Il faut appeler ce fichier “stdafx_clr.cpp” et ajouter dans ce fichier:
#include “stdafx.h”

D’autres options dans les propriétés du projet sont nécessaires:
Dans C/C++ -> Precompiled Headers:

  • “Create/Use precompiled headers”: affecter la valeur “Create precompiled header /Yc”
  • “Precompiled header file”: affecter “$(IntDir)\$(TargetName)_clr.pch”
  • “Create/Use PCH Through file”: affecter “stdafx.h”

Dans C/C++ -> General:
“Compile with CLR Support”: affecter “Common Language Runtime Support /clr”.

Dans C/C++ -> Code Generation:

  • “Basic Runtime Checks”: affecter “Default”
  • “Enable Minimal Rebuild”: affecter “No”.
  • “Enable C++ Exceptions”: affecter “Yes with SHE exceptions /EHa”.

Organisation des fichiers d’un projet

Comme en C++, la déclaration et la définition des objets sont réparties dans 2 types de fichier:
La déclaration des objets se trouvent dans des fichiers “.h”.
La définition des objets se trouvent dans des fichiers “.cpp”.

Il peut y avoir plusieurs objets dans un même groupe de fichiers toutefois, pour rendre le code plus clair on range un seul objet classe ou structure dans une pair de fichiers “.h” et “.cpp”.

Par exemple, une classe déclarée dans le fichier “.h”:

class NativePoint  
{  
    public:  
      NativePoint(int x, int y)  
      {  
         innerX = x;  
         innerY = y;  
      } 
  
    private:  
      int innerX, innerY;  
};

C’est le fichier “.h” de classe ou de la structure qui doit contenir les directives #include vers les autres fichiers “.h” où se trouvent les déclarations d’objets utilisés par cette classe ou cette structure.

Le fichier “.cpp” contient le définition des membres de l’objet, par exemple:

#include "NativePoint.h" 
 
NativePoint::NativePoint(int x, int y)  
{  
 innerX = x;  
 innerY = y;  
} 
 
bool NativePoint::IsEqual(int x, int y)  
{  
   return this->innerX == x && this->innerY == y;  
}

Il ne faut pas oublier le nom de la classe devant le nom de la méthode.

Transitions entre des objets managés et des objets non managés

On pourrait penser que les appels managés-non managés sont anodins car ils sont faciles à effectuer en C++/CLI. Pourtant même si la syntaxe est parfois simple, le code généré effectue des appels managés-non managés qui peuvent être couteux en performance.

Thunks

Ainsi, en fonction de ce qui est implémenté le compilateur et le linker génère des metadatas qui contiennent des informations d’interopérabilité dans l’assembly générée. A l’exécution, le CLR lira ses informations et les utilisera pour générer des “thunks” quand c’est nécessaire. Dans un cadre différent, le compilateur peut produire aussi directement des “thunks” qui seront chargés directement au démarrage.

Ainsi:

  • Pour les appels de code managé vers du code non managé: les “thunks” sont générés le plus souvent par le compilateur JIT à la demande.
  • Pour les appels de code non managé vers du code managé: les “thunks” sont générés et chargés au démarrage.

Les “thunks” sont routines générés automatiquement dans du code natif ou dans du code MSIL suivant les besoins et le type d’appels. Ils font souvent appels à plusieurs autres instructions et peuvent être la source de pertes en performances s’ils sont utilisés trop souvent durant l’exécution. D’une façon générale, il faut éviter au maximum les transitions entre objets managés et objets non managés de façon à réduire l’utilisation de ces “thunks”.

Certaines transitions managées-non managées ne sont pas évidentes, par exemple quand du code C++ natif est compilé dans une assembly managée, le code produit n’est pas managé mais il reste natif. Ainsi les appels qui sont effectués par ce code natif vers d’autres objets managés entraîneront des transitions managées-non managées.

Convention d’appels

L’utilisation de “thunk” est très dépendante des conventions d’appels utilisées:

  • __cdecl: convention d’appels généralement utilisé en C/C++ et qui peut être utilisé pour des fonctions membres d’une classe managée lorsqu’elle comporte des arguments non managés.
  • __stdcall: convention d’appels utilisée en C/C++ pouvant aussi être utilisée pour des fonctions membres d’une classe managée lorsqu’elle comporte des arguments non managés.
  • __thiscall: convention d’appels utilisée en C/C++ pour les appels de membres d’une classe. Elle peut aussi être utilisé pour des fonctions membres d’une classe managée et pour des fonctions membres non-statiques avec des arguments non managés.
  • __clrcall: convention d’appels utilisée pour des fonctions membres de classe managée en particulier lorsque les arguments des fonctions sont purement managés.

Ainsi si une fonction managée utilise une convention d’appels natives, elle peut éventuellement être appelée par des fonctions natives. Si c’est le cas, un “thunk” sera généré et utilisé dans le cas d’un appel par une fonction native. Si le compilateur ne constate pas d’appels par des fonctions natives, le “thunk” ne sera pas généré.

De même, si une classe native compilée en code managé et qu’elle utilise des conventions d’appels C/C++ comme __thiscall” ou __stdcall, même des appels provenant des codes natifs se produiront en utilisant des “thunks”.

Platform Invoke

Hormis le cas d’appels avec un pointeur de fonction, les appels de code managés vers du code non managés sont effectués en utilisant le mécanisme de Platform Invoke. Ce mécanisme est très semblable à celui qui existe pour des appels de code C# vers des fonctions dans des DLL natives. Le compilateur génére lui-même les metadatas pour effectuer les appels P/Invoke. Ce type d’appel est couteux en performance même si elles sont généralement meilleures que lorsqu’elles sont générés à la main par le dévelopeur.

Pointeur de fonction et fonction virtuelle

Lorsque du code managé appelle une fonction compilée dans du code natif en utilisant un pointeur de fonction, le CLR passe par l’intermédiaire d’un “thunk”. Ce “thunk” contient les arguments de la fonction et la convention d’appel et est généré directement par le compilateur sans faire appel à des instructions P/Invoke.
Le même procédé est utilisé dans le cas d’appels d’une fonction virtuelle codée dans une classe native par du code managé.

Enfin le cas le plus défavorable est le cas où des fonctions virtuelles d’une classe managée utilisent des conventions d’appels natives (par exemple __thiscall ou __stdcall), même si l’appel provient d’une fonction managée, il passera par l’intermédiaire de plusieurs “thunks” pour obtenir l’adresse de la fonction à exécuter.

Share on FacebookShare on Google+Tweet about this on TwitterShare on LinkedInEmail this to someonePrint this page

Leave a Reply