Partilhar via


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

A maior parte do código C# que escreves é código verificavelmente seguro. Código verificavelmente seguro significa que as ferramentas .NET podem verificar que o código é seguro. Em geral, o código seguro não acede diretamente à memória usando ponteiros. Ele também não aloca memória bruta. Em vez disso, ele cria objetos gerenciados.

A referência da linguagem C# documenta a versão mais recentemente lançada da linguagem C#. Contém também documentação inicial para funcionalidades em pré-visualizações públicas para o próximo lançamento linguístico.

A documentação identifica qualquer funcionalidade introduzida pela primeira vez nas últimas três versões da língua ou em pré-visualizações públicas atuais.

Sugestão

Para saber quando uma funcionalidade foi introduzida pela primeira vez em C#, consulte o artigo sobre o histórico de versões da linguagem C#.

C# suporta um contexto unsafe, no qual é possível escrever código não verificável. Num unsafe contexto, o código pode usar ponteiros, alocar e libertar 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:

  • Pode definir métodos, tipos e blocos de código como inseguros.
  • Em alguns casos, o código não seguro pode aumentar o desempenho de um aplicativo, permitindo o acesso direto à memória por meio de ponteiros para evitar verificações de limites de matriz.
  • Usas código inseguro para chamar funções nativas que requerem ponteiros.
  • O uso de código não seguro introduz riscos de segurança e estabilidade.
  • Deve adicionar a opção do compilador AllowUnsafeBlocks para compilar o código que contém blocos inseguros.

Para obter informações sobre práticas recomendadas para código não seguro em C#, consulte Práticas recomendadas de código não seguro.

Tipos de ponteiro

Num contexto inseguro, um tipo pode ser um tipo apontador, além de um tipo de valor ou 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 que especificas antes do * num tipo de ponteiro é o tipo referente.

Os tipos de ponteiro não herdam do objeto, e não existem conversões entre tipos de ponteiro e object. Além disso, boxing e unboxing não oferecem suporte a ponteiros. No entanto, é possível converter entre diferentes tipos de ponteiros e entre tipos de ponteiros e tipos integrais.

Quando declaras múltiplos ponteiros na mesma declaração, escreve o asterisco (*) juntamente 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#

O coletor de lixo não controla se um objeto está sendo apontado por algum tipo de ponteiro. Se o referente for um objeto no heap gerido (incluindo variáveis locais capturadas por expressões lambda ou delegados anónimos), deve fixar o objeto enquanto o ponteiro for utilizado.

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 caracter.
  • void* p: p é um ponteiro para um tipo desconhecido.

Pode usar o operador * de direção do ponteiro para aceder ao conteúdo na localização apontada pela variável do apontador. 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 indicações nos artigos sobre a declaraçã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. Esses exemplos devem ser compilados com o AllowUnsafeBlocks conjunto de opções do compilador.

// 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 de indireção a um ponteiro do tipo void*. No entanto, pode usar um cast para converter um ponteiro void 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 funções pode causar comportamento indefinido. Considere um método que retorna um ponteiro para uma variável local por meio de um parâmetro in, outou ref ou como resultado da função. Se o ponteiro foi definido em um bloco fixo, a variável para a qual ele aponta pode não ser mais fixa.

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

Operador/Declaração Utilização
* Executa a operação de indireção de 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 a aritmética de ponteiros.
==, !=, <, >, <=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 ponteiros.

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

O exemplo a seguir converte um int* em um byte*. 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: {number}");

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

Buffers de tamanho fixo

Use a fixed palavra-chave para criar um buffer com um array de tamanho fixo numa 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 que sejam permitidos para membros regulares de uma struct. A única restrição é que o tipo de matriz deve ser bool, byte, char, short, int, long, sbyte, ushort, uint, ulong, floatou double.

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 um struct quando ela é usada em um bloco de código inseguro.

O tamanho dos seguintes 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 matriz fixedBuffer tem um tamanho fixo. Você usa uma instrução fixed para obter um ponteiro para o primeiro elemento. Você acessa os elementos da matriz por meio desse ponteiro. A instrução fixed fixa o campo de 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. Os buffers de de de tamanho fixo sempre levam 2 bytes por caractere, independentemente da codificação. Esse tamanho de matriz é o mesmo, mesmo quando os buffers de caracteres são convertidos para métodos de API ou estruturas com CharSet = CharSet.Auto ou CharSet = CharSet.Ansi. Para obter mais informações, consulte CharSet.

O exemplo anterior demonstra o acesso aos campos fixed sem os fixar. Outra matriz de tamanho fixo comum é a matriz bool. Os elementos em uma matriz bool têm sempre 1 byte de tamanho. bool arrays não são adequadas para criar arrays de bits ou buffers.

Os buffers de tamanho fixo são compilados com o System.Runtime.CompilerServices.UnsafeValueTypeAttribute, que instrui o Common Language Runtime (CLR) que um tipo contém uma matriz não administrada que pode potencialmente transbordar. A memória alocada através do stackalloc também ativa automaticamente funcionalidades de deteção de overrun de buffer no CLR. O exemplo anterior mostra como um buffer de tamanho fixo poderia existir num unsafe struct.

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ó podes usá-los num unsafe contexto.
  • Só podem ser 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[].

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 método Copy. A instrução fixa declara ponteiros para os arrays de origem e destino. A instrução fixedpinos o local das matrizes de origem e destino na memória para que a coleta de lixo não mova as matrizes. O fixed bloco fixa os blocos de memória dos arrays no âmbito do bloco. Como o Copy método neste exemplo usa a unsafe palavra-chave, deve compilá-lo usando a opção do compilador AllowUnsafeBlocks .

Este exemplo acede aos elementos de ambos os arrays usando índices em vez de um segundo ponteiro não gerido. A declaração dos ponteiros pSource e pTarget bloqueia 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 ponteiros de função seguros. Invocar um delegado envolve instanciar um tipo derivado de System.Delegate e fazer uma chamada de método virtual para seu método Invoke. Esta chamada virtual usa a instrução callvirt IL. Em caminhos de código críticos de desempenho, usar a instrução calli IL é mais eficiente.

Podes definir um ponteiro de função usando a delegate* sintaxe. O compilador chama 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 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 unsafe T UnsafeCombine<T>(delegate*<T, T, T> combinator, T left, T right) => 
    combinator(left, right);

O código seguinte mostra como se declara uma função local estática e se invoca o UnsafeCombine método usando um ponteiro para essa função local:

int product = 0;
unsafe
{
    static int localMultiply(int x, int y) => x * y;
    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:

  • Só podes declarar ponteiros de função num unsafe contexto.
  • Só podes chamar métodos que tomam um delegate* (ou devolvam um delegate*) num 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 membros como às funções locais.

A sintaxe tem paralelos com a declaração de delegate tipos e o uso de ponteiros. O sufixo * no 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 toma o endereço do método.

Pode especificar a convenção de chamada para a 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 uma delas. A primeira declaração usa a convenção de chamada managed, que é o padrão. As próximas quatro utilizam uma convenção de chamada do tipo unmanaged. Cada uma especifica uma das convenções de chamada ECMA 335: Cdecl, Stdcall, Fastcallou 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 escolhe a convenção de chamada durante o tempo de execução.

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

Você pode aprender mais sobre ponteiros de função na especificação de recurso ponteiros de função.

Especificação da linguagem C#

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

Consulte também