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

A maior parte do código C# que você escreve é "código seguro verificável". Código seguro verificável significa que as ferramentas do .NET podem confirmar que 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.

O C# dá suporte a um contexto unsafe em que você pode escrever códigos não verificáveis. Em um contexto unsafe, o código pode usar ponteiros, alocar e liberar blocos de memória e chamar métodos usando ponteiros de função. O código não seguro 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 propriedades a seguir:

  • Blocos de código, tipos e métodos podem ser definidos como não seguros.
  • 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.
  • Usar o código não seguro apresenta riscos de segurança e estabilidade.
  • O código que contém blocos não seguros deve ser compilado com a opção do compilador AllowUnsafeBlocks.

Tipos de ponteiro

Em um contexto não seguro, os tipos podem ser de ponteiro, além de tipo de valor ou tipo de referência. Uma declaração de tipo de ponteiro usa 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 referent. Somente um tipo não gerenciado pode ser um tipo referent.

Os tipos de ponteiro não são herdados de object e não há conversão entre tipos de ponteiro e object. Além disso, as conversões boxing e unboxing não oferecem suporte a ponteiros. No entanto, você pode converter entre diferentes tipos de ponteiro e tipos de ponteiro e tipos integrais.

Ao declarar vários ponteiros na mesma declaração, você escreve o asterisco (*) juntamente com o tipo subjacente apenas. 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 um struct que contenha referências, pois uma referência de objeto pode ser coletada como lixo mesmo se um ponteiro estiver apontando para ela. O coletor de lixo não se dá conta de que um objeto está sendo apontado por qualquer tipo de ponteiro.

O valor da variável de ponteiro do tipo MyType* é o endereço de uma variável do tipo MyType. Estes são exemplos de declarações de tipos 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 caractere.
  • void* p: p é um ponteiro para um tipo desconhecido.

O operador de indireção 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 variável int encontrada no endereço contido em myVariable.

Há vários exemplos de ponteiros nos artigos sobre a instrução fixed. O exemplo a seguir usa a palavra-chave unsafe e a instrução fixed 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. Estes exemplos precisam ser compilados com o conjunto de opções do compilador AllowUnsafeBlocks.

// Normal pointer to an object.
int[] a = new int[5] { 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
*/

Você não pode aplicar o operador de indireção para um ponteiro do tipo void*. No entanto, você pode usar uma conversão para converter um ponteiro nulo em qualquer outro tipo de ponteiro e vice-versa.

Um ponteiro pode ser null. Aplicar o operador de indireção a um ponteiro nulo causa um comportamento definido por implementação.

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

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

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

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

Qualquer tipo de ponteiro pode ser convertido implicitamente em um tipo void*. Qualquer tipo de ponteiro pode ser atribuído ao valor null. Qualquer tipo de ponteiro pode ser convertido explicitamente em qualquer outro tipo de ponteiro usando uma expressão de conversão. Também é possível converter qualquer tipo integral em um tipo de ponteiro ou qualquer tipo de ponteiro em um tipo integral. Essas conversões exigem uma conversão explícita.

O exemplo a seguir converte um int* em um byte*. Observe que o ponteiro aponta para o menor byte endereçado da variável. Quando você incrementar sucessivamente o resultado, até o tamanho de int (4 bytes), você poderá 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 fixed palavra-chave para criar um buffer com uma matriz de tamanho fixo em uma estrutura de dados. Buffers de tamanho fixo são úteis quando você escreve métodos que interoperam com fontes de dados de outras linguagens ou plataformas. O buffer de tamanho fixo pode usar todos os atributos ou modificadores permitidos para membros de struct regulares. A única restrição é que o tipo da matriz deve ser bool, byte, char, short, int, long, sbyte, ushort, uint, ulong, float ou double.

private fixed char name[30];

No código de seguro, um struct 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 inserir uma matriz de tamanho fixo em um struct quando ele é usado em um bloco de código não seguro.

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

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

Um struct pode conter uma matriz inserida em código não seguro. No exemplo a seguir, a matriz fixedBuffer tem tamanho fixo. Você usa uma fixed instrução para obter um ponteiro para o primeiro elemento. Os elementos da matriz são acessados por este ponteiro. A instrução fixed fixa o campo da instância fixedBuffer 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 char de 128 elementos é 256 bytes. Buffers char de tamanho fixo sempre levam 2 bytes por caractere, independentemente da codificação. Esse tamanho de matriz é igual até mesmo quando os buffers de char passam por marshaling para structs ou métodos de API com CharSet = CharSet.Auto ou CharSet = CharSet.Ansi. Para obter mais informações, consulte CharSet.

O exemplo anterior demonstra o acesso a campos fixed sem fixação, que estão disponíveis do C# 7.3 em diante.

Outra matriz de tamanho fixo comum é a matriz bool. Os elementos de uma matriz bool tem sempre um byte de tamanho. Matrizes bool não são adequadas para criar buffers ou matrizes de bits.

Buffers de tamanho fixo são compilados com o System.Runtime.CompilerServices.UnsafeValueTypeAttributeCLR (common language runtime) que um tipo contém uma matriz não gerenciada que pode potencialmente transbordar. A memória alocada usando stackalloc também habilita automaticamente os recursos de detecção de sobrecarga de buffer no CLR. O exemplo anterior mostra como um buffer de tamanho fixo poderia existir em um unsafe struct.

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

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

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 das matrizes regulares das seguintes maneiras:

  • Só pode ser usado em contextos unsafe.
  • Pode ser apenas campos de instância de structs.
  • Eles são sempre vetores ou matrizes unidimensionais.
  • A declaração deve incluir o comprimento, como fixed char id[8]. Não é possível usar fixed char id[].

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

Este exemplo acessa os elementos das duas matrizes usando índices em vez de um segundo ponteiro não gerenciado. A declaração dos ponteiros pSource e pTarget fixa as matrizes. Esse recurso está disponível começando com o C# 7.3.

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

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

Você pode definir um ponteiro de função usando a sintaxe delegate*. O compilador chamará a função usando a instrução calli em vez de instanciar um objeto delegate e chamar Invoke. O código a seguir declara dois métodos que usam um delegate ou um delegate* para combinar dois objetos do mesmo tipo. O primeiro método usa um tipo de delegado System.Func<T1,T2,TResult>. O segundo método usa uma declaração delegate* 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 seguinte código mostra como você declararia uma função local estática e invocaria o método UnsafeCombine usando um ponteiro para tal 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 da função acessada como um ponteiro de função:

  • Ponteiros de função só podem ser declarados em contextos unsafe.
  • Métodos que aceitam delegate* (ou retornam delegate*) só podem ser chamados em contextos unsafe.
  • O operador & para obter o endereço de uma função é permitido somente em funções static. (Essa regra se aplica a funções membro e funções locais).

A sintaxe tem paralelos com a declaração de tipos delegate e o uso de ponteiros. O sufixo * em delegate indica que a declaração é um ponteiro de função. 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 delegate* usando as palavras-chave managed e unmanaged. Além disso, para ponteiros de função unmanaged, você pode especificar a convenção de chamada. As declarações a seguir mostram exemplos de cada um. A primeira declaração usa a convenção de chamada managed, que é o padrão. As quatro a seguir usam a convenção de chamada unmanaged. Cada uma especifica uma das convenções de chamada do ECMA 335: Cdecl, Stdcall, Fastcall ou Thiscall. A última declaração usa a convenção de chamada unmanaged, 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 proposta Ponteiro de função para C# 9.0.

Especificação da linguagem C#

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