Compartir a través de


Código no seguro, tipos de puntero y punteros de función

La mayoría del código de C# que escribe es "código seguro verificable". código seguro verificable significa que las herramientas de .NET pueden comprobar que el código es seguro. En general, el código seguro no accede directamente a la memoria mediante punteros. Tampoco asigna memoria sin procesar. Crea objetos administrados en su lugar.

C# admite un contexto unsafe, en el que puede escribir código no comprobable. En un contexto de unsafe, el código puede usar punteros, asignar y liberar bloques de memoria y llamar a métodos mediante punteros de función. El código no seguro en C# no es necesariamente peligroso; es solo código cuya seguridad no se puede comprobar.

El código no seguro tiene las siguientes propiedades:

  • Los métodos, los tipos y los bloques de código se pueden definir como no seguros.
  • En algunos casos, el código no seguro puede aumentar el rendimiento de una aplicación quitando las comprobaciones de límites de matriz.
  • El código no seguro es necesario cuando se llama a funciones nativas que requieren punteros.
  • El uso de código no seguro presenta riesgos de seguridad y estabilidad.
  • El código que contiene bloques no seguros debe compilarse con la opción del compilador AllowUnsafeBlocks.

Tipos de puntero

En un contexto no seguro, un tipo puede ser un tipo de puntero, además de un tipo de valor o un tipo de referencia. Una declaración de tipos de puntero toma una de las siguientes formas:

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

El tipo especificado antes del * en un tipo de puntero se denomina tipo tipo de referencia.

Los tipos de puntero no heredan de objeto y no existen conversiones entre tipos de puntero y object. Además, las conversiones boxing y unboxing no admiten punteros. Sin embargo, puede realizar la conversión entre diferentes tipos de puntero y entre tipos de puntero y tipos enteros.

Cuando declare varios punteros en la misma declaración, únicamente debe escribir el asterisco (*) con el tipo subyacente. No se usa como prefijo para cada nombre de puntero. Por ejemplo:

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

El recolector de elementos no utilizados no realiza un seguimiento de si algún tipo de puntero señala a un objeto. Si el origen de referencia es un objeto del montón administrado (incluidas las variables locales capturadas por expresiones lambda o delegados anónimos), el objeto debe estar anclado mientras se use el puntero.

El valor de la variable de puntero de tipo MyType* es la dirección de una variable de tipo MyType. A continuación se muestran ejemplos de declaraciones de tipo de puntero:

  • int* p: p es un puntero a un entero.
  • int** p: p es un puntero a un puntero a un entero.
  • int*[] p: p es una matriz unidimensional de punteros a enteros.
  • char* p: p es un puntero a un valor char.
  • void* p: p es un puntero a un tipo desconocido.

El operador de direccionamiento indirecto del puntero * se puede usar para acceder al contenido en la ubicación a la que apunta la variable de puntero. Por ejemplo, considere la siguiente declaración:

int* myVariable;

La expresión *myVariable denota la variable int que se encuentra en la dirección contenida en myVariable.

Hay varios ejemplos de punteros en los artículos sobre la instrucción fixed. En el ejemplo siguiente se usa la palabra clave unsafe y la instrucción fixed y se muestra cómo incrementar un puntero interior. Puede pegar este código en la función Main de una aplicación de consola para ejecutarlo. Estos ejemplos deben compilarse con el conjunto de opciones del 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
*/

No se puede aplicar el operador de direccionamiento indirecto a un puntero de tipo void*. Sin embargo, es posible usar una conversión para convertir un puntero void en cualquier otro tipo de puntero y viceversa.

Un puntero puede ser null. La aplicación del operador de direccionamiento indirecto a un puntero nulo provoca un comportamiento definido por la implementación.

Pasar punteros entre métodos puede provocar un comportamiento no definido. Considere un método que devuelve un puntero a una variable local a través de los parámetros in, outo ref, o como resultado de la función. Si el puntero se estableció en un bloque fijo, es posible que la variable a la que apunta ya no esté fija.

En la tabla siguiente se enumeran los operadores e instrucciones que pueden funcionar en punteros en un contexto no seguro:

Operador/Declaración Usar
* Realiza el direccionamiento del puntero de manera indirecta.
-> Obtiene acceso a un miembro de una estructura a través de un puntero.
[] Indiza un puntero.
& Obtiene la dirección de una variable.
++ y -- Incrementa y disminuye los punteros.
+ y - Realiza aritmética con punteros.
==, !=, <, >, <=y >= Compara los punteros.
stackalloc Asigna memoria en la pila.
Instrucción fixed Corrige temporalmente una variable para que se pueda encontrar su dirección.

Para obtener más información sobre los operadores relacionados con el puntero, consulte Operadores relacionados con el puntero.

Cualquier tipo de puntero se puede convertir implícitamente en un tipo de void*. A cualquier tipo de puntero se le puede asignar el valor null. Cualquier tipo de puntero se puede convertir explícitamente en cualquier otro tipo de puntero mediante una expresión de conversión. También puede transformar cualquier tipo integral a un tipo de puntero o cualquier tipo de puntero a un tipo integral. Estas conversiones requieren una conversión explícita.

En el ejemplo siguiente se convierte un int* en un byte*. Observe que el puntero apunta al byte direccionado más bajo de la variable. Al incrementar sucesivamente el resultado, hasta el tamaño de int (4 bytes), puede mostrar los bytes restantes de la variable.

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

Búferes de tamaño fijo

Puede usar la palabra clave fixed para crear un búfer con una matriz de tamaño fijo en una estructura de datos. Los búferes de tamaño fijo son útiles al escribir métodos que interoperan con orígenes de datos de otros lenguajes o plataformas. El búfer de tamaño fijo puede tomar cualquiera de los atributos o modificadores permitidos para los miembros de struct normales. La única restricción es que el tipo de matriz debe ser bool, byte, char, short, int, long, sbyte, ushort, uint, ulong, floato double.

private fixed char name[30];

En el código seguro, una estructura de C# que contiene una matriz no contiene los elementos de la matriz. La estructura contiene una referencia a los elementos en su lugar. Puede insertar una matriz de tamaño fijo en una estructura cuando se usa en un bloque de código inseguro .

El tamaño del siguiente struct no depende del número de elementos de la matriz, ya que pathName es una referencia:

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

Una estructura puede contener una matriz incrustada en código no seguro. En el ejemplo siguiente, la matriz fixedBuffer tiene un tamaño fijo. Se usa una instrucción fixed para establecer un puntero al primer elemento. Puede acceder a los elementos de la matriz a través de este puntero. La instrucción fixed ancla el campo de instancia de fixedBuffer a una ubicación específica en la memoria.

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

El tamaño de la matriz char de 128 elementos es de 256 bytes. Los búferes de tamaño fijo de caracteres siempre ocupan 2 bytes por carácter, independientemente de la codificación. Este tamaño de matriz es el mismo, incluso cuando se calculan las referencias de los búferes char a los métodos API o structs con CharSet = CharSet.Auto o CharSet = CharSet.Ansi. Para obtener más información, consulte CharSet.

En el ejemplo anterior se muestra el acceso a campos fixed sin anclar. Otra matriz de tamaño fijo común es la matriz de bool. Los elementos de una matriz de bool siempre tienen un tamaño de 1 byte. Las matrices bool no son adecuadas para crear matrices de bits o búferes.

Los búferes de tamaño fijo se compilan con el atributo System.Runtime.CompilerServices.UnsafeValueTypeAttribute, que indica a Common Language Runtime (CLR) que un tipo contiene una matriz no administrada que puede provocar un desbordamiento. La memoria asignada mediante stackalloc también habilita automáticamente las características de detección de saturación del búfer en CLR. En el ejemplo anterior se muestra cómo podría existir un búfer de tamaño fijo en un unsafe struct.

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

El C# generado por el compilador para Buffer se atribuye de la siguiente manera:

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

Los búferes de tamaño fijo difieren de las matrices normales de las maneras siguientes:

  • Solo se pueden usar en un contexto unsafe.
  • Solo pueden ser campos de instancia de las estructuras.
  • Siempre son vectores o matrices unidimensionales.
  • La declaración debe incluir la longitud, como fixed char id[8]. No puede usar fixed char id[].

Cómo usar punteros para copiar una matriz de bytes

En el ejemplo siguiente se usan punteros para copiar bytes de una matriz a otra.

En este ejemplo se usa la palabra clave no segura, que permite usar punteros en el método Copy. La instrucción fixed se usa para declarar punteros a las matrices de origen y destino. La declaración fixedfija la ubicación de las matrices de origen y destino en la memoria para que la recolección de basura no desplace dichas matrices. Estos bloques de memoria para las matrices se desanclan cuando finaliza el bloque fixed. Dado que el método Copy de este ejemplo usa la palabra clave unsafe, debe compilarse con la opción del compilador AllowUnsafeBlocks.

En este ejemplo se obtiene acceso a los elementos de ambas matrices mediante índices en lugar de a un segundo puntero no administrado. La declaración de los punteros pSource y pTarget ancla las matrices.

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

Punteros de función

C# proporciona tipos delegate para definir objetos de puntero de función seguros. Invocar un delegado implica instanciar un tipo derivado de System.Delegate y realizar una llamada de método virtual al método Invoke de este. Esta llamada virtual utiliza la instrucción de IL callvirt. En los caminos de código críticos para el rendimiento, el uso de la instrucción IL calli es más eficaz.

Puede definir un puntero de función mediante la sintaxis delegate*. El compilador llama a la función mediante la instrucción calli en lugar de crear instancias de un objeto delegate y llamar a Invoke. El código siguiente declara dos métodos que usan un delegate o un delegate* para combinar dos objetos del mismo tipo. El primer método usa un tipo de delegado System.Func<T1,T2,TResult>. El segundo método usa una declaración delegate* con los mismos parámetros y tipo de valor devuelto:

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

El código siguiente muestra cómo declararía una función local estática e invocaría el método UnsafeCombine mediante un puntero a esa función local:

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

En el código anterior se muestran varias de las reglas de la función a las que se accede como puntero de función:

  • Los punteros de función solo se pueden declarar en un contexto unsafe.
  • Los métodos que aceptan un delegate* (o devuelven un delegate*) solo se pueden invocar en un contexto de unsafe.
  • El operador & para obtener la dirección de una función solo se permite en static funciones. (Esta regla se aplica tanto a las funciones miembro como a las funciones locales).

La sintaxis tiene paralelos con la declaración de tipos delegate y el uso de punteros. El sufijo * en delegate indica que la declaración es un puntero de función . El & al asignar un grupo de métodos a un puntero de función indica que la operación toma la dirección del método.

Puede especificar la convención de llamada para un delegate* mediante las palabras clave managed y unmanaged. Además, en el caso de los punteros de función unmanaged, puede especificar la convención de llamada. Las declaraciones siguientes muestran ejemplos de cada uno. La primera declaración usa la convención de llamada managed, que es el valor predeterminado. Las cuatro siguientes usan una convención de llamada unmanaged. Cada especifica una de las convenciones de llamada de ECMA 335: Cdecl, Stdcall, Fastcallo Thiscall. La última declaración usa la convención de llamada unmanaged, lo que indica al CLR que elija la convención de llamada predeterminada para la plataforma. CLR elige la convención de llamada en tiempo de ejecución.

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

Puede obtener más información sobre los punteros de función en la especificación de características puntero de función.

Especificación del lenguaje C#

Para obtener más información, consulte el capítulo código no seguro de la especificación del lenguaje C# .