Comparteix a través de


Tutorial: Uso de administradores de serialización personalizados en P/Invokes generados por código fuente

En este tutorial, aprenderá a implementar un administrador de serialización y a usarlo para la serialización personalizada en P/Invokes generados por código fuente.

Implementará administradores de serialización para un tipo integrado, personalizará la serialización de un parámetro específico y un tipo definido por el usuario y especificará la serialización predeterminada para un tipo definido por el usuario.

Todo el código fuente usado en este tutorial está disponible en el repositorio dotnet/samples.

Información general sobre el generador de código fuente LibraryImport

El tipo System.Runtime.InteropServices.LibraryImportAttribute es el punto de entrada de usuario para un generador de código fuente introducido en .NET 7. Este generador de código fuente está diseñado para generar todo el código de serialización en tiempo de compilación en lugar de en tiempo de ejecución. Los puntos de entrada se han especificado tradicionalmente mediante DllImport, pero ese enfoque incluye costos que pueden no ser siempre aceptables; vea Generación de código fuente de P/Invoke para obtener más detalles. El generador de código fuente LibraryImport puede generar todo el código de serialización y quitar el requisito de generación en tiempo de ejecución intrínseco a DllImport.

Para expresar los detalles necesarios para el código de serialización generado para el tiempo de ejecución y para que los usuarios personalicen sus propios tipos, se necesitan varios tipos. En este tutorial se usan los siguientes tipos:

  • MarshalUsingAttribute: atributo buscado por el generador de código fuente en los sitios en que se usa, que sirve para determinar el tipo de administrador de serialización para serializar la variable con atributos.

  • CustomMarshallerAttribute: atributo utilizado para indicar un tipo a un administrador de serialización y el modo en el que se van a realizar las operaciones de serialización (por ejemplo, by-ref de administrado a no administrado).

  • NativeMarshallingAttribute: atributo utilizado para indicar qué administrador de serialización se va a usar para el tipo con atributos. Es útil para los autores de bibliotecas que proporcionan tipos y administradores de serialización complementarios para esos tipos.

Sin embargo, estos atributos no son los únicos mecanismos disponibles para un autor de administrador de serialización personalizado. El generador de código fuente inspecciona el propio administrador de serialización en busca de otras indicaciones que informen sobre cómo debe producirse la serialización.

Puede encontrar detalles completos sobre el diseño en el repositorio dotnet/runtime.

Analizador y reparador del generador de código fuente

Junto con el propio generador de código fuente, se proporcionan un analizador y un reparador. El analizador y el reparador están habilitados y disponibles de forma predeterminada desde .NET 7 RC1. El analizador está diseñado para ayudar a los desarrolladores a usar correctamente el generador de código fuente. El reparador proporciona conversiones automatizadas de muchos patrones DllImport a la firma LibraryImport adecuada.

Presentación de la biblioteca nativa

El uso del generador de código fuente LibraryImport supondría consumir una biblioteca nativa o no administrada. Una biblioteca nativa puede ser una biblioteca compartida (es decir, .dll, .so o dylib) que llama directamente a una API de sistema operativo que no se expone a través de .NET. La biblioteca también puede ser una que esté muy optimizada en un lenguaje no administrado que un desarrollador de .NET quiera utilizar. En este tutorial, creará su propia biblioteca compartida que expone una superficie de API de estilo C. El código siguiente representa un tipo definido por el usuario y dos API que consumirá de C#. Estas dos API representan el modo "entrada", pero hay modos adicionales para explorar en el ejemplo.

struct error_data
{
    int code;
    bool is_fatal_error;
    char32_t* message;    /* UTF-32 encoded string */
};

extern "C" DLL_EXPORT void STDMETHODCALLTYPE PrintString(char32_t* chars);

extern "C" DLL_EXPORT void STDMETHODCALLTYPE PrintErrorData(error_data data);

El código anterior contiene los dos tipos de interés, char32_t* y error_data. char32_t* representa una cadena codificada en UTF-32, que no es una codificación de cadena que .NET serializa históricamente. error_data es un tipo definido por el usuario que contiene un campo entero de 32 bits, un campo booleano de C++ y un campo de cadena con codificación UTF-32. Ambos tipos requieren que especifique un método para que el generador de código fuente genere código de serialización.

Personalización de la serialización de un tipo integrado

Primero considere el tipo char32_t*, ya que el tipo definido por el usuario requiere la serialización de este tipo. char32_t* representa el lado nativo, pero también necesita representación en código administrado. En .NET, solo hay un tipo "string", string. Por lo tanto, va a serializar una cadena nativa con codificación UTF-32 hacia y desde el tipo string en código administrado. Ya hay varios administradores de serialización integrados para el tipo string que serializan como UTF-8, UTF-16, ANSI e incluso como el tipo BSTR de Windows. Sin embargo, no hay ninguno para serializar como UTF-32. Necesita definirlo.

El tipo Utf32StringMarshaller se marca con un atributo CustomMarshaller, que describe lo que hace con el generador de código fuente. El primer argumento de tipo para el atributo es el tipo string, el tipo administrado que se va a serializar, el segundo es el modo, que indica cuándo usar el administrador de serialización, y el tercer tipo es Utf32StringMarshaller, el tipo que se va a usar para serializar. Puede aplicar CustomMarshaller varias veces para especificar el modo y el tipo de administrador de serialización que se va a usar para ese modo.

En el ejemplo actual se muestra un administrador de serialización "sin estado" que usa una entrada determinada y devuelve datos en el formato serializado. El método Free existe para la simetría con la serialización no administrada, y el recolector de elementos no utilizados es la operación de "liberación" para el administrador de serialización administrado. El implementador es libre de realizar cualquier operación que desee serializar la entrada en la salida, pero recuerde que el generador de código fuente no conservará explícitamente ningún estado.

namespace CustomMarshalling
{
    [CustomMarshaller(typeof(string), MarshalMode.Default, typeof(Utf32StringMarshaller))]
    internal static unsafe class Utf32StringMarshaller
    {
        public static uint* ConvertToUnmanaged(string? managed)
            => throw new NotImplementedException();

        public static string? ConvertToManaged(uint* unmanaged)
            => throw new NotImplementedException();

        public static void Free(uint* unmanaged)
            => throw new NotImplementedException();
    }
}

Los detalles de cómo este administrador de serialización concreto realiza la conversión de string a char32_t* se puede encontrar en el ejemplo. Tenga en cuenta que se pueden usar las API de .NET (por ejemplo, Encoding.UTF32).

Considere un caso en el que el estado es conveniente. Observe el modo adicional CustomMarshaller y tenga en cuenta el modo más específico, MarshalMode.ManagedToUnmanagedIn. Este administrador de serialización especializado se implementa como "con estado" y puede almacenar el estado en la llamada de interoperabilidad. Más especialización y estado permiten optimizaciones y una serialización adaptada para un modo. Por ejemplo, se puede indicar al generador de código fuente que proporcione un búfer asignado a la pila que podría evitar una asignación explícita durante la serialización. Para indicar la compatibilidad con un búfer asignado a la pila, el administrador de serialización implementa una propiedad BufferSize y un método FromManaged que usan un Span de un tipo unmanaged. La propiedad BufferSize indica la cantidad de espacio de la pila (la longitud de la clase Span que se va a pasar a FromManaged) que el administrador de serialización desea obtener durante la llamada de serialización.

namespace CustomMarshalling
{
    [CustomMarshaller(typeof(string), MarshalMode.Default, typeof(Utf32StringMarshaller))]
    [CustomMarshaller(typeof(string), MarshalMode.ManagedToUnmanagedIn, typeof(ManagedToUnmanagedIn))]
    internal static unsafe class Utf32StringMarshaller
    {
        //
        // Stateless functions removed
        //

        public ref struct ManagedToUnmanagedIn
        {
            public static int BufferSize => 0x100;

            private uint* _unmanagedValue;
            private bool _allocated; // Used stack alloc or allocated other memory

            public void FromManaged(string? managed, Span<byte> buffer)
                => throw new NotImplementedException();

            public uint* ToUnmanaged()
                => throw new NotImplementedException();

            public void Free()
                => throw new NotImplementedException();
        }
    }
}

Ahora puede llamar a la primera de las dos funciones nativas mediante los administradores de serialización de cadenas UTF-32. La siguiente declaración usa el atributo LibraryImport, al igual que DllImport, pero se basa en el atributo MarshalUsing para indicar al generador de código fuente qué administrador de serialización usar al llamar a la función nativa. No es necesario aclarar si se debe usar el administrador de serialización con o sin estado. Esto se controla mediante el implementador que define MarshalMode en los atributos CustomMarshaller del administrador de serialización. El generador de código fuente seleccionará el administrador de serialización más adecuado en función del contexto en el que se aplica MarshalUsing, con MarshalMode.Default como reserva.

// extern "C" DLL_EXPORT void STDMETHODCALLTYPE PrintString(char32_t* chars);
[LibraryImport(LibName)]
internal static partial void PrintString([MarshalUsing(typeof(Utf32StringMarshaller))] string s);

Personalización de la serialización de un tipo definido por el usuario

La serialización de un tipo definido por el usuario requiere definir no solo la lógica de serialización, sino también el tipo de C# para serializar el origen y el destino. Recuerde el tipo nativo que estamos intentando serializar.

struct error_data
{
    int code;
    bool is_fatal_error;
    char32_t* message;    /* UTF-32 encoded string */
};

Ahora, defina cómo se vería idealmente en C#. int tiene el mismo tamaño tanto en C++ moderno como en .NET. Un bool es el ejemplo canónico de un valor booleano en .NET. Basándose en Utf32StringMarshaller, puede serializar char32_t* como un elemento string de .NET. Teniendo en cuenta el estilo de .NET, el resultado es la siguiente definición en C#:

struct ErrorData
{
    public int Code;
    public bool IsFatalError;
    public string? Message;
}

Siguiendo el patrón de nomenclatura, asigne el nombre ErrorDataMarshaller al administrador de serialización. En lugar de especificar un administrador de serialización para MarshalMode.Default, solo definirá los administradores de serialización para algunos modos. En este caso, si el administrador de serialización se usa para un modo que no se proporciona, se producirá un error en el generador de código fuente. Comience por definir un administrador de serialización para la dirección de "entrada". Se trata de un administrador de serialización "sin estado" porque el propio administrador de serialización solo consta de funciones static.

namespace CustomMarshalling
{
    [CustomMarshaller(typeof(ErrorData), MarshalMode.ManagedToUnmanagedIn, typeof(ErrorDataMarshaller))]
    internal static unsafe class ErrorDataMarshaller
    {
        // Unmanaged representation of ErrorData.
        // Should mimic the unmanaged error_data type at a binary level.
        internal struct ErrorDataUnmanaged
        {
            public int Code;        // .NET doesn't support less than 32-bit, so int is 32-bit.
            public byte IsFatal;    // The C++ bool is defined as a single byte.
            public uint* Message;   // This could be as simple as a void*, but uint* is closer.
        }

        public static ErrorDataUnmanaged ConvertToUnmanaged(ErrorData managed)
            => throw new NotImplementedException();

        public static void Free(ErrorDataUnmanaged unmanaged)
            => throw new NotImplementedException();
    }
}

ErrorDataUnmanaged imita la forma del tipo no administrado. La conversión de ErrorData a ErrorDataUnmanaged ahora es trivial con Utf32StringMarshaller.

No es necesario serializar int, ya que su representación es idéntica en el código no administrado y administrado. La representación binaria de un valor bool no está definida en .NET, por lo que debe usar su valor actual para definir un valor cero y distinto de cero en el tipo no administrado. A continuación, reutilice el administrador de serialización en UTF-32 para convertir el campo string en uint*.

public static ErrorDataUnmanaged ConvertToUnmanaged(ErrorData managed)
{
    return new ErrorDataUnmanaged
    {
        Code = managed.Code,
        IsFatal = (byte)(managed.IsFatalError ? 1 : 0),
        Message = Utf32StringMarshaller.ConvertToUnmanaged(managed.Message),
    };
}

Recuerde que va a definir este serializador como "entrada", por lo que debe limpiar las asignaciones realizadas durante la serialización. Los campos int y bool no asignaron ninguna memoria, pero el campo Message, sí. Vuelva a usar Utf32StringMarshaller para limpiar la cadena serializada.

public static void Free(ErrorDataUnmanaged unmanaged)
    => Utf32StringMarshaller.Free(unmanaged.Message);

Vamos a considerar brevemente el escenario de "salida". Considere el caso en el que se devuelven una o varias instancias de error_data.

extern "C" DLL_EXPORT error_data STDMETHODCALLTYPE GetFatalErrorIfNegative(int code)

extern "C" DLL_EXPORT error_data* STDMETHODCALLTYPE GetErrors(int* codes, int len)
[LibraryImport(LibName)]
internal static partial ErrorData GetFatalErrorIfNegative(int code);

[LibraryImport(LibName)]
[return: MarshalUsing(CountElementName = "len")]
internal static partial ErrorData[] GetErrors(int[] codes, int len);

Una clase P/Invoke que devuelve un tipo de instancia único, que no es de colección, se clasifica como MarshalMode.ManagedToUnmanagedOut. Normalmente, se usa una colección para devolver varios elementos y, en este caso, se usa Array. El administrador de serialización de un escenario de colección, correspondiente al modo MarshalMode.ElementOut, devolverá varios elementos, y se describe más adelante.

namespace CustomMarshalling
{
    [CustomMarshaller(typeof(ErrorData), MarshalMode.ManagedToUnmanagedIn, typeof(ErrorDataMarshaller))]
    [CustomMarshaller(typeof(ErrorData), MarshalMode.ElementOut, typeof(Out))]
    internal static unsafe class ErrorDataMarshaller
    {
        //
        // Other marshallers removed
        //

        public static class Out
        {
            public static ErrorData ConvertToManaged(ErrorDataUnmanaged unmanaged)
                => throw new NotImplementedException();

            public static ErrorDataUnmanaged ConvertToUnmanaged(ErrorData managed)
                => throw new NotImplementedException();

            public static void Free(ErrorDataUnmanaged unmanaged)
                => throw new NotImplementedException();
        }
    }
}

La conversión de ErrorDataUnmanaged a ErrorData sigue el procedimiento inverso al aplicado para el modo de "entrada". Recuerde que también debe limpiar las asignaciones que el entorno no administrado esperaba que realizara. También es importante tener en cuenta que las funciones aquí están marcadas como static y, por lo tanto, son "sin estado"; el modo "sin estado" es un requisito para todos los modos "Elemento". También observará que hay un método ConvertToUnmanaged como en el modo "in". Todos los modos "Element" requieren control para los modos "in" y "out".

Para el administrador de serialización de "salida" de código administrado a no administrado, debe hacer algo especial. El nombre del tipo de datos que se va a serializar se llama error_data, y .NET suele expresar errores como excepciones. Algunos errores son más graves que otros, y los errores identificados como "irrecuperables" suelen indicar un error catastrófico o irreversibles. Observe que error_data tiene un campo para comprobar si el error es irrecuperable. Serializará un elemento error_data en código administrado y, si es irrecuperable, iniciará una excepción en lugar de convertirla en ErrorData y devolverla.

namespace CustomMarshalling
{
    [CustomMarshaller(typeof(ErrorData), MarshalMode.ManagedToUnmanagedIn, typeof(ErrorDataMarshaller))]
    [CustomMarshaller(typeof(ErrorData), MarshalMode.ElementOut, typeof(Out))]
    [CustomMarshaller(typeof(ErrorData), MarshalMode.ManagedToUnmanagedOut, typeof(ThrowOnFatalErrorOut))]
    internal static unsafe class ErrorDataMarshaller
    {
        //
        // Other marshallers removed
        //

        public static class ThrowOnFatalErrorOut
        {
            public static ErrorData ConvertToManaged(ErrorDataUnmanaged unmanaged)
                => throw new NotImplementedException();

            public static void Free(ErrorDataUnmanaged unmanaged)
                => throw new NotImplementedException();
        }
    }
}

Un parámetro de "salida" se convierte de un contexto no administrado a un contexto administrado, por lo que se implementa el método ConvertToManaged. Cuando el destinatario no administrado devuelve y proporciona un objeto ErrorDataUnmanaged, puede inspeccionarlo mediante el administrador de serialización en modo ElementOut y comprobar si está marcado como un error irrecuperable. Si es así, se trata de la indicación que se debe emitir en lugar de devolver solo ErrorData.

public static ErrorData ConvertToManaged(ErrorDataUnmanaged unmanaged)
{
    ErrorData data = Out.ConvertToManaged(unmanaged);
    if (data.IsFatalError)
        throw new ExternalException(data.Message, data.Code);

    return data;
}

Quizás no solo va a usar la biblioteca nativa, sino que también quiere compartir su trabajo con la comunidad y proporcionar una biblioteca de interoperabilidad. Puede proporcionar a ErrorData un administrador de serialización implícito cada vez que se use en un elemento P/Invoke agregando [NativeMarshalling(typeof(ErrorDataMarshaller))] a la definición ErrorData. Ahora, cualquier persona que use su definición de este tipo en una llamada LibraryImport se beneficiará de sus administradores de serialización. Siempre pueden invalidar los administradores de serialización mediante MarshalUsing en el sitio de uso.

[NativeMarshalling(typeof(ErrorDataMarshaller))]
struct ErrorData { ... }

Vea también