Pointeurs et code non managé
1. Introduction
Comme dans tout langage .NET, C# cache au développeur la gestion de la mémoire. Le ramasse-miettes (alias Garbage Collector ou encore GC) s'occupe de tout.
Par défaut, le code est sécurisé, la mémoire est managée par le ramasse-miettes et le progammeur n'y a donc pas accès.
Bien que les cas où un programmeur .NET puisse avoir besoin de gérer lui même la mémoire sont extrèmement rares, ces situations existent et requiert donc de savoir comment fonctionnent et comment s'utilisent les pointeurs en C#.
1.1 C# et la mémoire
Dans tout langage .NET (dont C# fait partie), existe deux types de variables :
- les types valeurs
- les types références
Les types valeurs sont des types très primitifs (sbyte, byte, short, ushort, int, uint, long, ulong, float, double, decimal et struct) qui sont stockés sur la pile.
Les types références sont stockés sur le tas managé (par le ramase-miettes). Les classes sont des types références. Le type string est donc un type référence (vient de la classe System.String) et est donc stocké sur le tas managé.
La pile fonctionne de la même manière que dans n'importe quel autre langage, inutile de s'étaler donc ici sur son fonctionnement.
En revanche, le tas est avec .NET managé par le ramasse-miettes. Le ramasse-miettes gère toutes les références du programme et sait si les objets que ces références représentent doivent être ou non supprimés. Le ramasse-miettes ne nettoie pas en permanence la mémoire, il attend que le système d'exploitation lui en demande, c'est alors qu'il va libérer les objets devenus inutiles.
1.2 A propos
Bien sûr toutes ces explications sont très sommaires mais le thème de cet article n'est pas là.
2. Les pointeurs
2.1 Qu'est ce qu'un pointeur ?
Lorsque vous créez un programme, les variables que vous créez et manipulez (cela peut-être des entiers mais aussi des classes) sont stockées dans la mémoire (jusque là pas de problème en principe). Pour s'y retrouver, le programme ne retient pas la valeur de votre variable mais son adresse. Par exemple, si vous voulez récupérer votre variable qui contient 10 il ne va pas chercher une variable quelconque contenant la valeur 10 mais va aller directement à l'adresse de la variable que vous avez créé et qui contient cette valeur.
Les adresses mémoires sont des entiers affichés communément en écriture hexadécimale (exemple : 0x03c0b8).
En temps normal, vous n'avez jamais connaissance de ces adresses sauf éventuellement en cas d'erreur dans votre application, un message peut s'afficher en vous précisant les adresses mémoires contenant les données ayant créées le boggue. Mais à part dans ce cas extrème, tout ceci est masqué.
Les pointeurs offrent les mêmes possibilités que les variables standards sauf qu'ils vous permettent en plus d'accéder aux adresses de ces variables.
Ces adresses sont accessibles en lecture (vous pouvez la connaître) mais aussi en écriture (vous pouvez la modifier). Cela veut dire que vous pouvez déplacer le pointeur de pile pour aller le faire pointer quelques octets plus loin pour voir ce qui s'y trouve.
2.2 Comment utiliser les pointeurs en C#
Les pointeurs permettant un accès libre de la mémoire par le programmeur ou même tout autre personne (via une interface graphique type débogueur par exemple), ceux-ci représentent un risque et .NET ne peut contrôler ce qui est fait lors de l'utilisation des pointeurs. C'est pour cela qu'un code utilisant des pointeurs est reconnu comme étant non sécurisé et qu'il faut, pour pouvoir s'en servir, spécifier le mot clé unsafe devant les blocs de codes ou champs qui utilisent des pointeurs.
Pour une méthode :
unsafe void maMethode()
{
}
Pour une classe :
unsafe class MaClasse
{
}
Pour un champs :
unsafe class MaClasse
{
private unsafe int * pCounter;
}
Pour un bloc de code :
void maMethode()
{
//Code managé
unsafe
{
//Code non managé
}
//Code managé
}
Par contre, il est interdit de marquer comme unsafe une variable locale de cette façon :
void maMethode()
{
unsafe int * pX;
}
Vous devrez marquer toute la méthode comme non sécurisée. Les variables locales seront alors non sécurisées elles aussi.
Jusque là, vous ne pouvez toujours pas compiler votre code non sécurisé. Il y a une dernière étape à effectuer. Vous devez préciser lors de la compilation que votre code contient du code non sécurisé comme ceci :
csc /unsafe FichierSource.cs
ou encore :
csc -unsafe FichierSource.cs
Remarque : Sous Visual Studio, vous avez une case à cocher dans la partie "Build" des propriétés de votre projet.
2.3 Syntaxe des pointeurs
Une fois votre bloc de code marqué comme unsafe, vous pouvez déclarer un pointeur en utilisant cette syntaxe :
int* pX, pY;
double* pAmount;
Ici, nous déclarons 3 variables : 2 pointeurs vers des entiers (pX et pY) et un pointeur vers un réel (pAmount). Ces variables stockent des adresses, pas des valeurs. Les valeurs sont accessibles depuis ces adresses mais cela est indirect.
Cela signifie que pX, pY et pAmount contiennent des adresses qui pointent respectivement vers des espaces mémoires de la taille d'un entier, d'un autre entier et d'un réel.
Lorsque vous utiliser le symbole * dans une déclaration de variable, cela signifie que vous déclarez un pointeur.
Attention : Les développeurs C/C++ dovient prendre garde aux différences de syntaxes en C#. En C#, int* pX, pY correspond en C++ à int *pX, *pY. En C#, le symbole * est associé au type et non à l'identifiant.
Vous n'aurez ensuite que deux opérateurs à retenir pour pouvoir manipuler les pointeurs :
- L'opérateur d'adressage : & qui signifie "prendre l'adresse de" et permet de convertir un type standard (comme int) en un pointeur (int*).
- L'opérateur d'adressage indirect ou opérateur de déréférencement : * qui signifie "prendre le contenu de cette adresse" et permet donc de passer d'un type int* par exemple à un type int.
Remarquez que ces deux opérateurs ont un effet antagoniste.
Voyons comment utiliser les pointeurs en C#...
Exemple :
using System;
public class Program
{
public unsafe static void Main()
{
int x = 10;
int y = 20;
Console.WriteLine("X vaut : {0}", x);
Console.WriteLine("Y vaut : {0}", y);
int* pX, pY; //Création de deux pointeurs d'entiers
pX = &x; //On passe l'adresse de la variable x au pointeur pX.
//pX pointe maintenant sur x
*pX = 15; //On modifie que le contenu de pX, c'est à dire x
pY = pX; //On copie l'adresse de pX dans pY
/* pY pointe maintenant sur la même chose que pX, c'est à dire
* que pX et pY pointent sur x */
(*pY)++; //On incrémente le contenu de pY.
Console.WriteLine("X vaut : {0}", x);
Console.WriteLine("Y vaut : {0}", y);
Console.WriteLine("*pX vaut : {0}", *pX);
Console.WriteLine("*pY vaut : {0}", *pY);
Console.ReadLine();
}
}
Le code est commenté. Pour plus d'informations, testez et débogguez le en suivant pas à pas les modifications que subissent vos variables.
Vous pouvez déclarer des pointeurs pour tous les types valeurs mais pas pour les types références. Vous ne pouvez donc pas déclarer un pointeur sur une classe. En effet, le ramasse-miettes gérant lui même les classes et le tas managé, il serait alors incapable de contrôler convenablement les informations acquises sur celui-ci si vous êtes amenés à manipuler des classes car vous risquez alors d'altérez certaines informations que le ramasse-miettes tient toujours pour vrai.
2.4 Transtypages
Un pointeur stockant une adresse (un nombre), vous pouvez récupérez cette adresse. Il suffit pour cela de faire un petit transtypage.
int* pX;
uint addressPX = (uint)pX;
Notez cependant qu'une adresse sur une machine équipée d'un processeur 32 bits occupe 4 octets alors que sur un 64 bits, celle-ci en occupe 8. Par conséquent, il est préférable pour éviter tout dépassement de stocker l'adresse dans un type ulong.
int* pX;
ulong addressPX = (ulong)pX;
Il est également conseillé que la variable récupérant l'adresse ne soit pas signée car les adresses mémoires vont de 0 à plus de 4 milliards (232) sur un processeur 32 bits et de 0 à 264 sur les 64 bits. Par conséquent, le dépassement est possible et un int ou un long peuvent ne pas suffir à stocker des adresses...
Par exemple, vous aurez besoin de transtyper votre pointeur si vous voulez pouvoir l'afficher. En effet, les méthodes Console.Write ou Console.WriteLine ne prennent pas de pointeurs en paramètres et le transtypage apparaît alors comme obligatoire pour afficher l'adresse mémoire souhaitée.
using System;
public class Program
{
public unsafe static void Main()
{
int x = 10;
int* pX = &x;
Console.WriteLine("x vaut : {0} et est stocké à l'adresse 0x{1:X}", *pX, (ulong)pX);
Console.ReadLine();
}
}
Vous pouvez également transtyper des pointeurs entre eux. Convertir un pointeur d'int en un pointeur de double, un pointeur de decimal en un pointeur de byte, etc. Notez cependant que vous aurez alors accès une zone plus grande de la mémoire et que vous ne pourrez donc pas prévoir ce que celle-ci contient. Voyez l'exemple suivant :
using System;
public class Program
{
public unsafe static void Main()
{
byte b = 0x10;
byte* pB = &b;
int* pIntB = (int*)pB;
decimal* pDecimalB = (decimal*)pB;
Console.WriteLine("b vaut : {0} et est stocké à l'adresse 0x{1:X}", *pB, (ulong)pB);
Console.WriteLine("pIntB vaut : {0} et est stocké à l'adresse 0x{1:X}", *pIntB, (ulong)pIntB);
Console.WriteLine("pDecimalB vaut : {0} et est stocké à l'adresse 0x{1:X}", *pDecimalB, (ulong)pDecimalB);
Console.ReadLine();
}
}
Testez ce code... vous aurez probablement une petite surprise sur le dernier transtypage. Pourquoi ? Parce que vous passez d'un type byte (stocké sur un octet) à un type decimal (stocké sur 16 octets) et que dans ces conditions, les 15 octets innocupés au début sont subitement "visibles" et ceux-ci n'étant pas forcément mis à zéro, ils vont donc modifier la valeur stockée dans *pDecimalB...
Mais dans ce cas me direz vous, pourquoi le premier transtypage de pointeurs (du byte* vers le int*) ne fait il pas autant de dégâts ? Cela vient du fait que, sur un processeur 32 bits, le pointeur de pile est optimisé pour se déplacer à chaque fois de 32 bits (4 octets, bytes). Par conséquent, créer un byte, un short ou un int ou leurs versions non signées reviendra au même pour le pointeur de pile, celui-ci avancera de 4 octets et reculera de 4 octets lorsqu'il libérera les variables.
Cela permet d'améliorer les performances.
Finalement, transtyper d'un type byte* vers un type int* fait que les deux pointeurs contiennent la même valeur car le pointeur de pile à, au passage, mis à zéro les octets non utilisés.
2.5 Pointeurs void
Vous pouvez également déclarer des pointeurs de type void. Ces pointeurs pointent vers des données de types indéfinies, inconnues, mais leur utilité s'avère limité en C# car vous ne pouvez déréférencer ce type de pointeur.
La seule utilité des pointeurs void est de pouvoir appeler des fonctions API qui requiert ce type de paramètre.
2.6 Arithmétique des pointeurs
Les pointeurs stockant des adresses, des entiers, il est possible de manipuler ces entiers (ajouter, soustraire...) pour accéder à de nouvelles zones de la mémoire.
Pour cela, le compilateur C# procède d'une façon particulière.
Si vous avez par exemple un pointeur sur un type int (4 octet) et que vous incrémentez ce pointeur, alors vous ne passerez pas à l'octet suivant mais ferez sauter le pointeur de pile de 4 octets (la taille du type). Si vous avez un pointeur sur un double et que vous incrémentez/décrémentez celui-ci, alors le saut du pointeur sera cette fois-ci de 8 octets (taille d'un double), etc.
Concrètement, voici ce que cela donne :
Vous avez un pointeur d'entier qui pointe sur l'adresse 0x3B0ECC4
Vous incrémentez ce pointeur
Le pointeur ne pointe pas sur 0x3B0ECC5 mais sur 0x3B0ECC4 + sizeof(int), le pointeur pointe donc sur l'adresse 0x3B0ECC8.
Exemple avec du code :
int x = 100;
int* pX = &x;
Console.WriteLine("Adresse #1 : 0x{0:X} - Contenu : {1}", (ulong)pX, *pX);
pX++;
Console.WriteLine("Adresse #2 : 0x{0:X} - Contenu : {1}", (ulong)pX, *pX);
Vous pouvez également tester ce code avec d'autres type comme un byte, un double ou autre...
La règle générale est la suivante :
Soit un pointeur de type T pointant vers une adresse P. Alors le résultat de l'addition d'un entier X à P est définit comme étant P + X * sizeof(T).
Exemples :
int* pX = &x; int* pXprime = pX + 4; //pXprime pointe sur pX + 4 * sizeof(int) soit pX + 4 * 4, soit sur pX + 16
double* pD = &d; double* pDprime = pD + 4; //pDprime pointeur sur pD + 4 * sizeof(double) soit pX + 4 * 8, soit sur pD + 32
byte* pB = &b; byte* pBprime = pB + 3; //pBprime pointe sur l'adresse pB + 3 * sizeof(byte) soit sur pB + 3
Les opérateurs autorisés pour modifier les pointeurs sont les suivants : +, -, +=, -=, ++, --
Ces opérations sont interdites sur les pointeurs void.
2.7 Pointeurs sur les structures
Les structures étant des types valeurs, vous pouvez déclarer un pointeur sur une structure.
struct MyStruct
{
public byte B;
public int I;
public decimal D;
}
//Quelque part dans le code...
MyStruct myStruct = new MyStruct();
MyStruct* pMyStruct = &myStruct;
Vous pouvez ensuite accéder aux membres de la structure comme auparavant.
(*pMyStruct).I = 10;
(*pMyStruct).B = 0x0A;
(*pMyStruct).D = 10m;
Mais cette syntaxe est un peu lourde. Du coup, C# propose un opérateur permettant d'accéder directement aux membres d'une structure. Il s'agit de l'opérateur d'accès aux membres d'un pointeur dont le symbole est ->. Voici comment il s'utilise :
pMyStruct->I = 10;
pMyStruct->B = 0x0A;
pMyStruct->D = 10m;
Les développeurs C++ reconnaitront là un opérateur qu'ils utilisent très fréquemment.
Vous pouvez également récupérer directement les pointeurs des membres d'une structure. Vous n'avez, pour cela, pas forcément besoin du pointeur de structure mais juste de son instance.
MyStruct myStruct = new MyStruct();
byte* pB = &(myStruct.B);
int* pI = &(myStruct.I);
decimal* pD = &(myStruct.D);
ou encore...
MyStruct myStruct = new MyStruct();
MyStruct* pMyStruct = &myStruct;
byte* pB = &(pMyStruct->B);
int* pI = &(pMyStruct->I);
decimal* pD = &(pMyStruct->D);
2.8 Pointeurs sur les membres d'une classe
Comme il est possible de pointer sur les membres d'une structure, il est également possible de pointer sur les membres d'une classe (mais pas sur la classe elle-même vu qu'il s'agît d'un type référence). La seule condition est, bien sûr, que le membre pointé soit également un type valeur. Mais attention, il y a des différences. En effet, reprenons notre structure précédente mais cette fois ci en tant que classe.
class MyClass
{
public byte B;
public int I;
public decimal D;
}
//Quelque part dans le code...
MyClass myClass = new MyClass();
byte* pB = &(myClass.B); //Erreur à la compilation
int* pI = &(myClass.I); //Erreur à la compilation
decimal* pD = &(myClass.D); //Erreur à la compilation
Les erreurs qui surviendront à la compilation ne sont pas là par hasard. En effet, bien que les membres d'une classe qui sont de type valeur se trouvent sur la pile, ils restent incorporés dans la classe et sont donc sous le contrôle indirect du ramasse-miettes. Si celui-ci décide par exemple de déplacer la classe en mémoire afin de faire de l'ordre dans le tas, il recréera toutes les références et vos pointeurs sur ces types valeurs (appartenant à une classe) ne seront plus valides.
Pour palier à ce problème, C# propose un mot clé : fixed qui indique au ramasse-miettes de ne pas modifier les références des membres d'une classe concernée.
Voici comment le code précédent doit être tourné pour pouvoir fonctionner :
class MyClass
{
public byte B;
public int I;
public decimal D;
}
//Quelque part dans le code...
MyClass myClass = new MyClass();
fixed (byte* pB = &(myClass.B))
fixed (int* pI = &(myClass.I))
fixed (decimal* pD = &(myClass.D))
{
//Possibilité d'utiliser les pointeurs
}
Le pointeur a pour porté le bloc de code (sortie des accolades, le pointeur est détruit).
Vous pouvez placer une ou plusieurs instructions fixed avant un bloc de code (cf. code précédent)
Vous pouvez également imbriquer des blocs de code fixed entre eux.
Dernier point, vous pouvez déclarer plusieurs pointeurs de même type dans un même bloc fixed. Par exemple :
fixed (byte* pB = &(myClass.B), pB2 = &(myClass.B))
{
//Possibilité d'utiliser les pointeurs
}
2.9 Allocation de mémoire avec stackalloc
En code managé, pour réserver un espace en mémoire consacré à tant types de données, vous procédiez de la façon suivante :
byte[] byteArray = new byte[10]; //où 10 est la taille du tableau
En code non managé, c'est à dire lorsque vous utilisez des pointeurs, pour obtenir un espace mémoire destiné à un certain type de données dans une certaine quantité (tableau), vous devez utiliser le mot clé stackalloc comme le montre le morceau de code ci-après.
byte* bytes = stackalloc byte[10]; //où 10 est la taille du tableau
Ici, stackalloc va réserver un espace de 10 octets en mémoire, tous initialisés à 0 et va retourner à votre pointeur de byte l'adresse de base, celle de la première cellule de votre tableau.
Pour se déplacer ensuite dans ce tableau non managé, vous pouvez soit déplacer votre pointeur comme vous le faisiez auparavant, c'est à dire :
*bytes = 0x10; //Case 0
*(bytes + 1) = 0x03; //Case 1
*(bytes + 9) = 0xFF; //Case 9, dernière case ici
//ou encore
Console.WriteLine("Affichage, début du tableau");
for (int i = 0; i < 10; i++)
{
Console.WriteLine(*bytes);
bytes++; //Déplace le pointeur sur la prochaine case du tableau
}
/* Attention, arrivé ici, votre pointeur a dépassé le tableau et faire *bytes = 5 ne modifiera
* pas la case 0 du tableau mais la valeur du byte suivant le tableau en mémoire. */
Console.WriteLine("Fin du tableau");
ou sinon, vous pouvez faire :
bytes[0] = 0x10; //Case 0, vous pouvez toujours faire *bytes = 0x10 bien sûr
bytes[1] = 0x03; //Case 1
bytes[9] = 0xFF; //Case 9, dernière case ici
//ou encore
Console.WriteLine("Affichage, début du tableau");
for (int i = 0; i < 10; i++)
{
//Ne déplace pas le pointeur
Console.WriteLine(bytes[i]);
}
//Arrivé ici, votre pointeur pointe toujours sur la case 0 du tableau
Console.WriteLine("Fin du tableau");
Cependant, restez vigilant, car si un tableau en code managé est un objet encapsulé et lève une exception en cas de dépassement, ici, vous manipulez des pointeurs et dépasser l'espace alloué à votre tableau en code non managé pour y écrire une valeur altérera une variable qui, lorsqu'elle sera devenue à nouveau nécessaire au programme, risque d'en précipiter la fin en créant une erreur innatendue. Mais bon, si vous manipulez des pointeurs, vous devez savoir ce que vous faîtes...
3. Exemples
Je n'ai jamais eu besoin d'utiliser les pointeurs en C# personnellement, ce qui fait que je n'ai pas d'exemples de code C# utilisant du code non managé qui me viennent à l'esprit. Cependant, j'ai pu trouver sur MSDN les exemples qui suivent.
3.1 Projet FastCopy
//Copyright (C) Microsoft Corporation. All rights reserved.
// fastcopy.cs
// compile with: /unsafe
using System;
class Test
{
// The unsafe keyword allows pointers to be used within
// the following method:
static unsafe void Copy(byte[] src, int srcIndex,
byte[] dst, int dstIndex, int count)
{
if (src == null || srcIndex < 0 ||
dst == null || dstIndex < 0 || count < 0)
{
throw new ArgumentException();
}
int srcLen = src.Length;
int dstLen = dst.Length;
if (srcLen - srcIndex < count ||
dstLen - dstIndex < count)
{
throw new ArgumentException();
}
// The following fixed statement pins the location of
// the src and dst objects in memory so that they will
// not be moved by garbage collection.
fixed (byte* pSrc = src, pDst = dst)
{
byte* ps = pSrc;
byte* pd = pDst;
// Loop over the count in blocks of 4 bytes, copying an
// integer (4 bytes) at a time:
for (int n = 0; n < count / 4; n++)
{
*((int*)pd) = *((int*)ps);
pd += 4;
ps += 4;
}
// Complete the copy by moving any bytes that weren't
// moved in blocks of 4:
for (int n = 0; n < count % 4; n++)
{
*pd = *ps;
pd++;
ps++;
}
}
}
static void Main(string[] args)
{
byte[] a = new byte[100];
byte[] b = new byte[100];
for (int i = 0; i < 100; ++i)
a[i] = (byte)i;
Copy(a, 0, b, 0, 100);
Console.WriteLine("The first 10 elements are:");
for (int i = 0; i < 10; ++i)
Console.Write(b[i] + " ");
Console.WriteLine("\n");
}
}
3.2 Projet PrintVersion
Ce projet affiche la version de l'exécutable en cours (de ce programme-ci) en utilisant une DLL Windows.
//Copyright (C) Microsoft Corporation. All rights reserved.
// printversion.cs
// compile with: /unsafe
using System;
using System.Reflection;
using System.Runtime.InteropServices;
// Give this assembly a version number:
[assembly:AssemblyVersion("4.3.2.1")]
public class Win32Imports
{
[DllImport("version.dll")]
public static extern bool GetFileVersionInfo (string sFileName,
int handle, int size, byte[] infoBuffer);
[DllImport("version.dll")]
public static extern int GetFileVersionInfoSize (string sFileName,
out int handle);
// The 3rd parameter - "out string pValue" - is automatically
// marshaled from Ansi to Unicode:
[DllImport("version.dll")]
unsafe public static extern bool VerQueryValue (byte[] pBlock,
string pSubBlock, out string pValue, out uint len);
// This VerQueryValue overload is marked with 'unsafe' because
// it uses a short*:
[DllImport("version.dll")]
unsafe public static extern bool VerQueryValue (byte[] pBlock,
string pSubBlock, out short *pValue, out uint len);
}
public class C
{
// Main is marked with 'unsafe' because it uses pointers:
unsafe public static int Main ()
{
try
{
int handle = 0;
// Figure out how much version info there is:
int size =
Win32Imports.GetFileVersionInfoSize("printversion.exe",
out handle);
if (size == 0) return -1;
byte[] buffer = new byte[size];
if (!Win32Imports.GetFileVersionInfo("printversion.exe", handle, size, buffer))
{
Console.WriteLine("Failed to query file version information.");
return 1;
}
short *subBlock = null;
uint len = 0;
// Get the locale info from the version info:
if (!Win32Imports.VerQueryValue (buffer, @"\VarFileInfo\Translation", out subBlock, out len))
{
Console.WriteLine("Failed to query version information.");
return 1;
}
string spv = @"\StringFileInfo\" + subBlock[0].ToString("X4") + subBlock[1].ToString("X4") + @"\ProductVersion";
byte *pVersion = null;
// Get the ProductVersion value for this program:
string versionInfo;
if (!Win32Imports.VerQueryValue (buffer, spv, out versionInfo, out len))
{
Console.WriteLine("Failed to query version information.");
return 1;
}
Console.WriteLine ("ProductVersion == {0}", versionInfo);
}
catch (Exception e)
{
Console.WriteLine ("Caught unexpected exception " + e.Message);
}
return 0;
}
}
3.3 Projet ReadFile
Ce projet ouvre un fichier et en affiche le contenu en utilisant une DLL native de Windows : Kernel32.dll (je suis sûr que çà vous dit quelque chose). Vous devez passer par la ligne de commande pour spécifier le fichier à ouvrir ou sinon, vous pouvez modifier le code pour donner que args[0] contienne le nom du fichier à ouvrir.
//Copyright (C) Microsoft Corporation. All rights reserved.
// readfile.cs
// compile with: /unsafe
// arguments: readfile.txt
// Use the program to read and display a text file.
using System;
using System.Runtime.InteropServices;
using System.Text;
class FileReader
{
const uint GENERIC_READ = 0x80000000;
const uint OPEN_EXISTING = 3;
IntPtr handle;
[DllImport("kernel32", SetLastError=true)]
static extern unsafe IntPtr CreateFile(
string FileName, // file name
uint DesiredAccess, // access mode
uint ShareMode, // share mode
uint SecurityAttributes, // Security Attributes
uint CreationDisposition, // how to create
uint FlagsAndAttributes, // file attributes
int hTemplateFile // handle to template file
);
[DllImport("kernel32", SetLastError=true)]
static extern unsafe bool ReadFile(
IntPtr hFile, // handle to file
void* pBuffer, // data buffer
int NumberOfBytesToRead, // number of bytes to read
int* pNumberOfBytesRead, // number of bytes read
int Overlapped // overlapped buffer
);
[DllImport("kernel32", SetLastError=true)]
static extern unsafe bool CloseHandle(
IntPtr hObject // handle to object
);
public bool Open(string FileName)
{
// open the existing file for reading
handle = CreateFile(
FileName,
GENERIC_READ,
0,
0,
OPEN_EXISTING,
0,
0);
if (handle != IntPtr.Zero)
return true;
else
return false;
}
public unsafe int Read(byte[] buffer, int index, int count)
{
int n = 0;
fixed (byte* p = buffer)
{
if (!ReadFile(handle, p + index, count, &n, 0))
return 0;
}
return n;
}
public bool Close()
{
//close file handle
return CloseHandle(handle);
}
}
class Test
{
public static int Main(string[] args)
{
if (args.Length != 1)
{
Console.WriteLine("Usage : ReadFile <FileName>");
return 1;
}
if (! System.IO.File.Exists(args[0]))
{
Console.WriteLine("File " + args[0] + " not found.");
return 1;
}
byte[] buffer = new byte[128];
FileReader fr = new FileReader();
if (fr.Open(args[0]))
{
// We are assuming that we are reading an ASCII file
ASCIIEncoding Encoding = new ASCIIEncoding();
int bytesRead;
do
{
bytesRead = fr.Read(buffer, 0, buffer.Length);
string content = Encoding.GetString(buffer,0,bytesRead);
Console.Write("{0}", content);
}
while ( bytesRead > 0);
fr.Close();
return 0;
}
else
{
Console.WriteLine("Failed to open requested file");
return 1;
}
}
}
3.4 Les sources
Ces sources ont été, comme je l'ai dit au début, trouvées sur MSDN : sur cette page.
Les trois projets sont téléchargeables directement sur la page MSDN.
3.5 Pointeurs multiples
Voici un dernier exemple, après demande, montrant comment utiliser des pointeurs multiples en C#. Si vous avez bien compris comment les pointeurs fonctionnent, cela ne devraient pas vous poser de problème.
Le code ci-dessous manipule des matrices, des pointeurs doubles donc, et se contente de remplir un tableau pour en afficher le contenu.
using System;
namespace Article
{
class Program
{
private static int max = 10;
static unsafe void Main()
{
int** pTab = stackalloc int*[max + 1];
for (int i = 0; i < max + 1; i++)
{
int* p = stackalloc int[max + 1];
for (int j = 0; j < max + 1; j++)
{
if (i == 0 || j == 0)
p[j] = i > 0 ? i : j;
else
p[j] = i * j;
}
*pTab = p;
pTab++;
}
pTab -= max + 1;
Console.WriteLine("--------- Table de multiplications ---------\n");
for (int i = 0; i < max + 1; i++)
{
for (int j = 0; j < max + 1; j++)
Console.Write(pTab[i][j] < 10 ? "0" + pTab[i][j] + " " : pTab[i][j] + " ");
Console.WriteLine(Environment.NewLine);
}
Console.ReadLine();
}
}
}
Remarques :
- la fonction précédente peut s'implémenter sans pointeurs
- il y a différentes façons d'implémenter cette fonction avec les pointeurs