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 significa que .NET herramientas 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.

La referencia del lenguaje C# documenta la versión publicada más recientemente del lenguaje C#. También contiene documentación inicial sobre las características de las versiones preliminares públicas de la próxima versión del lenguaje.

La documentación identifica cualquier característica introducida por primera vez en las últimas tres versiones del idioma o en las versiones preliminares públicas actuales.

Sugerencia

Para buscar cuándo se introdujo por primera vez una característica en C#, consulte el artículo sobre el historial de versiones del lenguaje C#.

C# también admite un unsafe contexto en el que se puede escribir código no comprobado . El código no seguro no es necesariamente peligroso; es solo código cuya seguridad no se puede comprobar mediante herramientas de .NET. El código no seguro se usa para llamar a funciones nativas que requieren punteros y, en algunos casos, para mejorar el rendimiento a través del acceso directo a la memoria que evita las comprobaciones de límites de matriz. El código no seguro también presenta riesgos de seguridad y estabilidad. Para compilar código que contiene un unsafe contexto, agregue la opción del compilador AllowUnsafeBlocks .

C# define dos modelos para lo que cuenta como código no seguro: el modelo original y un modelo de seguridad de memoria actualizado que se encuentra en versión preliminar en C# 15 y .NET 11. Para obtener información sobre cómo difieren los dos modelos, consulte Dos modelos para código no seguro.

Para obtener información sobre los procedimientos recomendados para el código no seguro en C#, consulte Procedimientos recomendados de código no seguro.

Dos modelos para código no seguro

C# define dos modelos para código no seguro. El modelo en vigor determina qué operaciones requieren un unsafe contexto y cómo afecta el unsafe modificador a los autores de llamadas.

  • Modelo no seguro original: el unsafe contexto abarca la existencia de características de puntero. Declara un tipo de puntero, toma la dirección de una variable, desreferencia un puntero, convierte una stackalloc expresión en un puntero o se aplica sizeof a un tipo arbitrario solo dentro de un unsafe contexto. (Se permite una stackalloc expresión asignada a o ReadOnlySpan<T>Span<T> en código seguro). El unsafe modificador de un tipo, un miembro o un bloque establece ese contexto, pero no impone ninguna obligación a los autores de llamadas. C# 1.0 introdujo este modelo y sigue siendo el valor predeterminado.
  • Modelo de seguridad de memoria actualizado: el unsafe contexto abarca las operaciones que acceden a la memoria que el tiempo de ejecución no administra. La existencia de un puntero no es segura; la desreferencia de un puntero es . El unsafe modificador de un miembro se convierte en un contrato que propaga la obligación de auditar la seguridad al autor de la llamada. Este modelo está en versión preliminar en C# 15 y .NET 11.

En la tabla siguiente se comparan las operaciones que requieren un unsafe contexto en cada modelo.

Operación Modelo original Modelo actualizado
Declarar un tipo de puntero o tomar una dirección con & Requiere unsafe Permitido en código seguro
Instrucción fixed Requiere unsafe Permitido en código seguro
Convertir una stackalloc expresión en un puntero Requiere unsafe Permitido en código seguro
Operador sizeof en cualquier tipo no administrado Requiere unsafe Permitido en código seguro
Direccionamiento indirecto de puntero (*p), acceso a miembros (p->m) o acceso a elementos (p[i]) Requiere unsafe Requiere unsafe
Invocación del puntero de función Requiere unsafe Requiere unsafe
Acceso a elementos en un búfer de tamaño fijo Requiere unsafe Requiere unsafe
Llamada a un miembro marcado unsafe No se necesita ningún llamador Requiere unsafe

Para probar el modelo actualizado, use el SDK de .NET 11 (en versión preliminar) y establezca la LangVersion opción previewdel compilador en . Las relajaciones del puntero se aplican cada vez que se compila con el compilador de C# 15 y la versión del preview lenguaje. El cumplimiento total, incluidas las obligaciones del autor de la llamada y la participación del ensamblado, todavía está en desarrollo. Para obtener más información, consulte El modelo de seguridad de memoria actualizado (versión preliminar).

El modelo no seguro original

En el modelo original, la unsafe palabra clave establece un contexto no seguro en un tipo, un miembro o un bloque, y ese contexto desbloquea las características de puntero descritas en las secciones siguientes. El unsafe modificador solo cambia lo que puede hacer el código marcado; no hace ningún requisito en los autores de llamadas. Para compilar cualquiera de estos ejemplos, establezca 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 es el tipo de referencia.

Los tipos de puntero no heredan del objeto y no existen conversiones entre los 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.

Al declarar varios punteros en la misma declaración, escriba el asterisco (*) junto con el tipo subyacente solo. 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 referente es un objeto del montón administrado (incluidas las variables locales capturadas por expresiones lambda o delegados anónimos), debe anclar el objeto siempre y cuando 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.

Puede usar el operador * de direccionamiento indirecto de puntero 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. Puede convertir explícitamente cualquier tipo de puntero a 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: {number}");

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

Búferes de tamaño fijo

Las matrices son tipos de referencia, por lo que en código seguro, un campo de estructura que es una matriz almacena solo una referencia a los elementos de la matriz, no a los propios elementos. El tamaño de lo 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;
}

Para almacenar el contenido de la matriz dentro de la propia estructura, use la fixed palabra clave para declarar un búfer de tamaño fijo. La fixed palabra clave requiere un unsafe contexto. 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. Un búfer de tamaño fijo puede tomar cualquier atributo o modificador que se permita para los miembros de estructura normales. La única restricción es que el tipo de matriz debe ser bool, byte, shortcharlongint, , uintsbyteushort, ulong, o : floatdouble

private fixed char name[30];

En el ejemplo siguiente, la matriz fixedBuffer tiene un tamaño fijo. Use una fixed instrucción para obtener un puntero al primer elemento y, a continuación, acceda a los elementos de la matriz a través de ese puntero. La fixed instrucción ancla el fixedBuffer campo de instancia 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 puede usarlos en un unsafe contexto.
  • Solo pueden ser campos de instancia de 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[].

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 delegate* sintaxis . El compilador llama a la función mediante la calli instrucción en lugar de crear instancias de un delegate objeto 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);

En el código siguiente se muestra cómo declarar una función local estática e invocar el UnsafeCombine método 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:

  • Solo puede declarar punteros de función en un unsafe contexto.
  • Solo puede llamar a métodos que toman delegate* (o devuelven ) delegate*en un unsafe contexto.
  • 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 sección Punteros de función de la especificación del lenguaje C#.

Ejemplo: Uso de 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 unsafe palabra clave , que permite usar punteros en el Copy método . La fixed instrucción declara 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. El fixed bloque ancla los bloques de memoria de las matrices en el ámbito del bloque. Dado que el Copy método de este ejemplo usa la unsafe palabra clave , debe compilarlo mediante la opción del compilador AllowUnsafeBlocks .

En este ejemplo se obtiene acceso a los elementos de ambas matrices mediante índices en lugar de 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
    */
}

El modelo de seguridad de memoria actualizado (versión preliminar)

Importante

El modelo de seguridad de memoria actualizado es una característica en versión preliminar en C# 15 y .NET 11. Sigue evolucionando en función de los comentarios durante las versiones preliminares. Para probar el modelo, use el SDK de .NET 11 (versión preliminar) y establezca la LangVersion opción del compilador en preview. El compilador de .NET 11 Preview 5 implementa las relajaciones del puntero, pero aún no aplica las obligaciones del autor de la llamada, la participación del ensamblado o la safe palabra clave . Para obtener el diseño completo, consulte la especificación de características de seguridad de memoria.

El modelo actualizado separa dos cosas que el modelo original trata como uno: la existencia de código de puntero y la propagación de obligaciones de seguridad a los autores de llamadas. Marcar un miembro unsafe ya no solo permite punteros en su cuerpo; hace que el autor de la llamada del miembro no sea seguro, por lo que cada llamador debe propagar esa obligación o descargarla detrás de un límite validado y seguro invocable. Para admitir esa separación, el modelo también limita el contexto no seguro: la existencia de un puntero no es seguro, solo las operaciones que acceden a la memoria que el tiempo de ejecución no administra. El estrechamiento le permite mantener, pasar y devolver punteros en código seguro, mientras unsafe que marca las operaciones y los miembros que realmente pueden infringir la seguridad de la memoria.

Miembros no seguros del autor de la llamada

En el modelo original, el unsafe modificador de un miembro solo permite punteros en la firma y el cuerpo del miembro. No informa a los autores de llamadas sobre la seguridad. El modelo actualizado proporciona el significado del modificador para los autores de llamadas. Cuando se marca un miembro unsafe, el compilador lo trata como no seguro del autor de la llamada (también denominado require-unsafe): cada llamador debe invocarlo desde un unsafe contexto y la obligación de auditar la seguridad se mueve a ese autor de la llamada.

El unsafe modificador de una firma de miembro ya no establece un contexto no seguro para el cuerpo. Los dos roles se dividen:

  • El unsafe modificador de la firma propaga la obligación de llamar a los autores.
  • Un bloque interno unsafe limita las operaciones que acceden a la memoria no administrada.

En el siguiente simulacro de vista previa, ReadInt32 no es seguro para el autor de la llamada. La firma lleva el unsafe modificador y un bloque interno unsafe encapsula la desreferencia:

// Preview: illustrates the updated model, which the current compiler doesn't fully enforce yet.
public static unsafe int ReadInt32(byte* source)
{
    unsafe
    {
        return *(int*)source;
    }
}

Un llamador encapsula la llamada en su propio unsafe bloque:

// Preview
unsafe
{
    int value = ReadInt32(buffer);
}

El modelo actualizado también ajusta algunas reglas relacionadas:

  • El unsafe modificador genera un error en una declaración de tipo, un constructor estático y un finalizador, porque el modificador no tiene que informar al autor de la llamada.
  • Los delegados no pueden ser unsafe, porque un delegado tiene forma de tipo.
  • Tipo cuyo constructor sin parámetros unsafe no cumple la new() restricción.

Operaciones que requieren un contexto no seguro

Las operaciones que acceden a la memoria apuntada requieren un unsafe contexto:

  • Direccionamiento indirecto de puntero (*p), acceso de miembro de puntero (p->member) y acceso a elementos de puntero (p[i]).
  • Invocación del puntero de función.
  • Acceso a elementos en un búfer de tamaño fijo.

En el ejemplo siguiente se ancla una matriz sin contexto unsafe , pero se desreferencia el puntero dentro de uno:

public static int ReadValue(int[] numbers)
{
    fixed (int* first = numbers)
    {
        // Dereferencing a pointer accesses unmanaged memory, so it still
        // requires an unsafe context.
        unsafe
        {
            return *first;
        }
    }
}

Operaciones relajadas

Las operaciones que no tienen acceso a la memoria apuntada ya no requieren un unsafe contexto:

  • Declarar un tipo de puntero y tomar la dirección de una variable con el & operador .
  • Instrucción fixed que ancla una variable.
  • Convertir una stackalloc expresión en un puntero.
  • Operador sizeof aplicado a cualquier tipo no administrado.

En el ejemplo siguiente se crean punteros y se anclan sin contexto unsafe :

public static void CreatePointer()
{
    int value = 42;
    // Creating a pointer doesn't require an unsafe context.
    int* pointer = &value;
    int** pointerToPointer = &pointer;
}
public static void PinArray(int[] numbers)
{
    // The fixed statement no longer requires an unsafe context.
    fixed (int* first = numbers)
    {
        int* current = first;
    }
}

Estas relajaciones se aplican cada vez que se compila con la versión del preview lenguaje, independientemente de si un ensamblado opta por las reglas de seguridad de memoria actualizadas.

Descarga de obligaciones no seguras del autor de la llamada

Un miembro que llama a una operación no segura para el autor de la llamada tiene dos opciones: propagar la obligación o descargarla.

  • Propagación: marque su propio miembro unsafe. La obligación pasa a sus llamadores. Use la propagación cuando no pueda validar completamente la obligación usted mismo.
  • Descarga: deje la firma de su miembro a salvo. Valide la obligación dentro del miembro, normalmente con guardias en tiempo de ejecución y, a continuación, realice la operación no segura en un bloque interno unsafe . Un miembro que contiene un bloque interno unsafe , pero que no marca su propia firma unsafe es un límite no seguro: convierte el código no seguro en una superficie a la que se puede llamar de forma segura.

El siguiente simulacro de vista previa valida su entrada con una protección, ancla una matriz administrada y lee el puntero. Los autores de llamadas no necesitan un unsafe contexto, ya que el método descarga la obligación:

// Preview
public static int SumBytes(byte[] source)
{
    ArgumentNullException.ThrowIfNull(source);

    fixed (byte* first = source)
    {
        unsafe
        {
            // SAFETY: the null check and source.Length bound every read to the pinned array.
            int total = 0;
            for (int i = 0; i < source.Length; i++)
            {
                total += first[i];
            }

            return total;
        }
    }
}

La comprobación nula y la longitud de la matriz descartan las entradas que permitirían que una lectura se ejecutara más allá del búfer, por lo que la desreferencia dentro del unsafe bloque es sonido. El método no deja ninguna obligación residual, por lo que expone una firma que se puede llamar de forma segura.

Documentación de seguridad

Un miembro no seguro del autor de la llamada debe documentar lo que debe garantizar el autor de la llamada. El modelo actualizado fomenta dos estilos de comentario complementarios:

  • Un /// <safety> bloque de documentación situado encima de la firma indica el contrato formal: las condiciones que debe cumplir un autor de la llamada. Un analizador puede marcar un miembro no seguro del autor de la llamada que falta.
  • Un // SAFETY: comentario dentro de un unsafe bloque registra por qué la operación es sólida en ese lugar, para los desarrolladores y auditores que leen el cuerpo.

El siguiente simulacro de vista previa muestra ambos estilos en un método no seguro ReadByte para el autor de la llamada:

// Preview
/// <summary>Reads a single byte from unmanaged memory.</summary>
/// <safety>
/// The sum of <paramref name="ptr"/> and <paramref name="offset"/> must address a byte
/// the caller is permitted to read.
/// </safety>
public static unsafe byte ReadByte(IntPtr ptr, int offset)
{
    byte* address = (byte*)ptr;
    unsafe
    {
        // SAFETY: relies on the caller obligation stated in the <safety> block.
        return address[offset];
    }
}

El /// <safety> bloque le indica el contrato. El contrato pertenece a la documentación en la que cada autor de llamada y revisor la ve.

Campos no seguros

Use el unsafe modificador para un campo cuando su tipo declarado no expresa contratos de los que el tipo envolvente mantiene y de otro código depende. La falta de seguridad existe entre lo que ve el sistema de tipos y lo que promete el tipo. El modificador fuerza cada escritura al campo en un unsafe bloque, lo que mantiene las escrituras revisables en un solo lugar.

El caso más claro es un campo que contiene un puntero nativo. El puntero no declara cuántos bytes direcciona como System.Span<T> lo hace, por lo que el tipo contenedor mantiene esa información en sí:

// Preview
public class NativeBuffer
{
    /// <safety>
    /// Null, or points to a buffer of Length bytes.
    /// </safety>
    private unsafe byte* _pointer;

    public int Length { get; }

    public byte ReadAt(int index)
    {
        ArgumentOutOfRangeException.ThrowIfNegative(index);
        ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(index, Length);
        unsafe
        {
            // SAFETY: the bounds checks confine the read to the buffer that _pointer addresses.
            return _pointer[index];
        }
    }
}

Un readonly unsafe campo empareja el contrato con una protección integrada: unsafe asigna nombres a la invariable y readonly evita una escritura que podría interrumpirlo después de la construcción. Marcar una propiedad o un evento unsafe no hace que su campo de respaldo no sea seguro. En un struct con [StructLayout(LayoutKind.Explicit)], marca todos los campos safe o unsafe.

Palabra clave safe

El modelo actualizado agrega una safe palabra clave contextual que da fe de que una declaración es sólida donde el compilador requiere que haga la elección explícita.

Un extern miembro llama al código nativo, por lo que el compilador no puede clasificar su seguridad. En el modelo actualizado, marca cada extern declaración, incluido un LibraryImport método parcial, ya sea safe o unsafe:

// Preview
[LibraryImport("libc")]
internal static safe partial int getpid();

[LibraryImport("libc", StringMarshalling = StringMarshalling.Utf8)]
internal static unsafe partial nint strlen(byte* str);

getpid no toma parámetros y devuelve un primitivo, por lo que el autor atestigua que la llamada es segura y los autores de llamadas lo usan sin ceremonia. strlen toma un puntero sin procesar que el código nativo desreferencia, por lo que la declaración es unsafe y propaga la obligación de llamar a los autores. Omitir ambos modificadores es un error, lo que le obliga a tomar la decisión de seguridad. Un campo de una estructura con diseño explícito usa la misma regla.

Comportamiento de participación y entre ensamblados

El modelo actualizado tiene dos modificadores independientes de nivel de proyecto:

  • Una nueva propiedad opt-in activa las reglas actualizadas. Cuando la propiedad está desactivada, se aplican las reglas originales. Cuando está activado, unsafe en un miembro se propaga a los autores de llamadas y el compilador registra la elección en el ensamblado con el MemorySafetyRulesAttribute atributo .
  • La propiedad AllowUnsafeBlocks existente bloquea todas las apariencias de la unsafe palabra clave, incluidos los bloques internos en los sitios de llamada. El valor predeterminado es false, por lo que un proyecto en el valor predeterminado no puede llamar a ninguna API no segura.

Las dos propiedades se combinan de la siguiente manera:

Propiedad Opt-in AllowUnsafeBlocks Result
Activado Desactivado (valor predeterminado) La configuración más segura. El proyecto usa el modelo actualizado y no permite ningún código no seguro.
Activado Activado El proyecto usa el modelo actualizado y permite código no seguro.
Apagado Apagado El modelo original se aplica y el proyecto no puede usar tipos de puntero.
Apagado Activado El modelo original se aplica y el proyecto puede usar tipos de puntero.

Si un ensamblado aplica las reglas actualizadas en otra depende de qué lado opte por:

  • Autor de llamada del modelo actualizado, destinatario del modelo actualizado: los marcadores del autor de la llamada viajan a través de unsafe metadatos. El autor de la llamada encapsula cada llamada a un miembro no seguro del autor de la llamada en un unsafe bloque.
  • Llamador de modelo actualizado, llamador del modelo original: un modo de compatibilidad trata a cualquier miembro de llamada con un tipo de puntero en su firma como llamador-unsafe, por lo que el sitio de llamada necesita un bloque envolvente unsafe . Este modo impide que una API basada en puntero pierda silenciosamente su unsafe requisito.
  • Autor de llamada del modelo original, destinatario del modelo actualizado: se siguen aplicando las reglas de puntero originales. Un miembro no seguro del autor de la llamada que no tiene ningún tipo de puntero en su firma se puede llamar desde código seguro, ya que el autor de la llamada del modelo original no puede leer los nuevos marcadores.

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# .

Para conocer el diseño del modelo de seguridad de memoria actualizado, consulte la especificación de características de seguridad de memoria.

Consulte también