Grokay DLR

Prefacio del traductor

Esto es más un recuento gratuito, no una traducción. Incluí en este artículo solo aquellas partes del original que están directamente relacionadas con los mecanismos internos de DLR o explican ideas importantes. Las notas se incluirán entre corchetes.

Muchos desarrolladores de .NET han escuchado sobre Dynamic Language Runtime (DLR), pero no saben casi nada al respecto. Los desarrolladores que escriben en lenguajes como C # o Visual Basic evitan los lenguajes de tipeo dinámico por temor a problemas de escalabilidad históricamente relacionados. También están preocupados por el hecho de que lenguajes como Python o Ruby no realizan verificaciones de tipo en tiempo de compilación, lo que puede conducir a errores de tiempo de ejecución que son difíciles de encontrar y corregir. Estos son temores bien fundados que pueden explicar por qué DLR no es popular entre la mayoría de los desarrolladores de .NET incluso dos años después del lanzamiento oficial [el artículo es bastante antiguo, pero desde entonces nada ha cambiado] . Después de todo, cualquier .NET Runtime que contenga las palabras Dinámico e Idioma en su nombre debe diseñarse estrictamente para admitir lenguajes como Python, ¿verdad?

Más despacio Si bien DLR fue realmente diseñado para admitir la implementación Iron de Python y Ruby en .NET Framework, su arquitectura proporciona abstracciones mucho más profundas.



Bajo el capó, DLR ofrece un rico conjunto de interfaces para la comunicación entre procesos [Comunicación entre procesos (IPC)]. Con los años, los desarrolladores han visto muchas herramientas de Microsoft para la interacción entre aplicaciones: DDE, DCOM, ActiveX, .Net Remoting, WCF, OData. Esta lista puede continuar por mucho tiempo. Este es un desfile de acrónimos casi interminable, cada uno de los cuales representa una tecnología que promete que este año será aún más fácil intercambiar datos o llamar a código remoto que antes.

Lengua de lenguas


La primera vez que escuché a Jim Hugunin hablar sobre DLR, su discurso me sorprendió. Jim creó una implementación de Python para la máquina virtual Java (JVM) conocida como Jython. Poco antes del espectáculo, se unió a Microsoft para crear IronPython para .NET. Según sus antecedentes, esperaba que se concentrara en el lenguaje, pero en cambio, Jim hablaba casi todo el tiempo sobre cosas abstrusas como árboles de expresión, despacho dinámico de llamadas y mecanismos de almacenamiento en caché de llamadas. Jim describió un conjunto de servicios de compilación en tiempo de ejecución que permitían que dos idiomas interactuaran entre sí prácticamente sin pérdida de rendimiento.

Durante este discurso, escribí un término que surgió en mi cabeza cuando escuché a Jim volver a contar la arquitectura DLR: el lenguaje de los idiomas. Cuatro años después, este apodo todavía caracteriza a DLR con mucha precisión. Sin embargo, habiendo adquirido experiencia de uso en el mundo real, me di cuenta de que DLR no se trata solo de compatibilidad de idiomas. Gracias al soporte de tipos dinámicos en C # y Visual Basic, DLR puede actuar como una puerta de enlace desde nuestros lenguajes .NET favoritos a datos y códigos en cualquier sistema remoto, independientemente del tipo de equipo o software que use este último.



Para comprender la idea detrás de DLR, que es un mecanismo integrado en el lenguaje IPC, comencemos con un ejemplo que no tiene nada que ver con la programación dinámica. Imagine dos sistemas informáticos: uno llamado iniciador y el segundo, el sistema de destino. El iniciador necesita ejecutar la función foo en el sistema de destino, pasar allí un cierto conjunto de parámetros y obtener los resultados. Una vez que se descubre el sistema de destino, el iniciador debe proporcionar toda la información necesaria para la ejecución de la función en un formato que le sea comprensible. Como mínimo, esta información incluirá el nombre de la función y los parámetros pasados. Después de desempaquetar la solicitud y validar los parámetros, el sistema objetivo ejecutará la función foo. Después de eso, debe empaquetar el resultado, incluidos los errores que ocurrieron durante la ejecución, y enviarlos de vuelta al iniciador. Finalmente, el iniciador debería poder desempaquetar los resultados y notificar el objetivo. Este patrón de solicitud-respuesta es bastante común y en un alto nivel describe el funcionamiento de casi cualquier mecanismo de IPC.

Dynamicmetaobject


Para comprender cómo DLR implementa el patrón presentado, veamos una de las clases centrales de DLR: DynamicMetaObject . Comenzamos explorando tres de los doce métodos clave de este tipo:

  1. BindCreateInstance: crea o activa un objeto
  2. BindInvokeMember: llame al método encapsulado
  3. BindInvoke - ejecución de objetos (como una función)

Cuando necesite ejecutar un método en un sistema remoto, primero debe crear una instancia del tipo. Por supuesto, no todos los sistemas están orientados a objetos, por lo que el término "instancia" puede ser una metáfora. De hecho, el servicio que necesitamos puede implementarse como un conjunto de objetos o como un singleton, de modo que los términos "activación" o "conexión" puedan usarse con el mismo derecho que "instancia".

Otros marcos siguen el mismo patrón. Por ejemplo, COM proporciona una función CoCreateInstance para crear objetos. En .NET Remoting, puede usar el método CreateInstance de la clase System.Activator . DLR DynamicMetaObject proporciona una BindCreateInstance para fines similares.

Después de usar el método BindCreateInstance , algo creado puede ser un tipo que expone varios métodos. El método de metaobjeto BindInvokeMember se usa para vincular una operación que puede llamar a una función. En la imagen de arriba, la cadena foo se puede pasar como un parámetro para indicar al archivador que se debe llamar a un método con ese nombre. Además, se incluye información sobre el número de argumentos, sus nombres y un indicador especial que indica al archivador si es posible ignorar mayúsculas y minúsculas cuando se busca un elemento con nombre adecuado. Después de todo, no todos los idiomas distinguen entre mayúsculas y minúsculas.

Cuando algo devuelto por BindCreateInstance es solo una función (o delegado), se utiliza el método BindInvoke. Para aclarar la imagen, veamos el siguiente fragmento pequeño de código dinámico:

delegate void IntWriter(int n); void Main() { dynamic Write = new IntWriter(Console.WriteLine); Write(5); } 

Este código no es la mejor manera de imprimir el número 5 en la consola. Un buen desarrollador nunca usará nada tan derrochador. Sin embargo, este código ilustra el uso de una variable dinámica cuyo valor es un delegado que se puede usar como una función. Si el tipo delegado implementa la interfaz IDynamicMetaObjectProvider , entonces el método BindInvoke de DynamicMetaObject se usará para vincular la operación al trabajo real. Esto se debe a que el compilador reconoce que el objeto de escritura dinámica se usa sintácticamente como una función. Ahora considere otra pieza de código para entender cuándo el compilador generará BindInvokeMember :

 class Writer : IDynamicMetaObjectProvider { public void Write(int n) { Console.WriteLine(n); } //    } void Main() { dynamic Writer = new Writer(); Writer.Write(7); } 

Omitiré la implementación de la interfaz en este pequeño ejemplo, porque tomará mucho código para demostrar esto correctamente. En este ejemplo abreviado, implementamos un metaobjeto dinámico con solo unas pocas líneas de código.

Una cosa importante a entender es que el compilador reconoce que Writer.Write (7) es una operación de acceso a elementos. Lo que generalmente llamamos el "operador de punto" en C # se llama formalmente el "operador de acceso de miembro de tipo". El código DLR generado por el compilador en este caso eventualmente llamará a BindInvokeMember , al cual pasará la cadena Escribir y el número de parámetro 7 a la operación que es capaz de realizar la llamada. En resumen, BindInvoke se usa para llamar a un objeto dinámico como una función, mientras que BindInvokeMember se usa para llamar a un método como un elemento de un objeto dinámico.

Acceda a propiedades a través de DynamicMetaObject


Se puede ver en los ejemplos anteriores que el compilador usa la sintaxis del lenguaje para determinar qué operaciones de enlace DLR deben realizarse. Si usa Visual Basic para trabajar con objetos dinámicos, se utilizará su semántica. El operador de acceso (punto), por supuesto, es necesario no solo para acceder a los métodos. Puede usarlo para acceder a las propiedades. El metaobjeto DLR proporciona tres métodos para acceder a las propiedades de los objetos dinámicos:

  1. BindGetMember - obtiene el valor de la propiedad
  2. BindSetMember - establece el valor de la propiedad
  3. BindDeleteMember - eliminar un elemento

El propósito de BindGetMember y BindSetMember debería ser obvio. Especialmente ahora que sabe cómo se relacionan con cómo funciona .NET con las propiedades. Cuando el compilador calcula las propiedades get ("lectura") de un objeto dinámico, utiliza una llamada a BindGetMember . Cuando el compilador calcula el conjunto ("registro"), utiliza BindSetMember .

Representación de un objeto como una matriz.


Algunas clases son contenedores para instancias de otros tipos. DLR sabe cómo manejar tales casos. Cada método de metaobjeto "orientado a la matriz" tiene un postfix "Índice":

  1. BindGetIndex - obtener valor por índice
  2. BindSetIndex - establece el valor por índice
  3. BindDeleteIndex: elimina un valor por índice

Para comprender cómo se usan BindGetIndex y BindSetIndex , imagine una clase de contenedor JavaBridge que pueda cargar archivos con clases Java y le permita usarlos desde el código .NET sin ninguna dificultad. Tal contenedor puede usarse para cargar la clase Java del Cliente , que contiene algún código ORM. El metaobjeto DLR se puede usar para llamar a este código ORM desde .NET en el estilo clásico de C #. A continuación se muestra un código de muestra que muestra cómo JavaBridge puede funcionar en la práctica:

 JavaBridge java = new JavaBridge(); dynamic customers = java.Load("Customer.class"); dynamic Jason = customers["Bock"]; Jason.Balance = 17.34; customers["Wagner"] = new Customer("Bill"); 

Como las líneas tercera y quinta usan el operador de acceso por índice ([]), el compilador reconoce esto y usa los métodos BindGetIndex y BindSetIndex cuando trabaja con el metaobjeto devuelto por JavaBridge . Se entiende que la implementación de estos métodos en el objeto devuelto solicitará la ejecución del método de la JVM a través de la Invocación remota de métodos (RMI) de Java. En este escenario, DLR actúa como un puente entre C # y otro lenguaje con tipeo estático. Espero que esto aclare por qué llamé DLR "lenguaje de idiomas".

El método BindDeleteMember , al igual que BindDeleteIndex , no está diseñado para su uso desde lenguajes con escritura estática como C # y Visual Basic, ya que no admiten el concepto en sí. Sin embargo, puede aceptar considerar "eliminar" alguna operación expresada por los medios del lenguaje, si eso es útil para usted. Por ejemplo, puede implementar BindDeleteMember como anulación de un elemento por índice.

Transformaciones y Operadores


El último grupo de métodos de metaobjetos de DLR trata sobre el manejo de operadores y transformaciones.

  1. BindConvert - convierte un objeto a otro tipo
  2. BindBinaryOperation: uso de un operador binario en dos operandos
  3. BindUnaryOperation: uso de un operador unario en un operando

El método BindConvert se usa cuando el compilador se da cuenta de que el objeto debe convertirse a otro tipo conocido. La conversión implícita se produce cuando el resultado de una llamada dinámica se asigna a una variable con un tipo estático. Por ejemplo, en el siguiente ejemplo de C #, la asignación de la variable y conduce a una llamada implícita a BindConvert :

 dynamic x = 13; int y = x + 11; 

Los métodos BindBinaryOperation y BindUnaryOperation siempre se usan cuando se encuentran operaciones aritméticas ("+") o incrementos ("++"). En el ejemplo anterior, agregar la variable dinámica x a la constante 11 llamará al método BindBinaryOperation . Recuerde este pequeño ejemplo, lo usamos en la siguiente sección para golpear otra clase DLR clave llamada CallSite.

Despacho dinámico con CallSite


Si su introducción a DLR no fue más allá del uso de la palabra clave dinámica , entonces probablemente nunca habría sabido sobre la existencia de CallSite en .NET Framework. Este tipo modesto, conocido formalmente como CallSite < T > , reside en el espacio de nombres System.Runtime.CompilerServices . Esta es la "fuente de energía" de la metaprogramación: está llena de todo tipo de métodos de optimización que hacen que el código dinámico .NET sea rápido y eficiente. Mencionaré los aspectos de rendimiento de CallSite < T > al final del artículo.

La mayor parte de lo que CallSite hace en código .NET dinámico implica generar y compilar código en tiempo de ejecución. Es importante tener en cuenta que la clase CallSite < T > se encuentra en el espacio de nombres que contiene las palabras " Runtime " y " CompilerServices ". Si DLR es un "lenguaje de idiomas", CallSite < T > es una de sus construcciones gramaticales más importantes. Miremos nuestro ejemplo de la sección anterior nuevamente para conocer CallSite y cómo el compilador los integra en su código.

 dynamic x = 13; int y = x + 11; 

Como ya sabe, se invocarán los métodos BindBinaryOperaion y BindConvert para ejecutar este código. En lugar de mostrarle una larga lista del código MSIL desmontado generado por el compilador, hice un diagrama:



Recuerde que el compilador usa la sintaxis del lenguaje para determinar qué métodos de tipo dinámico ejecutar. En nuestro ejemplo, se realizan dos operaciones: agregar la variable x al número ( Sitio2 ) y transmitir el resultado a int ( Sitio1 ). Cada una de estas acciones se convierte en CallSite, que se almacena en un contenedor especial. Como puede ver en el diagrama, los CallSites se crean en el orden inverso, pero se llaman de la manera correcta.

En la figura puede ver que los métodos de metaobjetos BindConvert y BindBinaryOperation se llaman inmediatamente antes de las operaciones "create CallSite1" y "create CallSite2". Sin embargo, las operaciones enlazadas solo se realizan al final. Espero que la visualización le ayude a comprender que los métodos vinculantes y llamarlos son operaciones diferentes en el contexto de DLR. Además, el enlace ocurre solo una vez, mientras que una llamada ocurre tantas veces como sea necesario, reutilizando CallSites ya inicializados para optimizar el rendimiento.

Sigue el camino fácil


En el corazón mismo de DLR, los árboles de expresión se utilizan para generar funciones vinculadas a los doce métodos de enlace presentados anteriormente. Muchos desarrolladores se enfrentan constantemente a árboles de expresión que utilizan LINQ, pero solo unos pocos tienen la experiencia lo suficientemente profunda como para implementar completamente el contrato IDynamicMetaObjectProvider . Afortunadamente, .NET Framework contiene una clase base llamada DynamicObject que se encarga de la mayor parte del trabajo.

Para crear su propia clase dinámica, todo lo que tiene que hacer es heredar de DynamicObject e implementar los siguientes doce métodos:

  1. TryCreateInstance
  2. TryInvokeMember
  3. Tryinvoke
  4. TryGetMember
  5. TrySetMember
  6. TryDeleteMember
  7. TryGetIndex
  8. TrySetIndex
  9. TryDeleteIndex
  10. Tryconvert
  11. TryBinaryOperation
  12. TryUnaryOperation

¿Los nombres de los métodos parecen familiares? Debe hacerlo, porque acaba de terminar de estudiar los elementos de la clase Abstract DynamicMetaObject , que incluye métodos como BindCreateInstance y BindInvoke . La clase DynamicMetaObject proporciona una implementación para IDynamicMetaObjectProvider , que devuelve DynamicMetaObject desde su único método. Las operaciones asociadas con la implementación base del metaobjeto simplemente delegan sus llamadas a métodos que comienzan con "Probar" en la instancia de DynamicObject . Todo lo que necesita hacer es sobrecargar métodos como TryGetMember y TrySetMember en una clase heredada de DynamicObject , mientras que el metaobjeto asumirá todo el trabajo sucio con árboles de expresión.

Almacenamiento en caché


[Puede leer más sobre el almacenamiento en caché en mi artículo anterior sobre DLR ]

La mayor preocupación al trabajar con lenguajes dinámicos para desarrolladores es el rendimiento. DLR toma medidas extraordinarias para disipar estas experiencias. Mencioné brevemente el hecho de que CallSite < T > reside en un espacio de nombres llamado System.Runtime.CompilerServices . En el mismo espacio de nombres se encuentran varias otras clases que proporcionan almacenamiento en caché multinivel. Con estos tipos, DLR implementa tres niveles principales de almacenamiento en caché para acelerar las operaciones dinámicas:

  1. Caché global
  2. Caché local
  3. Caché de delegado polimórfico

El caché se usa para evitar el desperdicio innecesario de recursos para crear enlaces para un CallSite específico. Si se pasan dos objetos de tipo cadena a un método dinámico que devuelve int , entonces el caché global o local guardará el enlace resultante. Esto simplificará enormemente las llamadas posteriores.

El caché de delegados, que se encuentra dentro de CallSite, se llama polimórfico, porque estos delegados pueden tomar diferentes formas dependiendo de qué código dinámico se ejecute y qué reglas de otros cachés se usaron para generarlos. El caché delegado también se denomina a veces caché en línea. La razón para usar este término es que las expresiones generadas por el DLR y sus carpetas se convierten en código MSIL que pasa a través de la compilación JIT, como cualquier otro código .NET. La compilación en tiempo de ejecución ocurre simultáneamente con la ejecución "normal" de su programa. Está claro que convertir el código dinámico sobre la marcha en código MSIL compilado durante la ejecución del programa puede afectar en gran medida el rendimiento de la aplicación, por lo que los mecanismos de almacenamiento en caché son vitales.

Source: https://habr.com/ru/post/469075/


All Articles