Visual C#: небезопасный код Печать
Добавил(а) microsin   

В статье рассматривается, как использовать небезопасный (unsafe) код (код, где есть указатели) в C# - перевод руководства из MSDN [1].

Использование указателей (небезопасный контекст) требуется в C# не часто, только в некоторых ситуациях. Например, в следующих случаях:

• Работа с существующими структурами на диске.
• Сценарии с Advanced COM или Platform Invoke (PInvoke), когда вовлекаются структуры с указателями на них.
• Критичный к производительности код.

Использование небезопасного контекста в других случаях не рекомендуется. Особенно не нужно использовать unsafe context, чтобы писать код C средствами языка C#.

Внимание: код, написанный в unsafe-контексте не может быть проверен на безопасность, так что этот код должен выполняться только тогда, когда к нему есть полное доверие. Другими словами, unsafe-код не может выполняться в не доверяемом окружении. Например, Вы не можете запустить unsafe-код напрямую из Интернет.

В руководстве рассмотрено 3 примера:

• Пример 1: использование указателей, чтобы копировать массив байт.
• Пример 2: показывает, как вызвать функцию ReadFile из Windows API.
• Пример 3: показывает, как напечатать Win32-версию исполняемого файла.

[Пример 1]

Здесь используются указатели для копирования массива байт из src в dst. Пример следует компилировать с опцией /unsafe.

// fastcopy.cs
// Компилируйте с опцией /unsafe
using System;
 
class Test
{
   // Ключевое слово unsafe позволяет использовать указатели внутри
   // следующего метода:
   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();
      }
 
      // Оператор fixed помечает место в памяти объектов
      // src и dst, так чтобы они не перемещались системой
      // сбора мусора (garbage collection, GC).
      fixed (byte* pSrc = src, pDst = dst)
      {
         byte* ps = pSrc;
         byte* pd = pDst;
         // Цикл по массиву блоками по 4 байта, благодаря чему
         // за 1 раз копируется целое число int (4 байта):
         for (int n =0 ; n < count/4 ; n++)
         {
            *((int*)pd) = *((int*)ps);
            pd += 4;
            ps += 4;
         }
 
         // Завершение копирования, где перемещаются байты, 
         // которые не могут уложиться в блоки по 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("Первые 10 элементов:");
      for(int i=0; i < 10; ++i) 
         Console.Write(b[i] + " ");
      Console.WriteLine("\n");
   }
}

Пример выведет следующее:

Первые 10 элементов:
0 1 2 3 4 5 6 7 8 9

Обратите внимание на следующие моменты в примере 1:

• В коде используется ключевое слово unsafe, которое позволяет использовать указатели в методе Copy.
• Оператор fixed используется для декларирования указателей на массивы источника данных (откуда данные копируются) и получателя данных (куда копируются). Он привязывает места расположения в памяти для объектов src и dst, чтобы они не были случайно перемещены сборщиком мусора. Объекты отвязываются, когда завершается блок оператора fixed.
• Небезопасный код работает быстрее, потому что пропускаются проверки границ массива.

[Пример 2]

Здесь показан вызов функции ReadFile из Windows API (Platform SDK), которая требует использования небезопасного контекста, потому что требует в параметрах указатель на буфер для чтения. Демонстрируется утилита командной строки, которая открывает указанный в командной строке файл, и отображает в консоли его содержимое.

// readfile.cs
// Компилируйте с опцией /unsafe
 
// Программа читает и отображает текстовый файл.
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,                    // имя файла
         uint DesiredAccess,                 // режим доступа
         uint ShareMode,                     // режим общего использования
         uint SecurityAttributes,            // атрибуты безопасности
         uint CreationDisposition,           // как создавать
         uint FlagsAndAttributes,            // атрибуты файла
         int hTemplateFile                   // handle для шаблона файла
         );
   [DllImport("kernel32", SetLastError=true)]
   static extern unsafe bool ReadFile(
         IntPtr hFile,                       // handle файла
         void* pBuffer,                      // буфер данных
         int NumberOfBytesToRead,            // количество байт для чтения
         int* pNumberOfBytesRead,            // количество прочитанных байт
         int Overlapped                      // здесь должен быть указатель на 
                                             // структуру overlapped, но в данном
                                             // примере она не используется, так
                                             // что тут просто int.
         );
   [DllImport("kernel32", SetLastError=true)]
   static extern unsafe bool CloseHandle(
         IntPtr hObject   // handle объекта
         );
   
   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()
   {
      // закрыть хендл файла
      return CloseHandle(handle);
   }
}
 
class Test
{
   public static int Main(string[] args)
   {
      if (args.Length != 1)
      {
         Console.WriteLine("Usage : ReadFile < имя_файла >");
         return 1;
      }
      
      if (! System.IO.File.Exists(args[0]))
      {
         Console.WriteLine("Файл " + args[0] + " не найден."); 
         return 1;
      }
 
      byte[] buffer = new byte[128];
      FileReader fr = new FileReader();
      
      if (fr.Open(args[0]))
      {          
         // Подразумевается, что происходит чтение файла в кодировке ASCII
         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("Не получилось открыть запрашиваемый файл");
         return 1;
      }
   }
}

Массив байт, передаваемый в функцию Read, является управляемым типом (managed-тип. Что такое managed и unmanaged, см. в [2]). Managed означает, что сборщик мусора мог бы перенести место в памяти, где находится указанный массив. Оператор fixed позволяет Вам как получить указатель на память, где находится массив, так и привязать его к памяти, пометив для сборщика мусора, что нельзя манипулировать областью памяти массива во время действия оператора fixed.

По окончании блока fixed экземпляр массива помечается, что теперь его можно переносить. Эта возможность известна как декларативное прикрепления (declarative pinning). Хорошее свойство этой технологии в том, что она не затратная за исключением случая, когда сборщик мусора попытается обработать фиксированный блок, что довольно редкое явление.

[Пример 3]

Код читает и отображает номер версии Win32 исполняемого файла, который у примера тот же самый, что и номер версии сборки. Исполняемый файл примера printversion.exe. Используются функции VerQueryValue, GetFileVersionInfoSize и GetFileVersionInfo из Platform SDK. В примере применяются указатели, потому что это упрощает использование методов с сигнатурами, где есть указатели. Указатели обычная техника в Win32 API.

// printversion.cs
// Компилируйте с опцией /unsafe
using System;
using System.Reflection;
using System.Runtime.InteropServices;
 
// Эта строка задает номер версии сборки:
[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);
   
   // Третий параметр - "out string pValue" - автоматически маршалируется
   // из ANSI в Unicode:
   [DllImport("version.dll")]
   unsafe public static extern bool VerQueryValue (byte[] pBlock,
         string pSubBlock, out string pValue, out uint len);
   // Эта перезагрузка VerQueryValue помечена 'unsafe', потому что
   // использует short*:
   [DllImport("version.dll")]
   unsafe public static extern bool VerQueryValue (byte[] pBlock,
         string pSubBlock, out short *pValue, out uint len);
}
 
public class C 
{
   // Тело функции Main помечено unsafe, потому что в нем используются указатели:
   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("Не получилось запросить версию файла.");
            return 1;
         }
 
         short *subBlock = null;
         uint len = 0;
 
         // Получение locale-информации из информации версии:
         if (!Win32Imports.VerQueryValue (buffer,
                                          @"\VarFileInfo\Translation",
                                          out subBlock,
                                          out len))
         {
            Console.WriteLine("Не получилось запросить версию файла.");
            return 1;
         }
 
         string spv = @"\StringFileInfo\"
                    + subBlock[0].ToString("X4")
                    + subBlock[1].ToString("X4")
                    + @"\ProductVersion";
 
         byte *pVersion = null;
         // Получить значение ProductVersion для этой программы:
         string versionInfo;
         if (!Win32Imports.VerQueryValue (buffer, spv, out versionInfo, out len))
         {
            Console.WriteLine("Не получилось запросить версию файла.");
            return 1;
         }
 
        Console.WriteLine ("ProductVersion == {0}", versionInfo);
      }
      catch (Exception e) 
      {
         Console.WriteLine ("Произошло неожиданное исключение (ошибка exception) "
                          + e.Message);
      }
   
      return 0;
   }
}

Пример вывода программы:

ProductVersion == 4.3.2.1

[Небезопасный код и язык C#. Ключевое слово unsafe]

Принцип работы языка C# отличается от языков C и C++ тем, что в нем отсутствуют указатели как тип данных. Вместо указателей C# предоставляет ссылки (references) и возможность создавать объекты, размещение в памяти которых (автоматически) обслуживается сборщиком мусора (garbage collector). Считается, что сборщик мусора вместе с другими технологическими возможностями делает язык C# безопаснее, чем C или C++. Т. е. в базовом языке C# просто невозможно иметь не инициализированную переменную, указатель "в никуда", или выражение, которое индексирует массив за его границы. Таким образом исключается возможность большой категории ошибок, которыми страдают программы на C и C++.

Примечание переводчика: за все надо платить. Теперь придется мириться с тормозами программ и с диким геморроем при использовании Windows API.

Несмотря на то, что практически у каждой конструкции с типом указателя на C или C++ есть аналог в виде reference-типа на C#, все равно бывают ситуации, когда нужен доступ к памяти с использованием указателей. Например, когда необходимо взаимодействие с нижележащей операционной системой, доступ к устройству, имеющему привязку к памяти, или если нужно реализовать критичный по времени выполнения алгоритм - в таких случаях бывает нельзя отказаться от указателей. Чтобы решить эти проблемы C# предоставляет возможность писать небезопасный (unsafe) код.

В unsafe-коде можно декларировать указатели и работать с ними, выполнять преобразования между указателями и целочисленными типами, получить адрес переменной и т. д. В некотором смысле написание unsafe-кода на C# во многом похожа на традиционное (native) программирование на языке C.

Unsafe-код фактически "безопасная" возможность с точки зрения разработчиков и пользователей, как бы странно ни звучало. Unsafe-код должен быть четко помечен модификатором unsafe, чтобы разработчики не могли использовать эту возможность случайно. Это гарантирует, что и система выполнения кода не будет выполнять небезопасный код в недоверяемом окружении.

Ключевое слово unsafe помечает небезопасный контекст, который требуется для использования указателей.

Вы можете использовать модификатор unsafe в декларировании типа или члена класса. Далее все текстовое расширение типа или члена класса считается небезопасным контекстом. Например, как в следующем методе, объявленном с модификатором unsafe:

unsafe static void FastCopy ( byte[] src, byte[] dst, int count )
{
   // Здесь работает unsafe-контекст, и можно использовать указатели.
   ...
}

Область unsafe-контекста находится от списка параметров до окончания метода, так что указатели можно использовать в списке параметров:

unsafe static void FastCopy ( byte* ps, byte* pd, int count ) {...}

Вы также можете использовать unsafe-блок кода, в котором допускается небезопасный код. Например:

unsafe 
{
   // unsafe-контекст: тут можно использовать указатели.
   ...
}

Чтобы можно было использовать unsafe-код, Вы должны указать для компилятора опцию /unsafe. Unsafe-код не проверяется подсистемой CLR (common language runtime). Простой пример:

// cs_unsafe_keyword.cs
// Компилируйте с опцией /unsafe
using System;
 
class UnsafeTest 
{
   // unsafe-метод: принимает указатель на int:
   unsafe static void SquarePtrParam (int* p) 
   {
      *p *= *p;
   }
 
   unsafe public static void Main() 
   {
      int i = 5;
 
      // unsafe-метод: использует оператор взятия адреса (&)
      SquarePtrParam (&i);
      Console.WriteLine (i);
   }
}

Пример вывода программы:

25

[Оператор fixed]

Оператор fixed запрещает сборщику мусора (garbage collector, GC) физическое перемещение переменной в памяти, что происходит в следующей форме:

fixed ( type* ptr = expr ) statement

Здесь:

type не обслуживаемый тип (unmanaged type) или void.
ptr имя указателя.
expr выражение, которое неявно конвертируется в type*.
statement выполняемый оператор или блок кода (код за фигурными скобками {}).

Оператор fixed разрешено использовать только в unsafe-контексте (код, ограниченный действием ключевого слова unsafe).

Оператор fixed устанавливает указатель на адрес managed-переменной, и одновременно "прикалывает" переменную к её физическому размещению в памяти на время выполнения оператора fixed. Без fixed указатели на managed-переменые были бы мало полезны, поскольку сборщик мусора мог бы непредсказуемо в любой момент переместить переменную в памяти. 

Примечание: фактически компилятор C# не позволит Вам установить значение указателя на managed-переменную, если это не делается под управлением оператора fixed.

// Предположим, есть class Point { public int x, y; }
Point pt = new Point();    // pt - managed-переменная, объект для GC
fixed ( int* p = &pt.x ){  // нужно использовать fixed, чтобы получить адрес pt.x,
    *p = 1;                // и пометить pt как не перемещаемое место в памяти, пока
                           // мы используем указатель p.
}

Вы можете инициализировать указатель адресом массива или строки:

fixed (int* p = arr) ...  // эквивалентно p = &arr[0]
fixed (char* p = str) ... // эквивалентно p = &str[0]

Вы можете инициализировать несколько указателей, пока они имеют один и тот же тип:

fixed (byte* ps = srcarray, pd = dstarray) {...}

Чтобы инициализировать указатели разных типов, просто используйте вложение для операторов fixed:

fixed (int* p1 = &p.x)
   fixed (double* p2 = &array[5])
      // здесь делайте что-то с p1 и p2

Нельзя модифицировать указатели внутри операторов fixed.

После завершения действия statement (т. е. оператора или блока кода fixed) любые "привязанные" к физической памяти переменные "отвязываются", и эти переменные снова попадают под действие сборщика мусора. Таким образом, не указывайте на эти переменные вне действия оператора fixed.

В unsafe-режиме Вы можете выделять память в стеке, что не является областью действия сборщика мусора, и таким образом не нужно "привязывать" оператором fixed такие unmanaged-переменные. Для дополнительной информации см. stackalloc. Пример:

// statements_fixed.cs
// Компилируйте с опцией /unsafe
using System;
 
class Point { 
   public int x, y; 
}
 
class FixedTest 
{
   // unsafe-метод: принимает указатель на int
   unsafe static void SquarePtrParam (int* p) 
   {
      *p *= *p;
   }
 
   unsafe public static void Main() 
   {
      Point pt = new Point();
 
      pt.x = 5;
      pt.y = 6;
      // действует "привязка" pt:
      fixed (int* p = &pt.x) 
      {
         SquarePtrParam (p);
      }
      // теперь "привязка" pt отменена:
      Console.WriteLine ("{0} {1}", pt.x, pt.y);
   }
}

Вывод программы:

25 6

[stackalloc]

Ключевое слово stackalloc применяется для выделения блока памяти в стеке.

type * ptr = stackalloc type [ expr ];

Здесь:

type unmanaged-тип.
ptr имя указателя.
expr интегральное выражение.

В результате будет в стеке (не в куче) будет выделен блок памяти достаточного размера, чтобы в нем поместилось expr элементов типа type. Адрес блока будет сохранен в указателе ptr. Эта память не попадает в область действия сборщика мусора, так что её не нужно привязывать к памяти оператором fixed. Время жизни выделенного таким способом блока памяти ограничено методом, где применен stackalloc.

Можно применять stackalloc только в инициализаторах локальной переменной.

Поскольку привлекается использование типов указателей, stackalloc требует unsafe-контекста.

Оператор stackalloc аналогичен функции _alloca в run-time библиотеке языка C.

Пример:

// cs_keyword_stackalloc.cs
// Компилировать с опцией /unsafe
using System;
 
class Test
{
   public static unsafe void Main() 
   {
      int* fib = stackalloc int[100];
      int* p = fib;
      *p++ = *p++ = 1;
      for (int i=2; i < 100; ++i, ++p)
         *p = p[-1] + p[-2];
      for (int i=0; i < 10; ++i)
         Console.WriteLine (fib[i]);
   }
}

Вывод примера:

1
1
2
3
5
8
13
21
34
55

[Ссылки]

1. Unsafe Code Tutorial site:msdn.microsoft.com.
2. Маршалинг в C#. Простые типы.