Con .NET 5 programado para ser lanzado a finales de este año, pensamos que sería un buen momento para discutir algunas de las actualizaciones de interoperabilidad que se incluyeron en el lanzamiento y señalar algunos elementos que estamos considerando para el futuro.
A medida que empezamos a pensar en lo que viene a continuación, buscamos desarrolladores y consumidores de cualquier solución de interoperabilidad para discutir sus experiencias. Buscamos comentarios sobre los escenarios de interoperabilidad en general, no solo los relacionados con .NET. Si ha trabajado en el espacio de interoperabilidad, nos encantaría saber de usted sobre nuestro problema de GitHub.
Algunos elementos mencionados en esta publicación son específicos de Windows (COM y WinRT). En esos casos, ‘el tiempo de ejecución’ se refiere solo a CoreCLR.
Punteros de función
Los punteros de función de C# proporcionan una forma eficaz de llamar a funciones nativas desde C#. Tiene sentido que el tiempo de ejecución proporcione una solución simétrica para llamar a funciones administradas desde código nativo.
UnmanagedCallersOnlyAttribute
Indica que se llamará a una función solo desde el código nativo, lo que permite que el tiempo de ejecución reduzca el costo de llamar a la función administrada.
Para limitar la complejidad del escenario, el uso de este atributo está restringido a métodos que deben:
- Ser
static
- Tener solamente argumentos blittable.
- Elimina la dependencia de cualquier lógica de clasificación especial.
- No ser llamado desde código administrado.
- Limita los escenarios que deben manejarse (por ejemplo, sin llamadas a través de la reflexión), lo que permite que el enfoque permanezca en reducir el costo de llamar a la función administrada desde el código nativo.
Un escenario de uso básico de pasar una devolución de llamada administrada a una función nativa se vería, sin UnmanagedCallersOnlyAttribute, como:
public static int Callback(int i)
{
// ...
}
private delegate void CallbackDelegate(int i);
private static CallbackDelegate s_callback = new CallbackDelegate(Callback);
[DllImport("NativeLib")]
private static extern void NativeFunctionWithCallback(IntPtr callback);
static void Main()
{
IntPtr callback = Marshal.GetFunctionPointerForDelegate(s_callback);
NativeFunctionWithCallback(callback);
}
Lo anterior requiere la asignación de un delegado y la clasificación de ese delegado a un puntero de función. Si la función nativa a la que se llama podría retener la devolución de llamada, también debemos asegurarnos de que el delegado no sea recolectado como basura. Este detalle a menudo se pasa por alto, lo que lleva a bloqueos intermitentes de «Devolución de llamada al delegado recopilado».
Con la combinación de punteros de función y UnmanagedCallersOnlyAttribute
, esto se puede reescribir como:
[UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvCdecl) })]
public static int Callback(int i)
{
// ...
}
[DllImport("NativeLib")]
private static extern void NativeFunctionWithCallback(delegate* cdecl<int, int> callback);
static void Main()
{
// The extra cast is a temporary workaround for Preview 8. It won't be required in the final version.
// The syntax will also be updated to use the 'unmanaged' keyword
// delegate* unmanaged[Cdecl]<int, int> unmanagedPtr = &Callback;
delegate* cdecl<int, int> unmanagedPtr = (delegate* cdecl<int, int>)(delegate* <int, int>)&Callback;
NativeFunctionWithCallback(unmanagedPtr);
}
El cambio más obvio es que la asignación de un delegado ya no es necesaria. Al requerir que la función solo tenga argumentos blittables, el tiempo de ejecución no necesita hacer ninguna clasificación, por lo que el único requisito para ingresar a la función es una transición de GC al modo cooperativo. La restricción de no permitir que se llame a la función desde el código administrado significa que la función JIT-ed en sí misma puede realizar la transición de GC. El puntero de función Callbackanterior apunta directamente a la función JIT-ed. El código adicional propenso a errores para mantener vivo al delegado tampoco es necesario.
System.Private.CoreLib
ha comenzado a usar este atributo para algunas funciones: dotnet/runtime#34270 , dotnet/runtime#39082
También UnmanagedCallersOnlyAttributees
compatible con las API de alojamiento .NET para llamar a una función administrada desde un host nativo.
Advertencias:
- La ruta x86 está menos optimizada que otras ( dotnet/runtime#33582 ).
- Marcando con una P/Invoke
UnmanagedCallersOnlyAttribute
no se admite.
Recursos:
- API:
UnmanagedCallersOnlyAttribute
- Propuesta: dotnet/runtime#32462
- Implementación: dotnet/runtime#33005 , dotnet/runtime#35592
- Prototipo de exportaciones nativas (utiliza las API de hospedaje .NET y
UnmanagedCallersOnlyAttribute
como bloques de construcción): DNNE
Convención de llamadas no administradas
Los punteros de función de C# permitirán la declaración con una convención de llamada no administrada usando la unmanaged palabra clave (esta sintaxis aún no se envió, pero estará en la versión final). Lo siguiente utilizará el valor predeterminado dependiente de la plataforma:
// Platform-dependent default calling convention
delegate* unmanaged<int, int>;
Dado que la función no administrada puede tener una convención de llamada diferente a la de la plataforma predeterminada, la convención de llamada no administrada también se puede especificar explícitamente:
// cdecl calling convention
delegate* unmanaged[Cdecl] <int, int>;
De manera similar, una función marcada con UnmanagedCallersOnlyAttribute
puede depender del valor predeterminado dependiente de la plataforma o especificar explícitamente su convención de llamada:
// Platform-dependent default calling convention
[UnmanagedCallersOnly]
public static int Callback(int i) { ... }
// cdecl calling convention
[UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvCdecl) })]
public static int Callback(int i) { ... }
El tiempo de ejecución reconoce las siguientes convenciones de llamada: CallConvCdecl, CallConvFastcall, CallConvStdcall, y CallConvThiscall.
Como el compilador de Roslyn y los equipos de tiempo de ejecución estaban agregando este soporte, la extensibilidad fue una consideración importante. Los metadatos para la firma de un método tienen un CallKind
valor que identifica su convención de llamada ( ECMA-335 II.15.3). El nuevo unmanaged
valor de la convención de llamada (0x9), en lugar de mapear directamente a una convención de llamada específica, indica que la convención de llamada se puede codificar en modopts
para el tipo de retorno. Para determinar la convención de llamada real, el tiempo de ejecución verificará si los modopt
valores coinciden con los tipos de convención de llamada conocidos y usará el valor predeterminado dependiente de la plataforma si ningún valor coincide.
Con este mecanismo implementado, el tiempo de ejecución puede agregar soporte para convenciones de llamada adicionales en el futuro sin usar más valores del bit de convención de llamada. También permite una forma de codificar el comportamiento modificado como SuppressGCTransition( dotnet/runtime#38134 ).
Recursos:
- Propuesta: dotnet/runtime#38133
- Implementación: dotnet/runtime#38357 , dotnet/runtime#39030
- Metadatos de firma del método: ECMA-335 II.15.3
API de bajo nivel para la interacción con el sistema de interoperabilidad integrado
El tiempo de ejecución tiene un sistema integrado que maneja el soporte de interoperabilidad, como P/Invocaciones, ordenamiento e interacciones COM . Un tema subyacente para la interoperabilidad en .NET 5 ha sido proporcionar bloques de construcción de bajo nivel que permiten que los componentes fuera del tiempo de ejecución se integren mejor con el sistema de interoperabilidad incorporado. En .NET 5, agregamos algunas API que permiten un mayor control sobre el sistema de interoperabilidad utilizado en el tiempo de ejecución.
SuppressGCTransition
Al ejecutar un P/Invoke , el tiempo de ejecución cambia el modo GC de cooperativo a preventivo. Dependiendo del escenario, esta transición, que también incluye un marco adicional, puede llevar a que la configuración de un P / Invoke sea más costosa que la función nativa que se invoca.
SuppressGCTransitionAttribute
proporciona una forma para que los desarrolladores indiquen que un P/Invoke debe evitar la transición de GC. La capacidad de reducir esta sobrecarga de interoperabilidad permite llamadas P / Invoke de alto rendimiento tanto en bibliotecas de tiempo de ejecución como en bibliotecas de terceros. Esto es similar en espíritu a FCalls internos en el propio tiempo de ejecución.
ComWrappers
En Windows, el Modelo de objetos componentes (COM) define un sistema mediante el cual los componentes binarios pueden exponerse e interactuar con otros componentes y aplicaciones. El tiempo de ejecución tiene un sistema integrado para interoperar con objetos COM, con clases contenedoras estándar – Contenedores invocables en tiempo de ejecución (RCW) y Contenedores invocables COM (CCW) – para manejar el límite entre COM y el tiempo de ejecución .NET.
IDynamicInterfaceCastable
En .NET, los metadatos de un tipo son estáticos, por lo que se puede determinar si es posible convertir un tipo a otro en función de los metadatos. El tiempo de ejecución contiene lógica para manejar casos especiales (por ejemplo, objetos COM) usando información más allá de los metadatos, pero no había un mecanismo general para que una clase participara en la lógica de conversión de tipos.
Soporte para WinRT
Las API agregadas anteriormente proporcionaron la base para las mejoras en la forma en que la interoperabilidad de WinRT funciona con .NET. Nos permitieron admitir las API de WinRT mientras desacoplaban el sistema de interoperabilidad de WinRT del tiempo de ejecución de .NET.
Como se anunció anteriormente , esto significaba que podíamos eliminar el soporte integrado para la interoperabilidad de WinRT en .NET 5 ( dotnet/runtime#36715 ). La cadena de herramientas C#/WinRT aprovecha las nuevas API y sirve como reemplazo para ese soporte integrado. Este nuevo modelo permite:
- Desarrollo y mejora de la interoperabilidad de WinRT por separado del tiempo de ejecución.
- Simetría con sistemas de interoperabilidad proporcionados para otros sistemas operativos (por ejemplo, iOS y Android).
- Uso de funciones NET como enlaces AOT e IL en el ecosistema WinRT.
- Simplificación de la base de código en tiempo de ejecución (~ 60k líneas de código eliminadas).
Objetos COM con la palabra clave dynamic
En .NET Core 3.xy versiones anteriores, la dynamicpalabra clave no funciona con objetos COM. Si bien el soporte existía en .NET Framework, la cantidad de código era grande y la lógica era compleja y especializada, por lo que el soporte no se incluyó en .NET Core. Gracias a los muchos desarrolladores que nos dejaron saber lo problemática que era para ellos esta falta de funcionalidad, sabíamos que necesitábamos agregar el soporte en .NET 5. dynamic
Ahora se admite el uso de palabras clave para objetos COM ( dotnet/runtime#33060 ).
Clasificación de genéricos blittable
El tiempo de ejecución no admitía la clasificación de tipos genéricos. Un intento de hacerlo daría como resultado una MarshalDirectiveException indicación de que los tipos genéricos no se pueden calcular. En .NET 5, se agregó soporte para la clasificación de genéricos blittable en P/Invokes ( dotnet/runtime#103 ). La clasificación de genéricos no transferibles sigue sin apoyo.
Más allá de .NET 5
A medida que nos acercamos al lanzamiento de .NET 5, también queríamos dar un vistazo a algunas de las cosas que estamos considerando para el futuro.
Analizadores de código
Los analizadores de código de Roslyn permiten obtener información y orientación inmediatas que forman parte directamente del ciclo de desarrollo de un usuario. La guía .NET contiene información sobre las mejores prácticas de interoperabilidad nativa , pero la lógica en torno a la clasificación y la interoperabilidad sigue siendo compleja y, a menudo, confusa.
Esperamos comenzar a invertir más en analizadores de código agregando reglas para las características existentes y asegurándonos de que se consideren nuevas reglas para cualquier característica nueva.
Generadores de fuentes
Al manejar la invocación de un P / Invoke, el tiempo de ejecución creará un flujo de instrucciones IL que es JIT-ed, generando un stub IL. Este modelo intenta ser una caja mágica de lógica de clasificación que ‘simplemente funciona’ y generalmente es opaca para el desarrollador. Sin embargo, tiene algunos inconvenientes importantes:
- El sistema de clasificación está acoplado al tiempo de ejecución, de modo que cualquier corrección de errores requiere una actualización de todo el tiempo de ejecución.
- Dado que el código de clasificación se genera en tiempo de ejecución, no está disponible para escenarios de compiladores con anticipación (AOT).
- Depurar el código auxiliar de IL de clasificación generado automáticamente es difícil para los desarrolladores en tiempo de ejecución y casi imposible para los consumidoresde P/Invokes.
Para aliviar estos problemas, planeamos utilizar generadores de código fuente para generar el código de clasificación necesario para P/Invokes en tiempo de compilación. Esto permitiría el lanzamiento y el desarrollo independientes del tiempo de ejecución, la compatibilidad con los escenarios AOT y una experiencia de depuración mejorada. Nuestras investigaciones iniciales sehan iniciado en dotnet/runtimelab .