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

La mayor parte del código de C# que se escribe es "código seguro comprobable". El código seguro comprobable 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. En su lugar, crea objetos administrados.

C# admite un contexto unsafe, en el que se puede escribir código no comprobable. En un contexto 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; solo es código cuya seguridad no se puede comprobar.

El código no seguro tiene las propiedades siguientes:

  • Los métodos, tipos y bloques de código se pueden definir como no seguros.
  • En algunos casos, el código no seguro puede aumentar el rendimiento de la aplicación al eliminar las comprobaciones de límites de matriz.
  • El código no seguro es necesario al llamar a funciones nativas que requieren punteros.
  • El código no seguro presenta riesgos para la seguridad y la estabilidad.
  • El código que contenga bloques no seguros deberá 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 de * en un tipo de puntero se denomina tipo referente. Solo un tipo no administrado puede ser un tipo de referente.

Los tipos de puntero no heredan de object y no existe ninguna conversión 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 en cada nombre de puntero. Por ejemplo:

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

Un puntero no puede señalar a una referencia ni a un struct que contenga referencias, porque una referencia de objeto puede recolectarse como elemento no utilizado aunque haya un puntero que la señale. El recolector de elementos no utilizados no realiza un seguimiento de si algún tipo de puntero señala a un objeto.

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 tipos 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 * puede usarse para acceder al contenido de la ubicación señalada por la variable de puntero. Por ejemplo, consideremos 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 ejecutarla. Estos ejemplos se deben compilar 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 NULL da como resultado un comportamiento definido por la implementación.

Pasar punteros entre métodos puede provocar un comportamiento no definido. Valore la posibilidad de usar un método que devuelva un puntero a una variable local mediante un parámetro in, out o ref, o bien como resultado de la función. Si el puntero se estableció en un bloque fijo, es posible que la variable a la que señala ya no sea fija.

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

Operador/Instrucción Usar
* Realiza el direccionamiento indirecto del puntero.
-> Obtiene acceso a un miembro de un struct 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 Fija provisionalmente una variable para que pueda encontrarse su dirección.

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

Cualquier tipo de puntero se puede convertir implícitamente en un tipo void*. Se puede asignar el valor null a cualquier tipo de puntero. 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 convertir cualquier tipo entero en un tipo de puntero o cualquier tipo de puntero en un tipo entero. Estas conversiones requieren una conversión explícita.

El siguiente ejemplo convierte un valor de tipo int* en byte*. Tenga en cuenta que el puntero señala al byte dirigido más bajo de la variable. Cuando incrementa el resultado sucesivamente, 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 resultan ú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, float o double.

private fixed char name[30];

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

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

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

Un struct puede contener una matriz insertada en el código no seguro. En el siguiente ejemplo, la matriz fixedBuffer tiene un tamaño fijo. Se usa una instrucción fixed para establecer un puntero al primer elemento. Se accede a los elementos de la matriz mediante este puntero. La instrucción fixed ancla el campo de instancia fixedBuffer a una ubicación concreta 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 256 bytes. Los búferes char de tamaño fijo siempre admiten 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 las estructuras o métodos de API con CharSet = CharSet.Auto o CharSet = CharSet.Ansi. Para obtener más información, vea 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 bool. Los elementos de una matriz bool siempre tienen 1 byte de tamaño. 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ódigo de C# generado por el compilador para Buffer se atribuye de la siguiente 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;
}

Los búferes de tamaño fijo son diferentes de las matrices normales en los siguientes puntos:

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

Procedimiento para 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 unsafe, que permite el uso de punteros en el método Copy. La instrucción fixed se usa para declarar punteros a las matrices de origen y destino. La instrucción fixedancla la ubicación de las matrices de origen y destino en memoria, para que no puedan ser desplazadas por la recolección de elementos no utilizados. Estos bloques de memoria para las matrices se desanclan cuando finaliza el bloque fixed. Como el método Copy de este ejemplo usa la palabra clave unsafe, se debe compilar con la opción del compilador AllowUnsafeBlocks.

Este ejemplo accede a los elementos de ambas matrices con índices en lugar de con 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. La invocación de un delegado implica la creación de instancias de un tipo derivado de System.Delegate y la realización de una llamada al método virtual a su método Invoke. Esta llamada virtual utiliza la instrucción de IL callvirt. En las rutas de acceso de código crítico para el rendimiento, el uso de la instrucción de IL calli es más eficaz.

Puede definir un puntero de función mediante la sintaxis delegate*. El compilador llamará a la función mediante la instrucción calli en lugar de crear una instancia de un objeto delegate y llamar a Invoke. En el código siguiente se declaran dos métodos que usan delegate o 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 el tipo de valor devuelto:

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

En el código siguiente se muestra cómo se declara una función local estática y se invoca el método UnsafeCombine con un puntero a esa función local:

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

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

  • Los punteros de función solo se pueden declarar en un contexto unsafe.
  • Los métodos que toman un tipo de delegate* (o devuelven un tipo de delegate*) solo se pueden llamar en un contexto unsafe.
  • Para obtener la dirección de una función, el operador & solo se permite en funciones static. Esta regla se aplica a las funciones miembro y a las funciones locales.

La sintaxis muestra paralelismos con la declaración de tipos de delegate y el uso de punteros. El sufijo * de delegate indica que la declaración es un puntero de función. El operador &, 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 puntero 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. En las siguientes declaraciones, se muestran ejemplos de cada una. La primera declaración usa la convención de llamada managed, que es la predeterminada. Las cuatro siguientes usan una convención de llamada unmanaged. Cada una especifica una de las convenciones de llamada de ECMA 335: Cdecl, Stdcall, Fastcall o Thiscall. La última declaración usa la convención de llamada unmanaged, que indica al CLR que elija la convención de llamada predeterminada para la plataforma. CLR elegirá la convención de llamada en tiempo de ejecución.

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

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, vea el capítulo Código no seguro de la Especificación del lenguaje C#.