Amélioration de “fixed” (C# 7)

Cet article fait partie d’une série d’articles sur les apports fonctionnels de C# 7 (i.e. C# 7.0/7.1/7.2/7.3).

Durant la phase de compactage du garbage collector (GC), des objets présent dans le tas managé peuvent être déplacés de façon à ne pas avoir un espace mémoire trop morcelé pour rendre les allocations plus rapides. Le déplacement des objets impliquent qu’ils changent d’adresses. Ces changements d’adresses sont gérés par le GC qui corrige les références ou les pointeurs vers ces objets.

Certains traitements peuvent nécessiter de travailler sur des adresses par l’intermédiaire de pointeurs, par exemple si on souhaite effectuer des appels à du code non managé en utilisant Platform Invoke ou si on souhaite effectuer des opérations sur les adresses des pointeurs. Si les adresses manipulées pointent vers des objets managés, elles peuvent être amenées à changer après une exécution du GC. Etant donné qu’il n’est pas facile de savoir de façon sure quand le GC sera exécuté, on ne peut pas prévoir quand les adresses manipulées sont corrompues à cause des changements d’adresse.

Avant C# 7.3

Pour éviter les changements d’adresse des objets manipulés, une possibilité est d’indiquer au GC qu’il ne doit pas modifier l’emplacement dans le tas managé des objets durant l’exécution d’une portion de code. Le mot clé fixed permet, par syntaxe, d’indiquer quelle est la portion du code dans laquelle le GC ne déplacera pas certains objets:

int[] items = new int[10]; // Allocation dans le tas managé 
fixed (int* pointer = items) 
{ 
  // Portion de code dans laquelle le tableau items ne sera pas déplacé par le GC. 
} 

Avant C# 7.3, il était possible d’utiliser fixed pour:

  • Des chaînes de caractères System.string:
    string simpleString = "Test"; // Allocation dans le managé car System.String est un objet de type référence. 
    fixed (char* pointer = &simpleString) 
    { 
      //... 
    } 
    

    Comme en C++, pour obtenir un pointeur à partir d’une référence, on peut utiliser &<nom référence>.

  • Des variables non managées ou
  • Des tableaux (exemple plus haut).

GCHandle

System.Runtime.InteropServices.GCHandle donne aussi la possibilité de travailler sur des pointeurs après avoir épinglé (i.e. pinned) l’objet pointé:

  • GCHandle.Alloc() permet d’instancier une référence GCHandle à partir d’un objet managé et de l’épingler pour que le GC ne le déplace pas pendant toute l’existence de la référence.
  • GCHandle.Free() permet de libérer la référence pour que le GC puisse de nouveau intervenir sur l’objet en mémoire si nécessaire.

Par exemple:

int[] items = new int[10]; 
GCHandle itemGcHandle = GCHandle.Alloc(items, GCHandleType.Pinned); // L'objet est désormais épinglé 

IntPtr pointer = itemGcHandle.ToIntPtr(); // On peut effectuer des traitements avec le pointeur 

ItemGcHandle.Free(); permet de libérer la référence et de ne plus épingler l'objet 

fixed permet de faciliter l’implémentation et vise à apporter une alternative à GCHandle.

Comment compiler du code unsafe ?

Pour compiler du code unsafe et autoriser le compilateur à utliser le mot-clé unsafe, il faut l’autoriser dans les propriétés du projet:

  • Dans les propriétés du projet dans Visual Studio, il faut cocher la propriété “Allow unsafe code” dans l’onglet Build.
  • En éditant directement le fichier .csproj, il faut rajouter le nœud AllowUnsafeBlocks dans PropertyGroup:
    <Project Sdk="Microsoft.NET.Sdk"> 
      <PropertyGroup> 
        <!—- ... -—> 
        <AllowUnsafeBlocks>true</AllowUnsafeBlocks> 
      </PropertyGroup> 
    </Project> 
    

Fixed pattern

C# 7.3

Avant C# 7.3, fixed ne pouvait être utilisé qu’avec des types d’objets bien précis. Pour étendre l’utilisation de fixed à d’autres types y compris des types déclarés en dehors du framework, C# 7.3 introduit le pattern fixed. Ce pattern permet à n’importe quel objet d’être utilisé avec fixed s’il contient une référence managée qui permettra:

  • D’épingler une instance de l’objet et
  • D’initialiser le pointeur utilisé par fixed pour épingler l’objet.

Lors de l’utilisation, de façon à fournir cette référence managée à fixed, l’objet devra comporter une fonction publique:

  • ref T GetPinnableReference() ou
  • ref readonly T GetPinnableReference()

T doit être le type d’une variable qui sera fixe en mémoire c’est-à-dire qu’elle ne sera pas affectée par l’exécution du garbage collector (GC). Les variables fixes en mémoire sont:

  • Des variables locales ou des paramètres de fonction de type valeur: les variables locales ou les paramètres de fonction de type valeur sont stockés sur la pile, ils ne sont pas affectés par le GC.

    Par exemple:

    int localVar = 5; 
    unsafe 
    { 
      int* varPtr = &localVar; // Opérateur address-of & (même signification qu’en C++) 
    }
    
  • Des variables résultants d’un buffer dans une structure (voir plus bas): ces variables sont fixes en mémoire par construction.

    Par exemple:

    unsafe struct StructWithFixedVar 
    { 
      public fixed int FixedVar[5]; 
    } 
    
    // ...
    var structInstance = new StructWithFixedVar(); 
    int* ptr = structInstance.FixedVar; 
    
  • Variable provenant d’indirection de pointeur *p, d’accès à un membre d’un pointeur p->m ou accès à un élément d’un pointeur p[i], par exemple:
    struct SimpleStruct 
    { 
      public int InnerVar; 
    } 
    
    // ...
    SimpleStruct simpleStruct = new SimpleStruct(); 
    unsafe 
    { 
      SimpleStruct* ptr = &simpleStruct; 
    
      // Pointer indirection 
      SimpleStruct ptrIndirection = *ptr; // Opérateur * (même signification qu’en C++) 
    
      // Pointer member access 
      int innerVar = ptr->InnerVar; // Opérateur -> 
    
      // Pointer element access 
      char* strPtr = stackalloc char[5]; // Allocation sur la pile 
      char elementPtr = strPtr[2]; // Opérateur [ ] 
    } 
    

Ainsi si la fonction GetPinnableReference() renvoie une référence d’une variable fixe en mémoire d’un objet, fixed sera capable d’épingler cet objet en mémoire. Par suite l’implémentation pourra être du type:

public class PinnableClass 
{ 
  public ref T GetPinnableReference() 
  { ... } 
} 

// ... 
var pinnableClass = new PinnableClass(); 
unsafe 
{ 
  fixed(T* ptr = pinnableClass) 
  { 
    // ... 
    // L’instance pinnableClass restera fixe en mémoire si le GC s’exécute 
  } 
} 
Manipuler des pointeurs en C#

Les notations utilisées pour manipuler les pointeurs en C# sont les mêmes qu’en C++ (pour plus de détails voir Aide-mémoire sur les pointeurs et références en C++):

  • Obtenir un pointeur à partir d’un objet de type valeur ou d’une référence avec l’opérateur &:
    struct StructObjet {} 
    
    // ...
    StructObject instance = new StructObject(); // Instanciation sur la pile 
    StructObject* ptr = &instance; // fixed n’est pas nécessaire 
    
  • Obtenir l’objet pointé ou une référence vers cet objet avec l’opérateur *:
    StructObject realObject = *ptr; 
    
  • Accéder aux membres d’un objet à partir d’un pointeur avec l’opérateur ->:
    struct StructObjet 
    { 
      public int InnerVariable; 
    } 
    
    // ...
    StructObject instance = new StructObject(); // Instanciation sur la pile 
    StructObject* ptr = &instance; // fixed n’est pas nécessaire 
    int innerValue = ptr->InnerVariable; 
    

Implémentation de GetPinnableReference()

Le choix de la variable retournée par GetPinnableReference() n’est pas anodin car si elle est déplacée par le GC dans le bloc de code suivant fixed, les pointeurs pourraient rediriger vers de mauvaises adresses. Des erreurs de compilation peuvent éviter certaines erreurs comme par exemple utiliser un pointeur provenant d’un objet de type référence qui n’est pas fixe en mémoire:

class ClassObject {} 

// ...
ClassObject instance = new ClassObject(); // Instance dans le tas managé 
ClassObject* ptr = &instance; // ERREUR: fixed est nécessaire 

L’erreur générée sera du type:

"CS0208: Cannot take the adresse of, get the size of, or declare a pointer to a managed type"

Quelques conseils d’implémentations pour GetPinnableReference():

  • Retourner un pointeur vers un objet natif: l’utilisation de code unsafe est le plus souvent motivée par la nécessité de manipuler des pointeurs dans le but d’effectuer des appels à du code non managé. Avec la fonction GetPinnableReference() on peut renvoyer un pointeur d’un objet alloué dans le tas non managé. Dans ce cas, le GC ne déplacera pas l’objet et son adresse ne sera pas modifiée.
  • Ne pas retourner un membre d’un objet de type référence: on pourrait être tenter de retourner un objet membre de type valeur. Si un objet de type valeur est membre d’un objet de type référence, le membre sera stocké dans le tas managé et non sur la pile. Lors de son exécution, le GC pourra affecter l’adresse du membre de la même façon que l’objet parent de type référence. Ce type d’implémentation est, donc, à éviter:
    class BadImplementation 
    { 
      public int InnerValueObject; 
    
      public ref int GetPinnableReference() 
      { 
        return ref this.InnerValueObject; // A éviter: ne pas utiliser 
      } 
    } 
    
    // ...
    var refTypeObject = new BadImplemetation(); 
    unsafe 
    { 
      fixed (int* ptr = refTypeObject) 
      { 
        // ... 
      } 
    } 
    
  • S’aider de Span<T>: dans le cas où on n’utilise pas de pointeurs vers un objet natif, on peut s’aider de Span<T>. Cet objet, par construction, épingle l’objet qu’il utilise, on peut donc être sûr qu’il ne sera pas déplacé:
    class PinnableClass 
    { 
      private readonly int[] InnerVariable = new int[] {0}; 
    
      public ref int GetPinnableReference() 
      { 
        Span<int> pinnableRef = this.InnerVariable.AsSpan(); // Span est construit sans effectuer de copie 
    
        return ref pinnableRef[0]; // L’object ref est retourné sans effectuer de copie 
      } 
    } 
    
  • Si on utilise le membre d’une classe, implémenter GetPinnableReference() n’est pas nécessaire: si on souhaite seulement épingler un membre d’un objet managé, il n’est pas nécessaire d’implémenter GetPinnableReferences(), on peut se contenter d’extraire le pointeur directement à partir du membre (disponible avant C# 7.3):
    public class MoveableObject 
    { 
      public int InnerVariable; 
    } 
    
    // ...
    var moveableObject = new MoveableObject(); 
    unsafe 
    { 
      fixed(int* ptr = &moveableObject.InnerVariable) 
      { 
    
        // Lecture en utilisant le pointeur 
        Console.WriteLine(*ptr); 
    
        // Écriture en utilisant le pointeur 
        *ptr = 5; 
      } 
    } 
    

    Il faut, toutefois, garder à l’esprit que ptr devient un pointeur fixe sur le membre d’un objet managé. Si on modifie les membres de l’objet managé notamment avec des tableaux d’objets, même s’il n’est pas déplacé par le GC, l’organisation des membres dans l’objet peut être modifié et le pointeur peut ne plus pointer à la bonne adresse mémoire. Dans l’exemple précédent, la valeur du membre est modifiée mais la taille du type de InnerVariable n’est pas modifiée, son adresse pointe donc bien vers le même objet.

Utiliser fixed pour déclarer un buffer

C# 7.3

A partir de C# 7.3, pour faciliter l’implémentation de buffers utilisés avec du code non managé, il est possible d’indiquer qu’un tableau membre d’une structure est fixe:

unsafe struct BufferWrapper 
{ 
  public fixed int buffer[5]; 
} 

Le tableau doit satisfaire certaines conditions:

  • Le type des éléments du tableau doit être un type primitif (bool, byte, short, ushort, int, uint, long, ulong, float, double ou char).
  • La taille du tableau est fixe et doit être déclarée au moyen d’une constante.
  • Il faut faire attention à la déclaration, la taille du tableau doit être indiquée après le nom du membre:
    public fixed int buffer[5]; 
    

    Et non:

    public fixed int[] buffer; // ERREUR
    

    ou

    public fixed int buffer[]; // ERREUR
    

On peut se demander l’intérêt de cette fonctionnalité en sachant que le plus souvent, l’instance d’une structure et son membre sont stockés sur la pile. L’instance d’une structure ne sera donc pas impactée par le GC. Toutefois si la structure est elle-même le membre d’un objet parent de type référence, elle sera allouée dans le tas managé et non sur la pile. Dans ce cas là, elle peut être amenée à être déplacée par le GC. Ainsi pour éviter d’alourdir l’implémentation en épinglant l’objet parent de type référence, il est possible d’indiquer que le membre de la structure est fixe de façon à ce qu’il soit utilisé dans un contexte unsafe éventuellement avec du code natif. Il n’est pas nécessaire ensuite d’utiliser fixed:

unsafe struct BufferWrapper 
{ 
  public fixed int buffer[5]; 
} 

unsafe class MoveableRefObject 
{ 
  public BufferWrapper InnerBuffer; 

  public MoveableRefObject() 
  { 
    this.InnerBuffer = new InnerBuffer(); 
  } 
} 

// ...
var moveableRefObject = new MoveRefObject(); 
unsafe 
{ 
  // Écriture classique 
  moveableRefObject.InnerBuffer.buffer[0] = 42; 

  // Lecture 
  Console.WriteLine(moveableRefObject.InnerBuffer.buffer[0]); 
} 

L’utilisation du pointeur du membre peut toujours être utilisé:

unsafe 
{ 
  fixed (int* ptr = moveableRefObject.InnerBuffer.buffer) 
  {
    *ptr = 42; 
  } 
} 

Références

Leave a Reply