Les contextes unsafe

Utilisation des contextes unsafe en C#

Résumé

Cet article a pour but de vous présenter quelque chose de relativement pratique lorsque l'on vient du C : les contextes unsafe en C#. Ces contextes permettent l'utilisation de pointeurs à la place de l'objet Marshal. Comme tout ce qui a rapport avec les pointeurs, les contextes unsafe sont à utiliser avec précaution.

Introduction aux contextes unsafe

Le but principal de .Net est de cacher le plus possible la gestion de la mémoire grâce aux références et au Garbage Collector. L'espace de nom System.Runtime.InteropServices et plus particulièrement la classe Marshal permet d'obtenir et d'utiliser les pointeurs en .Net. Cependant, il y a plus simple en C# : les contextes unsafe qui permettent d'utiliser les pointeurs en C# comme on le fait en C.

Utilisation

On peut, par exemple, utiliser les contextes unsafe dans les cas suivants :

  • appel d'API externes non écrites en .Net
  • débogage de la mémoire d'un processus ou autre
  • amélioration des performances en travaillant avec des pointeurs plutôt qu'avec des références

Avantages

Manipuler les données directement par pointeurs permet d'augmenter grandement les performances et la flexibilité du code
Utilisation des API non .Net. Certains paramètres de ces API peuvent être de types pointeurs. On peut utiliser DllImport et MarshalAs pour définir le marshaling des paramètres pointeurs mais il arrive que l'utilisation des pointeurs soit plus simple
Il n'y a pas d'autres moyens que Marshal ou les contextes unsafe pour connaître l'adresse mémoire d'une variable

Inconvénients et problèmes possibles

Les pointeurs peuvent donner des syntaxes très complexes relativement à celles que l'on trouve habituellement en C#
Nécessite de faire très attention à ce que l'on fait sans quoi on risque de modifier une autre variable que celle visée, faire des dépassements de pile, accéder à une zone mémoire non allouée...enfin tout un tas de choses qui ont pour résultat de faire crasher une application en beauté...
Code largement plus difficile à déboguer quand l'application crashe lorsque l'on ne s'y attend pas
Empêche l'application de s'exécuter si le framework est configuré pour ne pas autoriser l'absence de contrôle de types

Comparaison avec l'objet Marshal

L'objet Marshal, qui est l'objet d'un autre article, a deux avantages :

  • Il déclenche des exceptions (AccessViolationException,...) et ne devrait jamais faire crasher brutalement votre application
  • Il ne nécessite pas d'autorisation pour être utilisé (contrairement aux contextes unsafe qui doivent être activés dans la configuration du projet).

Cependant, son utilisation est plus contraignante (du fait de IntPtr) et donne une longueur de code plus conséquente que l'utilisation de pointeur.

Déclaration d'un contexte unsafe

Bon, maintenant que vous êtes conscient des risques mais aussi des avantages, nous pouvons commencer à les utiliser. Le mot clé unsafe indique d'ailleurs bien le caractère non sûr de la méthode. Ce mot clé peut se placer comme modificateur à plusieurs endroits :

  • devant class : indique que toute la classe peut utiliser des pointeurs. Par exemple :
    unsafe class NomClass {...}
  • devant une méthode/constructeur/propriété : indique que toute la méthode peut utiliser des pointeurs. Par exemple :
    unsafe type_retour NomMéthode(paramètres){...}
  • pour une déclaration d'une variable membre ou locale de type pointeur avec la notation étoilée :
    unsafe type *nom_pointeur;
  • pour déclarer une structure contenant un ou plusieurs pointeurs :
    unsafe struct nom_structure {...}
  • pour déclarer un block unsafe :
    unsafe {...}

On peut donc ensuite utiliser des pointeurs dans tout block (classe, méthode, block unsafe) marqué comme unsafe. En dehors de ces blocks, l'utilisation de pointeurs est interdite et génèrerait des erreurs de compilation.

Exemple :

unsafe class SomeUnsecureClass{
    //vous pouvez utiliser les pointeurs dans tout l'intérieur de la classe et de ses méthodes
}
class SomeClass
{
    unsafe void SomeDummyMethod() {
        //vous pouvez utiliser les pointeurs dans tout l'intérieur de cette méthode
    }
    static void Main() {
        //utilisation de pointeurs, interdite ici
        unsafe {
            //vous pouvez utiliser les pointeurs dans tout l'intérieur du block
        }
        //utilisation de pointeurs, interdite ici aussi
    }
}

Par contre, le code suivant ne compilera pas :


static void Main() {
    unsafe int *ptr;
    //on ne peut pas utiliser de pointeurs dans une méthode non marquée unsafe
}

Compilation avec des contextes unsafe

Comme précisé dans la section Déclaration d'un contexte unsafe, on ne peut pas compiler du code avec contexte unsafe sans le préciser explicitement. Ainsi si vous faites csc test.cs, vous allez recevoir l'erreur suivante "error CS0227: Unsafe code may only appear if compiling with /unsafe". Pour pouvoir compiler, il faut donc préciser le commutateur de ligne de commande /unsafe comme ceci csc test.cs /unsafe. Dans Visual Studio, vous pouvez activer ce commutateur par le menu Projet -> Propriétés de ... puis dans l'onglet Générer cocher la case Autoriser du code unsafe.

Utilisation des pointeurs

Déclaration de pointeurs

Pour déclarer un pointeur, il suffit d'ajouter * après le nom du type de la variable pointeur, par exemple, int* ptr;

Contrairement à la syntaxe C/C++, l'étoile * s'applique au type et non à la variable.

Les pointeurs void

Tout comme en C/C++, lorsque vous voulez un pointeur vers n'importe quel type et donc vers aucun type particulier, vous pouvez déclarer un pointeur void*, comme ceci : void* nom_pointeur;

Note: Ceci sert principalement lorsqu'un paramètre d'API nécessite un type void*. Pour le reste, void* n'a pas un grand intérêt.

Utiliser les pointeurs

Tout comme en C/C++, il existe deux opérateurs utilisables uniquement dans des contextes unsafe :

*nom_pointeur : lit la valeur pointée par le pointeur nom_pointeur
&variable : récupère l'adresse de la variable
nom_pointeur[index] : bien sûr utilisable en dehors des unsafe : permet de récupérer l'indexième type pointé par nom_pointeur, comme dans un tableau.
Par exemple :

static void Main() {
    //initialise une variable
    int some_var = 12;
    unsafe {
        //déclare deux pointeurs vers des entiers
        int* ptr1, ptr2;
        //récupère l'adresse de some_var et l'affecte à ptr1
        ptr1 = &some_var;
        //copie dans ptr2 l'adresse de some_var contenue dans ptr1
        ptr2 = ptr1;
        //affecte 34 à la variable pointée par ptr2 donc à some_var
        *ptr2 = 34;
    }
    //affiche 34
    Console.WriteLine(some_var);
}

Opérateur sizeof

Comme son nom l'indique, l'opérateur sizeof permet de récupérer la taille en octets du type qu'on lui passe en paramètre. Par exemple, sizeof(int) renverra 4. Précisons que cet opérateur peut s'utiliser hors d'un contexte unsafe. Cependant, il est plus utile dans un contexte unsafe qu'en dehors.

Types Exemples Tailles (octets)
sbyte sizeof(sbyte) 1
byte sizeof(byte) 1
short sizeof(short) 2
ushort sizeof(ushort) 2
int sizeof(int) 4
uint sizeof(uint) 4
long sizeof(long) 8
ulong sizeof(ulong) 8
char sizeof(char) 2
float sizeof(float) 4
double sizeof(double) 8
decimal sizeof(decimal) 16
bool sizeof(bool) 1

Note 1: Le type bool de C# est un vrai type bool comme en C++ et non un simple int comme l'alias BOOL en C.

Note 2 : Le type char est nativement UNICODE contrairement au type char du C/C++.

Caster des pointeurs vers un type entier

Après tout, un pointeur est un entier contenant une adresse mémoire. On peut donc simplement caster un pointeur en uint/ulong et un uint/ulong en type*. Par exemple ::

int x = 12;
int* ptr_x = &x;
uint y = (uint) ptr_x;
int* ptr_y = (int*) y;

On utilisera uint ou ulong sur les systèmes 32bits et ulong seulement sur les systèmes 64bits. Si vous utilisez un autre type, alors vous risquez des overflows. Et même si vous spécifiez checked, cela ne changera rien car le framework suppose que vous savez ce que vous faites avec les pointeurs et ne fait donc aucun contrôle.

Note: Ceci est surtout utile lorsque l'on veut les afficher avec Console.Write() ou Console.WriteLine() car ces méthodes ne supportent pas un paramètre de type pointeur. Par exemple :

Console.WriteLine("px = " + (uint)ptr_x);

Caster des pointeurs entre eux

On peut bien sûr caster un pointeur vers un type en un pointeur vers un autre type : type* ptr; autre_type* autre_ptr = (autre_type*)ptr;. Ceci peut servir à deux choses :

  • récupérer les bytes d'un type. Par exemple :
unsafe {
    int i = 0x12345678;
    byte* p = (byte*)&i;
    byte[] b = {p[0],p[1],p[2],p[3]};
}
  • copier la valeur brute (sans faire de calcul) d'un signé dans un non signé et réciproquement. Par exemple, pouvoir faire une conversion -1 <-> 0xFFFFFFFF.

Arithmétique des pointeurs

Tout comme en C/C++, on peut incrémenter ou décrémenter un pointeur pour pointer une valeur précédente ou suivante. On pourra employer les opérateurs suivants (sauf sur le type pointeur void*) :

  • +, += et ++ : par ex, ptr += 12; //si int* ptr, alors avance à la douzième case (int) du tableau pointé par ptr
  • -, -= et - : par ex, ptr--; //si int* ptr, alors recule à la case (int) précédente

Il est bien évident que l'on ajoute un entier uint/ulong à un pointeur pour le réaffecter au même pointeur ou à un autre pointeur. En effet, l'ajout entre deux pointeurs n'a aucun sens. Par contre, soustraire deux pointeurs permet de savoir le nombre de cases mémoire du type pointé qu'il y a entre les deux adresses. Par exemple (il est tout à fait possible d'initialiser un pointeur avec une constante) :

  • double *p1 = (double*) 12345632;
  • double *p2 = (double*) 12345600;
  • long nb_type p1 - p2; //ce qui donne : 32/sizeof(double) 32/8 = 4

Par exemple, si votre pointeur est de type int* alors si vous ajoutez 1 au pointeur, le compilateur comprendra que vous voulez accéder au prochain entier en mémoire et donc, qu'il doit ajouter sizeof(int) à l'adresse contenue dans le pointeur à savoir 4. La règle générale est que, étant donné un pointeur P pointant un type T auquel on ajoute N, le compilateur ajoutera, en fait, N * sizeof(T) à l'adresse contenue dans le pointeur P : P = P + N <=> P = (contenue(P)) + N * sizeof(T)

Pointeurs vers des structures

On peut aussi faire des pointeurs vers des structures à condition que celles-ci NE CONTIENNENT PAS de type REFERENCE mais PEUVENT contenir des pointeurs. On entend par référence, des objets .Net et non des pointeurs : on peut mettre un float* dans une structure déclarée unsafe mais pas un string. De toute façon, si l'on fait un pointeur vers une structure contenant des références, le compilateur génère une erreur. Un pointeur vers une structure sera exactement déclaré comme un autre pointeur. Il existe cependant deux façons d'accéder aux membres des structures :

  • (*un_pointeur).nom_membre : permet d'accéder au membre nom_membre de la structure pointée par un_pointeur.
  • un_pointeur->nom_membre : permet d'accéder au membre nom_membre de la structure pointée par un_pointeur. Plus concis : pointeur d'accès comme en C.

Voici comment utiliser les pointeurs de structure :

unsafe struct MyStruct {
    public byte b;
    public int i;
    public long l;
    public double d;
    public float* f;
}
//déclaration d'un pointeur vers la structure
MyStruct *pMyStruct;
//création effective de la structure
MyStruct myStruct = new MyStruct();
//récupération et affectation de l'adresse de la structure créée dans le pointeur
pMyStruct = & myStruct;
//accès aux membres
pMyStruct->b = 1;
(*pMyStruct).i = 123456;
pMyStruct->l = 123456;
(*pMyStruct).d = 123.45;
//utilisation de pointeur dans une structure :
float f = 0.1F;
pMyStruct->f = &f;

On peut aussi récupérer les adresses des membres de structures par la syntaxe &(variable.nom_membre) :

long *ptr_long = &(myStruct.l);
double *ptr_double = &(myStruct.d);

Pointeurs de membres de classes

Nous avons déjà vu la condition pour pouvoir créer un pointeur de structure : pas de types références dans la structure. De plus, on ne peut pas non plus faire de pointeur vers une classe. Et cela paraît logique puisque le Garbage Collector ne s'occupe que de références et non de pointeurs. Autoriser les pointeurs de classes aurait pour effet de pouvoir perdre un objet et donc d'empêcher le Garbage Collector de pouvoir le libérer. Par contre, les membres de classes peuvent être des types valeurs. On peut donc créer un pointeur vers un membre de classe mais pas avec la syntaxe vue précédemment. En effet, les classes sont stockées sur le tas et donc sont gérées par le Garbage Collector qui peut les reloger (changement d'adresse mémoire) à tout moment sans se soucier d'éventuels pointeurs vers un membre. Donc si vous essayez de pointer un membre de classe comme vu précédemment, le compilateur génèrera une erreur.

Pour pouvoir pointer des membres de classes, il faut utiliser un bloc fixed pour indiquer au Garbage Collector qu'il peut y avoir des pointeurs vers des membres d'une instance de classe dans le bloc fixed et qu'il ne doit donc pas reloger cette instance de classe.

Exemple :

class MyClass {
    public int i;
    public long l;
    public double d;
}
//instanciation de la classe
MyClass myClass = new MyClass();
//si vous décommentez le code suivant il produira une erreur de compilation
//pour les raisons évoquées ci-dessus
//long *ptr_long = &(myClass.l);
//création d'un pointeur vers un membre de l'instance de classe
fixed (long *ptr_long = &(myClass.l)) {
    //on peut ensuite utiliser le pointeur ptr_long dans ce bloc uniquement
    //dans ce bloc le Garbage Collector s'assurera de ne pas bouger
    //l'instance de classe myClass
}

Allocation de mémoire dans la pile

Avec la commande stackalloc, on peut allouer de la mémoire directement sur la pile. Pour allouer N int sur la pile et affecter la zone mémoire à un pointeur ptr, on écrira :

int* ptr = stackalloc int [N];

L'avantage de stackalloc est qu'il alloue simplement de la mémoire sans l'initialiser ce qui fait que c'est une méthode ultra rapide comparée à l'allocation d'un objet classique. Cela peut s'avérer très pratique pour allouer des tableaux directement sur la pile et non sur le tas comme le sont les tableaux dérivés d'ArrayList.

Exemple :

int size;
size = 10;
//on peut aussi donner la valeur de size au runtime
//allocation de 10 int sur la pile
int *int_array = stackalloc int [size];
//on a deux méthodes pour accéder aux cases du tableau
//la méthode lourde par pointeurs
*( int_array + 0) = 1;
//ou *int_array = 5;
*( int_array + 1) = 2;
*( int_array + 2) = 3;
//la méthode légère avec la syntaxe habituelle
int_array[3] = 4;
int_array[4] = 5;
int_array[5] = 6;

Note importante: Contrairement aux tableaux managés, si l'on passe les limites de la zone de mémoire allouée par stackalloc AUCUNE exception ne sera levée et une valeur écrite pourrait écraser n'importe quelle variable locale ou adresse de retour.

Un exemple concret : énumération des imprimantes du système

Un exemple d'utilisation des contextes unsafe peut être la récupération de la liste des imprimantes accessibles sur le système. On notera qu'il existe dans le Framework 3.0, un espace de nom System.Printing contenant des classes réalisant approximativement la même chose. Dans cet exemple, on alloue un buffer pour contenir le tableau des informations sur les imprimantes suivies des différentes chaînes référencées dans les cases de ce tableau. Chaque case du tableau est un PRINTER_INFO_2 qui sera pointé par pAddr et on pourra grâce à ce pointeur pAddr, parcourir le tableau (comme en C avec ++) et accéder aux membres de cette structure (comme en C avec ->)

Dans un fichier PrinterList.cs :

using System;
using System.Collections.Generic;
using System.Text;
using System.Runtime.InteropServices;
namespace prjContextUnsafe {
    class PrinterList {
        //indique le type des imprimantes que l'on renverra
        //Flags : permet de combiner les membres de l'énumération avec un OR
        [Flags()]
        public enum PrinterEnumFlags {
            PRINTER_ENUM_DEFAULT = 0x00000001,
            PRINTER_ENUM_LOCAL = 0x00000002,
            PRINTER_ENUM_CONNECTIONS = 0x00000004,
            PRINTER_ENUM_FAVORITE = 0x00000004,
            PRINTER_ENUM_NAME = 0x00000008,
            PRINTER_ENUM_REMOTE = 0x00000010,
            PRINTER_ENUM_SHARED = 0x00000020,

            PRINTER_ENUM_NETWORK = 0x00000040,
            PRINTER_ENUM_EXPAND = 0x00004000,
            PRINTER_ENUM_CONTAINER = 0x00008000,
            PRINTER_ENUM_ICONMASK = 0x00ff0000,
            PRINTER_ENUM_ICON1 = 0x00010000,
            PRINTER_ENUM_ICON2 = 0x00020000,
            PRINTER_ENUM_ICON3 = 0x00040000,
            PRINTER_ENUM_ICON4 = 0x00080000,
            PRINTER_ENUM_ICON5 = 0x00100000,
            PRINTER_ENUM_ICON6 = 0x00200000,
            PRINTER_ENUM_ICON7 = 0x00400000,
            PRINTER_ENUM_ICON8 = 0x00800000,
            PRINTER_ENUM_HIDE = 0x01000000
        }
        //contient des informations sur une imprimante
        public struct PRINTER_INFO_2 {
            public string pServerName;
            public string pPrinterName;
            public string pShareName;
            public string pPortName;
            public string pDriverName;
            public string pComment;
            public string pLocation;
            public IntPtr pDevMode;
            public string pSepFile;
            public string pPrintProcessor;
            public string pDatatype;
            public string pParameters;
            public IntPtr pSecurityDescriptor;
            public uint Attributes;
            public uint Priority;
            public uint DefaultPriority;
            public uint StartTime;
            public uint UntilTime;
            public uint Status;
            public uint cJobs;
            public uint AveragePPM;
        }
        //représente la structure unsafe permettant d'accéder
        //aux informations imprimantes brutes par pointeurs
        //LayoutKind.Sequential : indique que tous les membres sont continus en mémoire [StructLayout(LayoutKind.Sequential)]
        public unsafe struct rawPRINTER_INFO_2 {
            //chaînes pointées (utilisation de Marshal.PtrToStringAuto)
            public IntPtr pServerName;
            public IntPtr pPrinterName;
            public IntPtr pShareName;
            public IntPtr pPortName;
            public IntPtr pDriverName;
            public IntPtr pComment;
            public IntPtr pLocation;
            //pointeur vers une structure (mais que l'on n'utilise pas)
            public void* pDevMode;
            //chaînes pointées (utilisation de Marshal.PtrToStringAuto)
            public IntPtr pSepFile;
            public IntPtr pPrintProcessor;
            public IntPtr pDatatype;
            public IntPtr pParameters;
            //pointeur vers une structure (mais que l'on n'utilise pas)
            public void* pSecurityDescriptor;
            //donnée directement incluse dans la structure
            public uint Attributes;
            public uint Priority;
            public uint DefaultPriority;
            public uint StartTime;
            public uint UntilTime;
            public uint Status;
            public uint cJobs;
            public uint AveragePPM;
        }
        //API d'énumération des imprimantes
        //importée de winspool.drv
        //CharSet.Auto : utilisation du jeu de caractères par défaut du programme //SetLastError : appeler automatiquement GetLastError
        //en interne après l'exécution de l'API
        [DllImport("winspool.drv", CharSet = CharSet.Auto, SetLastError = true)]
        //gère le type bool comme le type BOOL C (0 = TRUE, <>0 = FALSE)
        [return: MarshalAs(UnmanagedType.Bool)]
        //API marquée comme unsafe car on utilise le type void* pour lui
        //passer le buffer pour les informations sur les imprimantes
        private unsafe static extern bool EnumPrinters(PrinterEnumFlags Flags, string Name, uint Level,void* pPrinterEnum, uint cbBuf,ref uint pcbNeeded, ref uint pcReturned);

        /// <summary>
        /// récupère la liste des imprimantes du système
        /// </summary>
        /// <param name="Flags">Type d'imprimantes à énumérer</param>
        /// <returns>renvoie un tableau d'informations sur les
        /// imprimantes listées</returns>
        private static PRINTER_INFO_2[] EnumPrinters(PrinterEnumFlags Flags) {
            //taille des données que l'api renvoie
            uint cbNeeded = 0;
            //nombre d'imprimantes énumérées
            uint cReturned = 0;
            //tableau de retour
            PRINTER_INFO_2[] retInfo2;
            //début d'utilisation des pointeurs
            unsafe {
                //demande de la taille des données d'énumération
                bool ret = EnumPrinters(PrinterEnumFlags.PRINTER_ENUM_LOCAL, null, 2, (void*)0, 0, ref cbNeeded, ref cReturned);
                //alloue la mémoire pour l'énumération
                void* pMem = (void*)Marshal.AllocHGlobal((int)cbNeeded);
                //récupère les imprimantes du système
                ret = EnumPrinters(PrinterEnumFlags.PRINTER_ENUM_LOCAL, null, 2, (void*)pMem, cbNeeded, ref cbNeeded, ref cReturned);
                //pointeur pour avancer dans le tableau
                //de l'énumération (rawPRINTER_INFO_2[])
                rawPRINTER_INFO_2* pAddr = (rawPRINTER_INFO_2*)pMem;
                //initialise le tableau renvoyé pour le nombre d'imprimantes
                retInfo2 = new PRINTER_INFO_2[cReturned];
                //si l'appel de l'api a réussi
                if (ret) {
                    //pour chaque imprimante
                    for (int i = 0; i < cReturned; i++) {
                        //pAddr pointe la première case du tableau d'imprimantes
                        //récupère les différentes chaînes en fonction
                        //du jeu de caractères en cours dans l'application
                        retInfo2[i].pServerName = Marshal.PtrToStringAuto(pAddr->pServerName);
                        retInfo2[i].pPrinterName = Marshal.PtrToStringAuto(pAddr->pPrinterName);
                        retInfo2[i].pShareName = Marshal.PtrToStringAuto(pAddr->pShareName);
                        retInfo2[i].pPortName = Marshal.PtrToStringAuto(pAddr->pPortName);
                        retInfo2[i].pDriverName = Marshal.PtrToStringAuto(pAddr->pDriverName);
                        retInfo2[i].pComment = Marshal.PtrToStringAuto(pAddr->pComment);
                        retInfo2[i].pLocation = Marshal.PtrToStringAuto(pAddr->pLocation);
                        retInfo2[i].pSepFile = Marshal.PtrToStringAuto(pAddr->pSepFile);
                        retInfo2[i].pPrintProcessor = Marshal.PtrToStringAuto(pAddr->pPrintProcessor);
                        retInfo2[i].pDatatype = Marshal.PtrToStringAuto(pAddr->pDatatype);
                        retInfo2[i].pParameters = Marshal.PtrToStringAuto(pAddr->pParameters);
                        //récupère des pointeurs vers des structures
                        //(mais que l'on n'utilisera pas)
                        retInfo2[i].pDevMode = new IntPtr(pAddr->pDevMode);
                        retInfo2[i].pSecurityDescriptor = new IntPtr(pAddr->pSecurityDescriptor);
                        //accède directement à la structure pointée par pAddr
                        //pour récupérer les valeurs entières des membres
                        retInfo2[i].Attributes = pAddr->Attributes;
                        retInfo2[i].Priority = pAddr->Priority;
                        retInfo2[i].DefaultPriority = pAddr->DefaultPriority;
                        retInfo2[i].StartTime = pAddr->StartTime;
                        retInfo2[i].UntilTime = pAddr->UntilTime;
                        retInfo2[i].Status = pAddr->Status;
                        retInfo2[i].cJobs = pAddr->cJobs;
                        retInfo2[i].AveragePPM = pAddr->AveragePPM;
                        //avance d'une case rawPRINTER_INFO_2 dans le tableau
                        // <=> (pAddr = (rawPRINTER_INFO_2*)
                        // ((byte*)pAddr + sizeof(rawPRINTER_INFO_2));
                        pAddr++;
                    }
                    //libère la mémoire allouée pour l'énumération
                    Marshal.FreeHGlobal((IntPtr)pMem);
                }
            }
            return retInfo2;
        }

        //liste des imprimantes
        private PRINTER_INFO_2[] printers;
        /// <summary>
        /// récupère la liste des imprimantes du système
        /// </summary>
        /// <param name="enFlags">Type d'imprimantes à énumérer</param>
        public PrinterList(PrinterEnumFlags enFlags) {
            this.printers = EnumPrinters(enFlags);
        }
        /// <summary>
        /// renvoie le nombre d'imprimantes énumérées
        /// </summary>
        /// <returns>renvoie le nombre d'imprimantes énumérées</returns>
        public int Count() {
            return this.printers.Length;
        }
        /// <summary>
        /// permet d'accéder aux imprimantes énumérées comme avec un tableau (objet[index])
        /// </summary>
        /// <param name="index">index de l'imprimante à renvoyer</param>
        /// <returns>renvoie des infos sur une imprimante</returns>
        public PRINTER_INFO_2 this[int index] {
            get {
                return this.printers[index];
            }
        }
    }
}

Dans le fichier Program.cs :

using System;
using System.Collections.Generic;
using System.Text;

namespace prjContextUnsafe {
    class Program {
        static void Main(string[] args) {
            PrinterList pl = new PrinterList(PrinterList.PrinterEnumFlags.PRINTER_ENUM_DEFAULT);
            Console.WriteLine("Printer Name Server Name Port Name Job Count");
            for (int i = 0; i < pl.Count(); i++) {
                Console.WriteLine("{0,-20} {1,-12} {2,-10} {3}", pl[i].pPrinterName, pl[i].pServerName, pl[i].pPortName, pl[i].cJobs);
            }
            Console.ReadKey();
        }
    }
}

Conclusion

Nous avons donc vu comment obtenir des pointeurs et les utiliser en C#. Cependant, comme l'indique le terme unsafe, l'utilisation de pointeurs n'est pas sûre et une toute petite erreur peut faire crasher votre application. Toutefois, les pointeurs peuvent vous apporter des bénéfices en performance et en flexibilité. Surtout gardez en mémoire de bien relire votre code unsafe car le débogage avec pointeur est bien plus délicat.

Ce document intitulé « Les contextes unsafe » issu de CodeS SourceS (codes-sources.commentcamarche.net) est mis à disposition sous les termes de la licence Creative Commons. Vous pouvez copier, modifier des copies de cette page, dans les conditions fixées par la licence, tant que cette note apparaît clairement.
Rejoignez-nous