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érenceGCHandle
à 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
.
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œudAllowUnsafeBlocks
dansPropertyGroup
:<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <!—- ... -—> <AllowUnsafeBlocks>true</AllowUnsafeBlocks> </PropertyGroup> </Project>
Fixed pattern
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()
ouref 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 pointeurp->m
ou accès à un élément d’un pointeurp[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
}
}
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émenterGetPinnableReferences()
, 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 deInnerVariable
n’est pas modifiée, son adresse pointe donc bien vers le même objet.
Utiliser fixed pour déclarer un buffer
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
ouchar
). - 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;
}
}
- fixed Statement: https://docs.microsoft.com/fr-fr/dotnet/csharp/language-reference/keywords/fixed-statement
- What’s new in C# 7.0 through C# 7.3: https://docs.microsoft.com/en-us/dotnet/csharp/whats-new/csharp-7
- Span<T>.GetPinnableReference Méthode: https://docs.microsoft.com/fr-fr/dotnet/api/system.span-1.getpinnablereference?view=netcore-3.1#System_Span_1_GetPinnableReference
- Fixed and moveable variables: https://docs.microsoft.com/fr-fr/dotnet/csharp/language-reference/language-specification/unsafe-code#fixed-and-moveable-variables
- GetPinnableReference Implementation that Pins Underlying String: https://stackoverflow.com/questions/52726779/getpinnablereference-implementation-that-pins-underlying-string
- Pointer member access: https://docs.microsoft.com/fr-fr/dotnet/csharp/language-reference/language-specification/unsafe-code#pointer-member-access
- Fixed pattern – C# 7.3 in Rider and ReSharper : https://blog.jetbrains.com/dotnet/2018/08/27/fixed-pattern/
- Pattern-based fixed statement: https://github.com/dotnet/csharplang/blob/master/proposals/csharp-7.3/pattern-based-fixed.md
- Using Span for high performance interop with unmanaged libraries: https://ericsink.com/entries/utf8z.html
- How can I pin an array of byte?: https://stackoverflow.com/questions/23254759/how-can-i-pin-an-array-of-byte
- .NET C# unsafe/fixed doesn’t pin passthrough array element?: https://stackoverflow.com/questions/5589945/net-c-sharp-unsafe-fixed-doesnt-pin-passthrough-array-element
- If I allocate some memory with AllocHGlobal, do I have to free it with FreeHGlobal?: https://stackoverflow.com/questions/17562295/if-i-allocate-some-memory-with-allochglobal-do-i-have-to-free-it-with-freehglob