Código não seguro, tipos de ponteiro e ponteiros de função

A maioria do código C# que você escreve é "código verificável seguro". Código verificável e seguro significa que as ferramentas .NET podem verificar se o código é seguro. Em geral, o código seguro não acessa diretamente a memória usando ponteiros. Ele também não aloca memória bruta. Em vez disso, ele cria objetos gerenciados.

C# suporta um unsafe contexto, no qual você pode escrever código não verificável . Em um unsafe contexto, o código pode usar ponteiros, alocar e liberar blocos de memória e chamar métodos usando ponteiros de função. Código inseguro em C# não é necessariamente perigoso; é apenas um código cuja segurança não pode ser verificada.

O código não seguro tem as seguintes propriedades:

  • Métodos, tipos e blocos de código podem ser definidos como inseguros.
  • Em alguns casos, o código não seguro pode aumentar o desempenho de um aplicativo removendo as verificações de limites de matriz.
  • O código não seguro é necessário quando você chama funções nativas que exigem ponteiros.
  • O uso de código não seguro introduz riscos de segurança e estabilidade.
  • O código que contém blocos não seguros deve ser compilado com a opção de compilador AllowUnsafeBlocks.

Tipos de ponteiro

Em um contexto não seguro, um tipo pode ser um tipo de ponteiro, além de um tipo de valor, ou um tipo de referência. Uma declaração de tipo de ponteiro assume uma das seguintes formas:

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

O tipo especificado antes do * em um tipo de ponteiro é chamado de tipo referente. Somente um tipo não gerenciado pode ser um tipo referente.

Os tipos de ponteiro não herdam do objeto e não existem conversões entre os tipos de ponteiro e objecto . Além disso, boxe e unboxing não suportam ponteiros. No entanto, você pode converter entre diferentes tipos de ponteiro e entre tipos de ponteiro e tipos integrais.

Ao declarar vários ponteiros na mesma declaração, você escreve o asterisco (*) junto apenas com o tipo subjacente. Ele não é usado como um prefixo para cada nome de ponteiro. Por exemplo:

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

Um ponteiro não pode apontar para uma referência ou para uma estrutura que contém referências, porque uma referência de objeto pode ser coletada mesmo que um ponteiro esteja apontando para ela. O coletor de lixo não controla se um objeto está sendo apontado por algum tipo de ponteiro.

O valor da variável de ponteiro do tipo MyType* é o endereço de uma variável do tipo MyType. Seguem-se exemplos de declarações de tipo de ponteiro:

  • int* p: p é um ponteiro para um inteiro.
  • int** p: p é um ponteiro para um ponteiro para um inteiro.
  • int*[] p: p é uma matriz unidimensional de ponteiros para inteiros.
  • char* p: p é um ponteiro para um char.
  • void* p: p é um ponteiro para um tipo desconhecido.

O operador * de indirection de ponteiro pode ser usado para acessar o conteúdo no local apontado pela variável de ponteiro. Por exemplo, considere a seguinte declaração:

int* myVariable;

A expressão *myVariable denota a int variável encontrada no endereço contido em myVariable.

Há vários exemplos de indicações nos artigos sobre a fixed declaração. O exemplo a seguir usa a unsafe palavra-chave e a fixed instrução e mostra como incrementar um ponteiro interior. Você pode colar esse código na função Principal de um aplicativo de console para executá-lo. Esses exemplos devem ser compilados com o conjunto de opções do compilador 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
*/

Não é possível aplicar o operador indirection a um ponteiro do tipo void*. No entanto, você pode usar uma conversão para converter um ponteiro vazio em qualquer outro tipo de ponteiro e vice-versa.

Um ponteiro pode ser null. A aplicação do operador indirection a um ponteiro nulo causa um comportamento definido pela implementação.

Passar ponteiros entre métodos pode causar comportamento indefinido. Considere um método que retorna um ponteiro para uma variável local por meio de um in, outou ref parâmetro ou como o resultado da função. Se o ponteiro foi definido em um bloco fixo, a variável para a qual ele aponta não pode mais ser fixa.

A tabela a seguir lista os operadores e instruções que podem operar em ponteiros em um contexto inseguro:

Operador/Declaração Utilizar
* Executa a indireção do ponteiro.
-> Acessa um membro de uma struct através de um ponteiro.
[] Indexa um ponteiro.
& Obtém o endereço de uma variável.
++ e -- Ponteiros de incrementos e decréscimos.
+ e - Executa aritmética de ponteiro.
==, !=, , <, <=>, e>= Compara ponteiros.
stackalloc Aloca memória na pilha.
fixed Declaração Corrige temporariamente uma variável para que seu endereço possa ser encontrado.

Para obter mais informações sobre operadores relacionados a ponteiros, consulte Operadores relacionados a ponteiro.

Qualquer tipo de ponteiro pode ser implicitamente convertido em um void* tipo. Qualquer tipo de ponteiro pode receber o valor null. Qualquer tipo de ponteiro pode ser explicitamente convertido em qualquer outro tipo de ponteiro usando uma expressão de transmissão. Você também pode converter qualquer tipo integral em um tipo de ponteiro ou qualquer tipo de ponteiro em um tipo integral. Essas conversões exigem um elenco explícito.

O exemplo a seguir converte an int* em um byte*arquivo . Observe que o ponteiro aponta para o byte endereçado mais baixo da variável. Quando você incrementa sucessivamente o resultado, até o tamanho de int (4 bytes), você pode exibir os bytes restantes da variável.

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
    */
}

Buffers de tamanho fixo

Você pode usar a palavra-chave para criar um buffer com uma matriz de tamanho fixo fixed em uma estrutura de dados. Os buffers de tamanho fixo são úteis quando você escreve métodos que interoperam com fontes de dados de outros idiomas ou plataformas. O buffer de tamanho fixo pode ter quaisquer atributos ou modificadores permitidos para membros struct regulares. A única restrição é que o tipo de matriz deve ser , , , , shortint, long, sbyte, ushort, ulonguintfloatou .doublecharbytebool

private fixed char name[30];

Em código seguro, uma estrutura C# que contém uma matriz não contém os elementos da matriz. Em vez disso, o struct contém uma referência aos elementos. Você pode incorporar uma matriz de tamanho fixo em uma struct quando ela é usada em um bloco de código não seguro .

O tamanho do seguinte struct não depende do número de elementos na matriz, uma vez que pathName é uma referência:

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

Uma struct pode conter uma matriz incorporada em código não seguro. No exemplo a seguir, a fixedBuffer matriz tem um tamanho fixo. Você usa uma fixed instrução para obter um ponteiro para o primeiro elemento. Você acessa os elementos da matriz por meio desse ponteiro. A fixed instrução fixa o fixedBuffer campo de ocorrência em um local específico na memória.

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]);
    }
}

O tamanho da matriz de 128 elementos char é de 256 bytes. Os buffers char de tamanho fixo sempre levam 2 bytes por caractere, independentemente da codificação. Esse tamanho de matriz é o mesmo mesmo quando os buffers char são empacotados para métodos ou estruturas de API com CharSet = CharSet.Auto ou CharSet = CharSet.Ansi. Para obter mais informações, veja CharSet.

O exemplo anterior demonstra o acesso a fixed campos sem fixação. Outra matriz de tamanho fixo comum é a matriz bool . Os elementos em uma bool matriz têm sempre 1 byte de tamanho. bool As matrizes não são apropriadas para criar matrizes de bits ou buffers.

Os buffers de tamanho fixo são compilados com o , que instrui System.Runtime.CompilerServices.UnsafeValueTypeAttributeo Common Language Runtime (CLR) que um tipo contém uma matriz não gerenciada que pode potencialmente estourar. A memória alocada usando stackalloc também habilita automaticamente os recursos de deteção de saturação de buffer no CLR. O exemplo anterior mostra como um buffer de tamanho fixo pode existir em um unsafe structarquivo .

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

O C# gerado pelo compilador para Buffer é atribuído da seguinte forma:

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;
}

Os buffers de tamanho fixo diferem dos arrays regulares das seguintes maneiras:

  • Só pode ser utilizado num unsafe contexto.
  • Podem ser apenas campos de instância de structs.
  • São sempre vetores, ou matrizes unidimensionais.
  • A declaração deve incluir o comprimento, como fixed char id[8]. Você não pode usar fixed char id[]o .

Como usar ponteiros para copiar uma matriz de bytes

O exemplo a seguir usa ponteiros para copiar bytes de uma matriz para outra.

Este exemplo usa a palavra-chave unsafe , que permite que você use ponteiros no Copy método. A instrução fixed é usada para declarar ponteiros para as matrizes de origem e destino. A fixed instrução fixa o local das matrizes de origem e destino na memória para que elas não sejam movidas pela coleta de lixo. Os blocos de memória para as matrizes são desfixados quando o fixed bloco é concluído. Como o Copy método neste exemplo usa a palavra-chave unsafe , ele deve ser compilado com a opção de compilador AllowUnsafeBlocks .

Este exemplo acessa os elementos de ambas as matrizes usando índices em vez de um segundo ponteiro não gerenciado. A declaração dos pSource ponteiros e pTarget fixa as matrizes.

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
    */
}

Ponteiros de função

C# fornece delegate tipos para definir objetos de ponteiro de função segura. Invocar um delegado envolve instanciar um tipo derivado e fazer uma chamada de System.Delegate método virtual para seu Invoke método. Esta chamada virtual usa a callvirt instrução IL. Em caminhos de código críticos de desempenho, usar a calli instrução IL é mais eficiente.

Você pode definir um ponteiro de função usando a delegate* sintaxe. O compilador chamará a função usando a calli instrução em vez de instanciar um delegate objeto e chamar Invoke. O código a seguir declara dois métodos que usam a delegate ou a delegate* para combinar dois objetos do mesmo tipo. O primeiro método usa um System.Func<T1,T2,TResult> tipo de delegado. O segundo método usa uma delegate* declaração com os mesmos parâmetros e tipo de retorno:

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);

O código a seguir mostra como você declararia uma função local estática e invocaria o UnsafeCombine método usando um ponteiro para essa função local:

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

O código anterior ilustra várias das regras sobre a função acessada como um ponteiro de função:

  • Os ponteiros de função só podem ser declarados em um unsafe contexto.
  • Os métodos que tomam um delegate* (ou retornam um delegate*) só podem ser chamados em um unsafe contexto.
  • O & operador para obter o endereço de uma função é permitido apenas em static funções. (Esta regra aplica-se tanto às funções de membro como às funções locais).

A sintaxe tem paralelos com a declaração de delegate tipos e o uso de ponteiros. O * sufixo em delegate indica que a declaração é um ponteiro de função. Ao & atribuir um grupo de métodos a um ponteiro de função indica que a operação usa o endereço do método.

Você pode especificar a convenção de chamada para um delegate* usando as palavras-chave managed e unmanaged. Além disso, para unmanaged ponteiros de função, você pode especificar a convenção de chamada. As declarações a seguir mostram exemplos de cada uma delas. A primeira declaração usa a managed convenção de chamada, que é o padrão. Os quatro seguintes usam uma convenção de unmanaged chamada. Cada uma especifica uma das convenções de chamada ECMA 335: Cdecl, Stdcall, Fastcall, ou Thiscall. A última declaração usa a unmanaged convenção de chamada, instruindo o CLR a escolher a convenção de chamada padrão para a plataforma. O CLR escolherá a convenção de chamada em tempo de execução.

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);

Você pode saber mais sobre ponteiros de função na especificação do recurso Ponteiro de função.

Especificação da linguagem C#

Para obter mais informações, consulte o capítulo Código não seguro da especificação da linguagem C#.