Grokay DLR

Préface du traducteur

Il s'agit plus d'une nouvelle version gratuite que d'une traduction. J'ai inclus dans cet article uniquement les parties de l'original qui sont directement liées aux mécanismes internes du DLR ou expliquent des idées importantes. Les notes seront placées entre crochets.

De nombreux développeurs .NET ont entendu parler du Dynamic Language Runtime (DLR), mais n'en savent presque rien. Les développeurs écrivant dans des langages tels que C # ou Visual Basic évitent les langages de frappe dynamiques par crainte de problèmes d'évolutivité historiquement liés. Ils sont également préoccupés par le fait que des langages comme Python ou Ruby n'effectuent pas de vérification de type au moment de la compilation, ce qui peut entraîner des erreurs d'exécution difficiles à trouver et à corriger. Ce sont des craintes bien fondées qui peuvent expliquer pourquoi DLR n'est pas populaire parmi la majorité des développeurs .NET même deux ans après la sortie officielle [l'article est assez ancien, mais rien n'a changé depuis lors] . Après tout, tout Runtime .NET contenant les mots Dynamic et Language dans son nom doit être conçu strictement pour prendre en charge des langages tels que Python, non?

Ralentissez. Alors que DLR a vraiment été conçu pour prendre en charge l'implémentation Iron de Python et Ruby dans le .NET Framework, son architecture fournit des abstractions beaucoup plus profondes.



Sous le capot, DLR offre un riche ensemble d'interfaces pour la communication inter-processus [Communication inter-processus (IPC)]. Au fil des ans, les développeurs ont vu de nombreux outils Microsoft pour l'interaction entre les applications: DDE, DCOM, ActiveX, .Net Remoting, WCF, OData. Cette liste peut durer longtemps. Il s'agit d'un défilé d'acronymes presque sans fin, chacun représentant une technologie qui promet que cette année, il sera encore plus facile d'échanger des données ou d'appeler du code à distance qu'auparavant.

Langue des langues


La première fois que j'ai entendu Jim Hugunin parler de DLR, son discours m'a surpris. Jim a créé une implémentation Python pour la machine virtuelle Java (JVM) connue sous le nom de Jython. Peu avant le spectacle, il a rejoint Microsoft pour créer IronPython pour .NET. Sur la base de ses antécédents, je m'attendais à ce qu'il se concentre sur la langue, mais à la place, Jim a parlé presque tout le temps de choses abstruses comme les arborescences d'expression, la répartition dynamique des appels et les mécanismes de mise en cache des appels. Jim a décrit un ensemble de services de compilation à l'exécution qui permettaient à deux langues d'interagir entre elles sans pratiquement aucune perte de performances.

Au cours de ce discours, j'ai écrit un terme qui a fait surface dans ma tête quand j'ai entendu Jim raconter l'architecture DLR: le langage des langues. Quatre ans plus tard, ce surnom caractérise toujours très précisément le DLR. Cependant, après avoir acquis une expérience d'utilisation réelle, j'ai réalisé que DLR n'est pas seulement une question de compatibilité linguistique. Grâce à la prise en charge des types dynamiques en C # et Visual Basic, le DLR peut servir de passerelle entre nos langages .NET préférés et les données et le code dans n'importe quel système distant, quel que soit le type d'équipement ou de logiciel utilisé par ce dernier.



Pour comprendre l'idée derrière DLR, qui est un mécanisme intégré dans le langage IPC, commençons par un exemple qui n'a rien à voir avec la programmation dynamique. Imaginez deux systèmes informatiques: l'un appelé l'initiateur, et le second - le système cible. L'initiateur doit exécuter la fonction foo sur le système cible, en y passant un certain ensemble de paramètres, et obtenir les résultats. Une fois le système cible découvert, l'initiateur doit fournir toutes les informations nécessaires à l'exécution de la fonction dans un format qui lui est compréhensible. Au minimum, ces informations incluront le nom de la fonction et les paramètres transmis. Après avoir déballé la demande et validé les paramètres, le système cible exécutera la fonction foo. Après cela, il doit emballer le résultat, y compris toutes les erreurs survenues lors de l'exécution, et les renvoyer à l'initiateur. Enfin, l'initiateur doit être capable de décompresser les résultats et de notifier l'objectif. Ce modèle de demande-réponse est assez courant et décrit à un niveau élevé le fonctionnement de presque tous les mécanismes IPC.

Dynamicmetaobject


Pour comprendre comment DLR implémente le modèle présenté, regardons l'une des classes centrales de DLR: DynamicMetaObject . Nous commençons par explorer trois des douze méthodes clés de ce type:

  1. BindCreateInstance - créer ou activer un objet
  2. BindInvokeMember - appeler la méthode encapsulée
  3. BindInvoke - exécution d'objet (en tant que fonction)

Lorsque vous devez exécuter une méthode sur un système distant, vous devez d'abord créer une instance du type. Bien sûr, tous les systèmes ne sont pas orientés objet, le terme «instance» peut donc être une métaphore. En fait, le service dont nous avons besoin peut être implémenté en tant que pool d'objets ou en tant que singleton, de sorte que les termes «activation» ou «connexion» peuvent être utilisés avec le même droit que «instance».

D'autres cadres suivent le même modèle. Par exemple, COM fournit une fonction CoCreateInstance pour créer des objets. Dans .NET Remoting, vous pouvez utiliser la méthode CreateInstance de la classe System.Activator . DLR DynamicMetaObject fournit un BindCreateInstance à des fins similaires.

Après avoir utilisé la méthode BindCreateInstance , quelque chose créé peut être un type qui expose plusieurs méthodes. La méthode du métaobjet BindInvokeMember est utilisée pour lier une opération qui peut appeler une fonction. Dans l'image ci-dessus, la chaîne foo peut être passée en tant que paramètre pour indiquer au classeur qu'une méthode portant ce nom doit être appelée. En outre, des informations sur le nombre d'arguments, leurs noms et un indicateur spécial qui indique au classeur s'il est possible d'ignorer la casse lors de la recherche d'un élément nommé approprié sont également incluses. Après tout, toutes les langues ne sont pas sensibles à la casse.

Lorsque quelque chose renvoyé par BindCreateInstance n'est qu'une fonction (ou déléguée), la méthode BindInvoke est utilisée. Pour clarifier l'image, regardons le petit morceau de code dynamique suivant:

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

Ce code n'est pas le meilleur moyen d'imprimer le numéro 5 sur la console. Un bon développeur n'utilisera jamais rien de si inutile. Cependant, ce code illustre l'utilisation d'une variable dynamique dont la valeur est un délégué qui peut être utilisé comme fonction. Si le type délégué implémente l'interface IDynamicMetaObjectProvider , la méthode BindInvoke de DynamicMetaObject sera utilisée pour lier l'opération au travail réel. Cela est dû au fait que le compilateur reconnaît que l'objet d' écriture dynamique est utilisé syntaxiquement comme une fonction. Considérons maintenant un autre morceau de code pour comprendre quand le compilateur générera BindInvokeMember :

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

Je vais omettre l'implémentation de l'interface dans ce petit exemple, car il faudra beaucoup de code pour le démontrer correctement. Dans cet exemple abrégé, nous implémentons un méta-objet dynamique avec seulement quelques lignes de code.

Une chose importante à comprendre est que le compilateur reconnaît que Writer.Write (7) est une opération d'accès aux éléments. Ce que nous appelons habituellement l '«opérateur de point» en C # est officiellement appelé «opérateur d'accès aux membres de type». Le code DLR généré par le compilateur dans ce cas appellera finalement BindInvokeMember , dans lequel il passera la chaîne d'écriture et le numéro de paramètre 7 à l'opération qui est capable d'effectuer l'appel. En bref, BindInvoke est utilisé pour appeler un objet dynamique en tant que fonction, tandis que BindInvokeMember est utilisé pour appeler une méthode en tant qu'élément d'un objet dynamique.

Accéder aux propriétés via DynamicMetaObject


Les exemples ci-dessus montrent que le compilateur utilise la syntaxe du langage pour déterminer les opérations de liaison DLR à effectuer. Si vous utilisez Visual Basic pour travailler avec des objets dynamiques, sa sémantique sera utilisée. L'opérateur d'accès (point), bien sûr, est nécessaire non seulement pour accéder aux méthodes. Vous pouvez l'utiliser pour accéder aux propriétés. Le méta-objet DLR propose trois méthodes pour accéder aux propriétés des objets dynamiques:

  1. BindGetMember - obtenir la valeur de la propriété
  2. BindSetMember - définir la valeur de la propriété
  3. BindDeleteMember - supprimer un élément

Le but de BindGetMember et BindSetMember devrait être évident. Surtout maintenant que vous savez comment ils se rapportent au fonctionnement de .NET avec les propriétés. Lorsque le compilateur calcule les propriétés get ("read") d'un objet dynamique, il utilise un appel à BindGetMember . Lorsque le compilateur calcule set ("record"), il utilise BindSetMember .

Représentation d'un objet sous forme de tableau


Certaines classes sont des conteneurs pour les instances d'autres types. DLR sait comment gérer de tels cas. Chaque méthode de méta-objet «orientée tableau» a un suffixe «Index»:

  1. BindGetIndex - récupère la valeur par index
  2. BindSetIndex - définir la valeur par index
  3. BindDeleteIndex - supprime une valeur par index

Pour comprendre comment BindGetIndex et BindSetIndex sont utilisés , imaginez une classe wrapper JavaBridge qui peut charger des fichiers avec des classes Java et vous permet de les utiliser à partir du code .NET sans aucune difficulté. Un tel wrapper peut être utilisé pour charger la classe Java client , qui contient du code ORM. Le méta-objet DLR peut être utilisé pour appeler ce code ORM à partir de .NET dans le style C # classique. Voici un exemple de code qui montre comment JavaBridge peut fonctionner dans la pratique:

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

Étant donné que les troisième et cinquième lignes utilisent l'opérateur d'accès par index ([]), le compilateur reconnaît cela et utilise les méthodes BindGetIndex et BindSetIndex lorsqu'il travaille avec le méta-objet renvoyé par JavaBridge . Il est entendu que l'implémentation de ces méthodes sur l'objet retourné demandera l'exécution de la méthode à partir de la JVM via le Java Remote Method Invocation (RMI). Dans ce scénario, DLR agit comme un pont entre C # et un autre langage avec typage statique. J'espère que cela clarifie pourquoi j'ai appelé DLR «langue des langues».

La méthode BindDeleteMember , tout comme BindDeleteIndex , n'est pas destinée à être utilisée à partir de langages avec un typage statique comme C # et Visual Basic, car ils ne prennent pas en charge le concept lui-même. Cependant, vous pouvez accepter d'envisager de «supprimer» une opération exprimée par le biais de la langue, si cela vous est utile. Par exemple, vous pouvez implémenter BindDeleteMember comme annulant un élément par index.

Transformations et opérateurs


Le dernier groupe de méthodes de métaobjet DLR concerne la gestion des opérateurs et des transformations.

  1. BindConvert - convertir un objet en un autre type
  2. BindBinaryOperation - utilisation d'un opérateur binaire sur deux opérandes
  3. BindUnaryOperation - utilisation d'un opérateur unaire sur un opérande

La méthode BindConvert est utilisée lorsque le compilateur se rend compte que l'objet doit être converti en un autre type connu. La conversion implicite se produit lorsque le résultat d'un appel dynamique est affecté à une variable de type statique. Par exemple, dans l'exemple C # suivant, l'affectation de la variable y conduit à un appel implicite à BindConvert :

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

Les méthodes BindBinaryOperation et BindUnaryOperation sont toujours utilisées lorsque des opérations arithmétiques ("+") ou des incréments ("++") sont rencontrées. Dans l'exemple ci-dessus, l'ajout de la variable dynamique x à la constante 11 appellera la méthode BindBinaryOperation . Rappelez-vous ce petit exemple, nous l'utilisons dans la section suivante pour frapper une autre classe DLR clé appelée CallSite.

Envoi dynamique avec CallSite


Si votre introduction au DLR ne dépassait pas l'utilisation du mot-clé dynamique , vous n'auriez probablement jamais su l'existence de CallSite dans le .NET Framework. Ce type modeste, officiellement appelé CallSite < T > , réside dans l' espace de noms System.Runtime.CompilerServices . C'est la «source d'énergie» de la métaprogrammation: elle est remplie de toutes sortes de méthodes d'optimisation qui rendent le code .NET dynamique rapide et efficace. Je mentionnerai les aspects de performance de CallSite < T > à la fin de l'article.

La plupart de ce que CallSite fait dans du code .NET dynamique implique la génération et la compilation de code lors de l'exécution. Il est important de noter que la classe CallSite < T > se trouve dans l'espace de noms qui contient les mots " Runtime " et " CompilerServices ". Si DLR est un "langage des langues", alors CallSite < T > est l'une de ses constructions grammaticales les plus importantes. Regardons à nouveau notre exemple de la section précédente pour connaître CallSite et comment le compilateur les incorpore dans votre code.

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

Comme vous le savez déjà, les méthodes BindBinaryOperaion et BindConvert seront appelées pour exécuter ce code. Au lieu de vous montrer une longue liste du code MSIL désassemblé généré par le compilateur, j'ai fait un diagramme:



N'oubliez pas que le compilateur utilise la syntaxe du langage pour déterminer les méthodes de type dynamique à exécuter. Dans notre exemple, deux opérations sont effectuées: l'ajout de la variable x au nombre ( Site2 ) et la conversion du résultat en int ( Site1 ). Chacune de ces actions se transforme en CallSite, qui est stocké dans un conteneur spécial. Comme vous pouvez le voir dans le diagramme, les CallSites sont créés dans l'ordre inverse, mais sont appelés de la bonne manière.

Dans la figure, vous pouvez voir que les méthodes de métaobjet BindConvert et BindBinaryOperation sont appelées immédiatement avant les opérations «create CallSite1» et «create CallSite2». Cependant, les opérations liées ne sont effectuées qu'à la toute fin. J'espère que la visualisation vous aide à comprendre que les méthodes de liaison et les appeler sont des opérations différentes dans le contexte du DLR. De plus, la liaison ne se produit qu'une seule fois, tandis qu'un appel se produit autant de fois que nécessaire, réutilisant des CallSites déjà initialisés pour optimiser les performances.

Suivez la voie facile


Au cœur même du DLR, les arbres d'expression sont utilisés pour générer des fonctions liées aux douze méthodes de liaison présentées ci-dessus. De nombreux développeurs sont constamment confrontés à des arborescences d'expression utilisant LINQ, mais seuls quelques-uns ont une expérience suffisamment approfondie pour implémenter pleinement le contrat IDynamicMetaObjectProvider . Heureusement, le .NET Framework contient une classe de base appelée DynamicObject qui prend en charge la plupart du travail.

Pour créer votre propre classe dynamique, il vous suffit d'hériter de DynamicObject et d'implémenter les douze méthodes suivantes:

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

Les noms de méthode vous semblent-ils familiers? Vous devez le faire, car vous venez de terminer l'étude des éléments de la classe Abstract DynamicMetaObject , qui incluent des méthodes comme BindCreateInstance et BindInvoke . La classe DynamicMetaObject fournit une implémentation pour IDynamicMetaObjectProvider , qui renvoie un DynamicMetaObject à partir de sa seule méthode. Les opérations associées à l'implémentation de base du méta-objet délèguent simplement leurs appels aux méthodes commençant par «Try» sur l'instance DynamicObject . Tout ce que vous devez faire est de surcharger les méthodes comme TryGetMember et TrySetMember dans une classe héritée de DynamicObject , tandis que le méta-objet se chargera de tout le sale boulot avec les arborescences d'expressions.

Mise en cache


[Vous pouvez en savoir plus sur la mise en cache dans mon précédent article sur DLR ]

La plus grande préoccupation lorsque vous travaillez avec des langages dynamiques pour les développeurs est la performance. Le DLR prend des mesures extraordinaires pour dissiper ces expériences. J'ai brièvement mentionné le fait que CallSite < T > réside dans un espace de noms appelé System.Runtime.CompilerServices . Dans le même espace de noms se trouvent plusieurs autres classes qui fournissent une mise en cache à plusieurs niveaux. À l'aide de ces types, DLR implémente trois niveaux principaux de mise en cache pour accélérer les opérations dynamiques:

  1. Cache global
  2. Cache local
  3. Cache de délégué polymorphe

Le cache est utilisé afin d'éviter le gaspillage inutile de ressources pour créer des liaisons pour un CallSite spécifique. Si deux objets de type chaîne sont passés à une méthode dynamique qui renvoie int , le cache global ou local enregistre la liaison résultante. Cela simplifiera considérablement les appels ultérieurs.

Le cache des délégués, qui se trouve à l'intérieur de CallSite lui-même, est appelé polymorphe, car ces délégués peuvent prendre différentes formes selon le code dynamique exécuté et les règles des autres caches qui ont été utilisées pour les générer. Le cache des délégués est également parfois appelé cache en ligne. La raison de l'utilisation de ce terme est que les expressions générées par le DLR et leurs liants sont converties en code MSIL qui passe par la compilation JIT, comme tout autre code .NET. La compilation au moment de l'exécution se produit simultanément avec l'exécution «normale» de votre programme. Il est clair que transformer un code dynamique à la volée en code MSIL compilé pendant l'exécution du programme peut affecter considérablement les performances de l'application, les mécanismes de mise en cache sont donc essentiels.

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


All Articles