Ненадежный код, типы указателей и указатели функций

Большая часть написанного кода C# — это "проверяемо безопасный код". Проверенный безопасный код означает, что средства .NET могут убедиться, что код является безопасным. Как правило, в безопасном коде не используется прямой доступ к памяти с помощью указателей. В нем также не используется выделение блоков памяти в «сыром» виде. Вместо этого в нем создаются управляемые объекты.

C# поддерживает контекст unsafe, в котором можно писать непроверяемый код. В контексте unsafe в коде можно использовать указатели, выделять и освобождать блоки памяти, а также обращаться к методам с помощью указателей функций. Небезопасный код в C# не обязательно является опасным, просто его безопасность нельзя проверить.

Небезопасный код имеет следующие свойства:

  • Методы, типы и блоки кода можно определить как небезопасные.
  • В некоторых случаях небезопасный код может увеличить скорость работы приложения, если не проверяются границы массивов.
  • Небезопасный код необходим при вызове встроенных стандартных функций, требующих указателей.
  • Использование небезопасного кода создает риски для стабильности и безопасности.
  • Код, содержащий небезопасные блоки, должен компилироваться с параметром компилятора AllowUnsafeBlocks.

Типы указателей

В небезопасном контексте тип может быть не только типом значения или ссылочным типом, но и типом указателя. Объявления типа указателя выполняется одним из следующих способов:

type* identifier;
void* identifier; //allowed but not recommended

Тип, указанный до * в типе указателя, называется ссылочным типом. Ссылочным типом может быть только неуправляемый тип.

Типы указателей не наследуются от объекта, а типы указателей не преобразуются в object. Кроме того, упаковка-преобразование и распаковка-преобразование не поддерживают указатели. Однако можно выполнять преобразования между различными типами указателей, а также между типами указателей и целочисленными типами.

При объявлении нескольких указателей в одном объявлении знак (*) указывается только с базовым типом. Он не используется в качестве префикса для каждого имени указателя. Например:

int* p1, p2, p3;   // Ok
int *p1, *p2, *p3;   // Invalid in C#

Указатель не может указывать на ссылку или на структуру, содержащую ссылки, поскольку ссылка на объект может быть подвергнута сбору мусора, даже если на нее указывает указатель. Сборщик мусора не отслеживает наличие указателей любых типов, указывающих на объекты.

Значением переменной-указателя типа MyType* является адрес переменной типа MyType. Ниже приведены примеры объявлений типов указателей.

  • int* p: p — указатель на целое число.
  • int** p: p — указатель на указатель на целое число.
  • int*[] p: p — одномерный массив указателей на целые числа.
  • char* p: p — указатель на тип char.
  • void* p: p — указатель на неизвестный тип.

Оператор косвенного обращения указателя * можно использовать для доступа к содержимому, на которое указывает переменная-указатель. В качестве примера рассмотрим следующее объявление:

int* myVariable;

Выражение *myVariable обозначает переменную int, находящуюся по адресу, содержащемуся в myVariable.

Несколько примеров указателей можно найти в статьях, посвященных операторуfixed. В следующем примере используются ключевое слово unsafe и оператор fixed, а также демонстрируется способ инкрементирования внутреннего указателя. Этот код можно вставить в функцию Main консольного приложения для его запуска. Эти примеры должны быть скомпилированы с заданным параметром AllowUnsafeBlocks.

// Normal pointer to an object.
int[] a = [10, 20, 30, 40, 50];
// Must be in unsafe code to use interior pointers.
unsafe
{
    // Must pin object on heap so that it doesn't move while using interior pointers.
    fixed (int* p = &a[0])
    {
        // p is pinned as well as object, so create another pointer to show incrementing it.
        int* p2 = p;
        Console.WriteLine(*p2);
        // Incrementing p2 bumps the pointer by four bytes due to its type ...
        p2 += 1;
        Console.WriteLine(*p2);
        p2 += 1;
        Console.WriteLine(*p2);
        Console.WriteLine("--------");
        Console.WriteLine(*p);
        // Dereferencing p and incrementing changes the value of a[0] ...
        *p += 1;
        Console.WriteLine(*p);
        *p += 1;
        Console.WriteLine(*p);
    }
}

Console.WriteLine("--------");
Console.WriteLine(a[0]);

/*
Output:
10
20
30
--------
10
11
12
--------
12
*/

Для указателя типа void* использовать оператор косвенного обращения нельзя. Однако можно использовать приведение для преобразования указателя типа void в любой другой тип и наоборот.

Указатель может иметь значение null. При применении оператора косвенного обращения к указателю со значением null результат зависит от конкретной реализации.

При передаче указателей между методами может возникнуть неопределенное поведение. Рекомендуется использовать метод, возвращающий указатель в локальную переменную с помощью параметра in, out или ref либо в виде результата функции. Если указатель был задан в фиксированном блоке, переменная, на которую он указывает, больше не может быть фиксированной.

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

Оператор Использование
* Косвенное обращение к указателю.
-> Доступ к члену структуры через указатель.
[] Индексирование указателя.
& Получение адреса переменной.
++ и --. Увеличение и уменьшение указателей.
+ и -. Арифметические действия с указателем.
==, !=, <, >, <= и >= Сравнение указателей.
stackalloc Выделение памяти в стеке.
Инструкция fixed Временная фиксация переменной, чтобы можно было найти ее адрес.

Дополнительные сведения об операторах, связанных с указателем, см. в разделе Операторы, связанные с указателем.

Любой тип указателя можно неявно преобразовать в тип void*. Любому типу указателя может быть присвоено значение null. Любой тип указателя можно явно преобразовать в любой другой тип указателя с помощью выражения приведения. Можно также преобразовать любой целочисленный тип в тип указателя или преобразовать любой тип указателя в целочисленный тип. Для этих преобразований требуется явным образом использовать приведение.

Например, в следующем примере тип int* преобразуется в тип byte*. Обратите внимание, что указатель указывает на наименьший адресуемый байт переменной. При последовательном увеличении результата до размера int (4 байта) можно отобразить оставшиеся байты переменной.

int number = 1024;

unsafe
{
    // Convert to byte:
    byte* p = (byte*)&number;

    System.Console.Write("The 4 bytes of the integer:");

    // Display the 4 bytes of the int variable:
    for (int i = 0 ; i < sizeof(int) ; ++i)
    {
        System.Console.Write(" {0:X2}", *p);
        // Increment the pointer:
        p++;
    }
    System.Console.WriteLine();
    System.Console.WriteLine("The value of the integer: {0}", number);

    /* Output:
        The 4 bytes of the integer: 00 04 00 00
        The value of the integer: 1024
    */
}

Буферы фиксированного размера

Вы можете использовать fixed ключевое слово для создания буфера с массивом фиксированного размера в структуре данных. Буферы фиксированного размера полезны при написании методов, взаимодействующих с источниками данных из других языков или платформ. Буфер фиксированного размера может принимать любые атрибуты или модификаторы, разрешенные для обычных элементов структуры. Единственным ограничением является то, что массив должен иметь тип bool, byte, char, short, int, long, sbyte, ushort, uint, ulong, float или double.

private fixed char name[30];

В безопасном коде структура C#, содержащая массив, не содержит элементы массива. Вместо этого в ней присутствуют ссылки на элементы. Вы можете внедрить массив фиксированного размера в структуру, если он используется в блоке небезопасного кода.

Размер следующего объекта struct не зависит от количества элементов в массиве, поскольку pathName представляет собой ссылку:

public struct PathArray
{
    public char[] pathName;
    private int reserved;
}

Структуру может содержать внедренный массив в небезопасном коде. В приведенном ниже примере массив fixedBuffer имеет фиксированный размер. Оператор используется fixed для получения указателя на первый элемент. С помощью этого указателя осуществляется доступ к элементам массива. Оператор fixed закрепляет поле экземпляра fixedBuffer в определенном месте в памяти.

internal unsafe struct Buffer
{
    public fixed char fixedBuffer[128];
}

internal unsafe class Example
{
    public Buffer buffer = default;
}

private static void AccessEmbeddedArray()
{
    var example = new Example();

    unsafe
    {
        // Pin the buffer to a fixed location in memory.
        fixed (char* charPtr = example.buffer.fixedBuffer)
        {
            *charPtr = 'A';
        }
        // Access safely through the index:
        char c = example.buffer.fixedBuffer[0];
        Console.WriteLine(c);

        // Modify through the index:
        example.buffer.fixedBuffer[0] = 'B';
        Console.WriteLine(example.buffer.fixedBuffer[0]);
    }
}

Размер массива из 128 элементов char составляет 256 байт. Буферы char фиксированного размера всегда принимают 2 байта на символ независимо от кодировки. Этот размер массива одинаков, даже если буферы символов маршалируются в методы ИЛИ структуры API.CharSet = CharSet.AutoCharSet = CharSet.Ansi Дополнительные сведения см. в разделе CharSet.

В предыдущем примере показано, как получить доступ к полям fixed без закрепления. Еще одним распространенным массивом фиксированного размера является массив bool. Элементы в массиве bool всегда имеют размер в 1 байт. Массивы bool не подходят для создания битовых массивов или буферов.

Буферы фиксированного размера компилируются с System.Runtime.CompilerServices.UnsafeValueTypeAttributeпомощью , который указывает среде CLR, что тип содержит неуправляемый массив, который может потенциально переполнение. Кроме того, в среде CLR для памяти, выделенной с помощью функции stackalloc, автоматически включаются функции обнаружения переполнения буфера. В предыдущем примере показано, как буфер фиксированного размера может существовать в объекте unsafe struct.

internal unsafe struct Buffer
{
    public fixed char fixedBuffer[128];
}

Созданный компилятором код C# для Buffer помечен с помощью атрибутов, как показано далее.

internal struct Buffer
{
    [StructLayout(LayoutKind.Sequential, Size = 256)]
    [CompilerGenerated]
    [UnsafeValueType]
    public struct <fixedBuffer>e__FixedBuffer
    {
        public char FixedElementField;
    }

    [FixedBuffer(typeof(char), 128)]
    public <fixedBuffer>e__FixedBuffer fixedBuffer;
}

Буферы фиксированного размера отличаются от обычных массивов следующими способами:

  • Может использоваться только в контексте unsafe .
  • Могут быть только полями экземпляров структур.
  • Всегда являются векторами или одномерными массивами.
  • Объявление должно включать длину, например fixed char id[8]. Вы не можете использовать fixed char id[].

Практическое руководство. Использование указателей для копирования массива байтов

В следующем примере указатели используются для копирования байт из одного массива в другой.

В этом примере применяется ключевое слово unsafe, которое позволяет использовать указатели в методе Copy. Оператор fixed используется для объявления указателей исходного и конечного массивов. Оператор fixedзакрепляет расположение исходного и конечного массивов в памяти, чтобы они не перемещались при сборке мусора. Закрепление этих блоков памяти отменяется, когда завершается обработка блока fixed. Поскольку метод Copy в этом примере использует ключевое слово unsafe, он должен быть скомпилирован с параметром компилятора AllowUnsafeBlocks.

В этом примере доступ к элементам обоих массивов выполняется с помощью индексов, а не второго неуправляемого указателя. Объявление указателей pSource и pTarget закрепляет массивы.

static unsafe void Copy(byte[] source, int sourceOffset, byte[] target,
    int targetOffset, int count)
{
    // If either array is not instantiated, you cannot complete the copy.
    if ((source == null) || (target == null))
    {
        throw new System.ArgumentException("source or target is null");
    }

    // If either offset, or the number of bytes to copy, is negative, you
    // cannot complete the copy.
    if ((sourceOffset < 0) || (targetOffset < 0) || (count < 0))
    {
        throw new System.ArgumentException("offset or bytes to copy is negative");
    }

    // If the number of bytes from the offset to the end of the array is
    // less than the number of bytes you want to copy, you cannot complete
    // the copy.
    if ((source.Length - sourceOffset < count) ||
        (target.Length - targetOffset < count))
    {
        throw new System.ArgumentException("offset to end of array is less than bytes to be copied");
    }

    // The following fixed statement pins the location of the source and
    // target objects in memory so that they will not be moved by garbage
    // collection.
    fixed (byte* pSource = source, pTarget = target)
    {
        // Copy the specified number of bytes from source to target.
        for (int i = 0; i < count; i++)
        {
            pTarget[targetOffset + i] = pSource[sourceOffset + i];
        }
    }
}

static void UnsafeCopyArrays()
{
    // Create two arrays of the same length.
    int length = 100;
    byte[] byteArray1 = new byte[length];
    byte[] byteArray2 = new byte[length];

    // Fill byteArray1 with 0 - 99.
    for (int i = 0; i < length; ++i)
    {
        byteArray1[i] = (byte)i;
    }

    // Display the first 10 elements in byteArray1.
    System.Console.WriteLine("The first 10 elements of the original are:");
    for (int i = 0; i < 10; ++i)
    {
        System.Console.Write(byteArray1[i] + " ");
    }
    System.Console.WriteLine("\n");

    // Copy the contents of byteArray1 to byteArray2.
    Copy(byteArray1, 0, byteArray2, 0, length);

    // Display the first 10 elements in the copy, byteArray2.
    System.Console.WriteLine("The first 10 elements of the copy are:");
    for (int i = 0; i < 10; ++i)
    {
        System.Console.Write(byteArray2[i] + " ");
    }
    System.Console.WriteLine("\n");

    // Copy the contents of the last 10 elements of byteArray1 to the
    // beginning of byteArray2.
    // The offset specifies where the copying begins in the source array.
    int offset = length - 10;
    Copy(byteArray1, offset, byteArray2, 0, length - offset);

    // Display the first 10 elements in the copy, byteArray2.
    System.Console.WriteLine("The first 10 elements of the copy are:");
    for (int i = 0; i < 10; ++i)
    {
        System.Console.Write(byteArray2[i] + " ");
    }
    System.Console.WriteLine("\n");
    /* Output:
        The first 10 elements of the original are:
        0 1 2 3 4 5 6 7 8 9

        The first 10 elements of the copy are:
        0 1 2 3 4 5 6 7 8 9

        The first 10 elements of the copy are:
        90 91 92 93 94 95 96 97 98 99
    */
}

Указатели функций

В C# имеются типы delegate, позволяющие определить безопасные объекты указателя функции. При вызове делегата создается экземпляр типа, производного от System.Delegate, и вызывается его виртуальный метод Invoke. В этом виртуальном вызове используется инструкция IL callvirt. В критически важных с точки зрения производительности путях к коду использование инструкции IL calli является более эффективным.

Указатель функции можно определить, используя синтаксический элемент delegate*. Вместо того, чтобы создавать экземпляр объекта delegate и вызывать метод Invoke, компилятор вызывает такую функцию, используя инструкцию calli. В следующем коде объявляются два метода, которые используют delegate или delegate* для объединения двух объектов одного типа. В первом методе используется тип делегата System.Func<T1,T2,TResult>. Во втором методе используется объявление delegate* с теми же параметрами и типом возвращаемого значения:

public static T Combine<T>(Func<T, T, T> combinator, T left, T right) => 
    combinator(left, right);

public static T UnsafeCombine<T>(delegate*<T, T, T> combinator, T left, T right) => 
    combinator(left, right);

В следующем коде показано, как объявить статическую локальную функцию и вызвать метод UnsafeCombine, используя указатель на эту локальную функцию:

static int localMultiply(int x, int y) => x * y;
int product = UnsafeCombine(&localMultiply, 3, 4);

В приведенном выше коде иллюстрируется ряд правил работы с функциями, доступ к которым осуществляется по указателю:

  • Указатели функций могут быть объявлены только в контексте unsafe.
  • Методы, принимающие в качестве параметра значение типа delegate* (или возвращающие значение типа delegate*), могут вызываться только в контексте unsafe.
  • Оператор & для получения адреса функции допускается только для функций static. (Это правило применяется как к функциям-членам, так и к локальным функциям).

Синтаксис имеет сходства с объявлением типов delegate и использованием указателей. Суффикс * в служебном слове delegate указывает на то, что данное объявление является указателем функции. Знак & при назначении группы методов указателю функции указывает, что операция использует адрес метода.

Для delegate* можно указать соглашение о вызовах, используя ключевые слова managed и unmanaged. Кроме того, соглашение о вызовах можно указать для указателей на функции unmanaged. Примеры для всех этих случаев можно увидеть в объявлениях ниже. В первом объявлении используется соглашение о вызовах managed, которое используется по умолчанию. Следующие четыре используют соглашение о вызове unmanaged . В каждом из них указано одно из соглашений о вызовах из стандарта ECMA 335: Cdecl, Stdcall, Fastcall или Thiscall. В последнем объявлении используется unmanaged соглашение о вызовах, в соответствии с инструкцией среды CLR выбрать соглашение о вызовах по умолчанию для платформы. Среда CLR выберет соглашение о вызовах во время выполнения кода.

public static T ManagedCombine<T>(delegate* managed<T, T, T> combinator, T left, T right) =>
    combinator(left, right);
public static T CDeclCombine<T>(delegate* unmanaged[Cdecl]<T, T, T> combinator, T left, T right) =>
    combinator(left, right);
public static T StdcallCombine<T>(delegate* unmanaged[Stdcall]<T, T, T> combinator, T left, T right) =>
    combinator(left, right);
public static T FastcallCombine<T>(delegate* unmanaged[Fastcall]<T, T, T> combinator, T left, T right) =>
    combinator(left, right);
public static T ThiscallCombine<T>(delegate* unmanaged[Thiscall]<T, T, T> combinator, T left, T right) =>
    combinator(left, right);
public static T UnmanagedCombine<T>(delegate* unmanaged<T, T, T> combinator, T left, T right) =>
    combinator(left, right);

Дополнительные сведения о указателях функций см. в спецификации функции указателя функции.

Спецификация языка C#

Дополнительные сведения см. в разделе Небезопасный кодспецификации языка C#.