Grokay DLR

Vorwort des Übersetzers

Dies ist eher eine kostenlose Nacherzählung, keine Übersetzung. Ich habe in diesen Artikel nur diejenigen Teile des Originals aufgenommen, die in direktem Zusammenhang mit den internen Mechanismen des DLR stehen oder wichtige Ideen erläutern. Notizen werden in eckigen Klammern eingeschlossen.

Viele .NET-Entwickler haben von Dynamic Language Runtime (DLR) gehört, wissen aber fast nichts darüber. Entwickler, die in Sprachen wie C # oder Visual Basic schreiben, vermeiden dynamische Tippsprachen aus Angst vor historisch verwandten Skalierbarkeitsproblemen. Sie sind auch besorgt darüber, dass Sprachen wie Python oder Ruby beim Kompilieren keine Typprüfung durchführen, was zu Laufzeitfehlern führen kann, die schwer zu finden und zu beheben sind. Dies sind begründete Befürchtungen, die erklären könnten, warum das DLR auch zwei Jahre nach der offiziellen Veröffentlichung bei der Mehrheit der .NET-Entwickler nicht beliebt ist [der Artikel ist ziemlich alt, aber seitdem hat sich nichts geändert] . Schließlich sollte jede .NET- Laufzeit , deren Name die Wörter Dynamisch und Sprache enthält, ausschließlich so konzipiert sein, dass sie Sprachen wie Python unterstützt, oder?

Langsam fahren. Während das DLR wirklich darauf ausgelegt war, die Iron-Implementierung von Python und Ruby in .NET Framework zu unterstützen, bietet seine Architektur viel tiefere Abstraktionen.



Unter der Haube bietet das DLR eine Vielzahl von Schnittstellen für die prozessübergreifende Kommunikation [Inter-Process Communication (IPC)]. Im Laufe der Jahre haben Entwickler viele Microsoft-Tools für die Interaktion zwischen Anwendungen gesehen: DDE, DCOM, ActiveX, .Net Remoting, WCF, OData. Diese Liste kann lange dauern. Dies ist eine fast endlose Parade von Akronymen, von denen jedes eine Technologie darstellt, die verspricht, dass es in diesem Jahr noch einfacher sein wird, Daten auszutauschen oder Remote-Code aufzurufen als zuvor.

Sprache der Sprachen


Als ich Jim Hugunin zum ersten Mal über das DLR sprechen hörte, überraschte mich seine Rede. Jim erstellte eine Python-Implementierung für die Java Virtual Machine (JVM) namens Jython. Kurz vor der Show trat er Microsoft bei, um IronPython für .NET zu erstellen. Aufgrund seines Hintergrunds erwartete ich, dass er sich auf die Sprache konzentrieren würde, aber stattdessen sprach Jim fast die ganze Zeit über abstruse Dinge wie Ausdrucksbäume, dynamischer Anrufversand und Anruf-Caching-Mechanismen. Jim beschrieb eine Reihe von Laufzeitkompilierungsdiensten, mit denen zwei beliebige Sprachen praktisch ohne Leistungseinbußen miteinander interagieren konnten.

Während dieser Rede schrieb ich einen Begriff auf, der in meinem Kopf auftauchte, als ich hörte, wie Jim die DLR-Architektur nacherzählte: die Sprache der Sprachen. Vier Jahre später kennzeichnet dieser Spitzname das DLR noch sehr genau. Nachdem ich jedoch praktische Erfahrungen gesammelt hatte, wurde mir klar, dass es beim DLR nicht nur um Sprachkompatibilität geht. Dank der Unterstützung dynamischer Typen in C # und Visual Basic kann das DLR als Gateway von unseren bevorzugten .NET-Sprachen zu Daten und Code in jedem Remote-System fungieren, unabhängig davon, welche Art von Ausrüstung oder Software das letztere verwendet.



Um die Idee hinter dem DLR zu verstehen, einem integrierten Mechanismus in der IPC-Sprache, beginnen wir mit einem Beispiel, das nichts mit dynamischer Programmierung zu tun hat. Stellen Sie sich zwei Computersysteme vor: eines als Initiator und das zweite als Zielsystem. Der Initiator muss die Funktion foo auf dem Zielsystem ausführen, dort einen bestimmten Parametersatz übergeben und die Ergebnisse abrufen. Nachdem das Zielsystem erkannt wurde, muss der Initiator alle für die Ausführung der Funktion erforderlichen Informationen in einem für ihn verständlichen Format bereitstellen. Diese Informationen enthalten mindestens den Namen der Funktion und die übergebenen Parameter. Nach dem Entpacken der Anforderung und dem Überprüfen der Parameter führt das Zielsystem die Funktion foo aus. Danach sollte das Ergebnis einschließlich aller während der Ausführung aufgetretenen Fehler gepackt und an den Initiator zurückgesendet werden. Schließlich sollte der Initiator in der Lage sein, die Ergebnisse zu entpacken und das Ziel zu benachrichtigen. Dieses Anforderungs- / Antwortmuster ist weit verbreitet und beschreibt auf hoher Ebene die Funktionsweise fast aller IPC-Mechanismen.

Dynamisches Metaobjekt


Um zu verstehen, wie das DLR das dargestellte Muster implementiert, schauen wir uns eine der zentralen Klassen des DLR an: DynamicMetaObject . Wir beginnen mit der Untersuchung von drei der zwölf Schlüsselmethoden dieses Typs:

  1. BindCreateInstance - Erstellt oder aktiviert ein Objekt
  2. BindInvokeMember - Ruft die gekapselte Methode auf
  3. BindInvoke - Objektausführung (als Funktion)

Wenn Sie eine Methode auf einem Remote-System ausführen müssen, müssen Sie zuerst eine Instanz des Typs erstellen. Natürlich sind nicht alle Systeme objektorientiert, daher kann der Begriff „Instanz“ eine Metapher sein. Tatsächlich kann der von uns benötigte Dienst als Objektpool oder als Singleton implementiert werden, sodass die Begriffe "Aktivierung" oder "Verbindung" mit demselben Recht wie "Instanz" verwendet werden können.

Andere Frameworks folgen demselben Muster. Beispielsweise bietet COM eine CoCreateInstance- Funktion zum Erstellen von Objekten. In .NET Remoting können Sie die CreateInstance- Methode aus der System.Activator- Klasse verwenden. DLR DynamicMetaObject bietet eine BindCreateInstance für ähnliche Zwecke.

Nach der Verwendung der BindCreateInstance- Methode kann ein erstelltes Objekt ein Typ sein, der mehrere Methoden verfügbar macht. Die Metaobject-Methode BindInvokeMember wird verwendet, um eine Operation zu binden, die eine Funktion aufrufen kann. In der obigen Abbildung kann die Zeichenfolge foo als Parameter übergeben werden, um dem Ordner anzuzeigen, dass eine Methode mit diesem Namen aufgerufen werden soll. Zusätzlich sind Informationen über die Anzahl der Argumente, ihre Namen und ein spezielles Flag enthalten, das dem Ordner anzeigt, ob es möglich ist, Groß- und Kleinschreibung bei der Suche nach einem geeigneten benannten Element zu ignorieren. Schließlich wird bei nicht allen Sprachen zwischen Groß- und Kleinschreibung unterschieden.

Wenn etwas, das von BindCreateInstance zurückgegeben wird, nur eine Funktion (oder ein Delegat) ist, wird die BindInvoke-Methode verwendet. Schauen wir uns zur Verdeutlichung des Bildes den folgenden kleinen dynamischen Code an:

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

Dieser Code ist nicht der beste Weg, um die Nummer 5 auf der Konsole zu drucken. Ein guter Entwickler wird niemals etwas so Verschwenderisches verwenden. Dieser Code veranschaulicht jedoch die Verwendung einer dynamischen Variablen, deren Wert ein Delegat ist, der als Funktion verwendet werden kann. Wenn der Delegatentyp die IDynamicMetaObjectProvider- Schnittstelle implementiert, wird die BindInvoke- Methode von DynamicMetaObject verwendet, um die Operation an die eigentliche Arbeit zu binden. Dies liegt daran, dass der Compiler erkennt, dass das dynamische Write- Objekt syntaktisch als Funktion verwendet wird. Betrachten Sie nun einen weiteren Code, um zu verstehen, wann der Compiler BindInvokeMember generiert:

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

Ich werde die Implementierung der Schnittstelle in diesem kleinen Beispiel weglassen, da viel Code erforderlich ist, um dies korrekt zu demonstrieren. In diesem verkürzten Beispiel implementieren wir ein dynamisches Metaobjekt mit nur wenigen Codezeilen.

Es ist wichtig zu verstehen, dass der Compiler erkennt, dass Writer.Write (7) eine Elementzugriffsoperation ist. Was wir in C # normalerweise als "Punktoperator" bezeichnen, wird formal als "Typmitgliedszugriffsoperator" bezeichnet. Der in diesem Fall vom Compiler generierte DLR-Code ruft schließlich BindInvokeMember auf , in das er die Schreibzeichenfolge und die Parameternummer 7 an die Operation übergibt , die den Aufruf ausführen kann. Kurz gesagt, BindInvoke wird verwendet, um ein dynamisches Objekt als Funktion aufzurufen , während BindInvokeMember verwendet wird, um eine Methode als Element eines dynamischen Objekts aufzurufen.

Greifen Sie über DynamicMetaObject auf Eigenschaften zu


Aus den obigen Beispielen ist ersichtlich, dass der Compiler die Sprachsyntax verwendet, um zu bestimmen, welche DLR-Bindungsoperationen ausgeführt werden sollen. Wenn Sie Visual Basic zum Arbeiten mit dynamischen Objekten verwenden, wird dessen Semantik verwendet. Der Zugriffsoperator (Punkt) wird natürlich nicht nur für den Zugriff auf Methoden benötigt. Sie können damit auf Eigenschaften zugreifen. Das DLR-Metaobjekt bietet drei Methoden für den Zugriff auf die Eigenschaften dynamischer Objekte:

  1. BindGetMember - Liefert den Eigenschaftswert
  2. BindSetMember - Eigenschaftswert festlegen
  3. BindDeleteMember - löscht ein Element

Der Zweck von BindGetMember und BindSetMember sollte offensichtlich sein. Besonders jetzt, wo Sie wissen, wie sie sich auf die Funktionsweise von .NET mit Eigenschaften beziehen. Wenn der Compiler die get ("read") - Eigenschaften eines dynamischen Objekts berechnet, verwendet er einen Aufruf von BindGetMember . Wenn der Compiler set berechnet ("record"), verwendet er BindSetMember .

Darstellung eines Objekts als Array


Einige Klassen sind Container für Instanzen anderer Typen. Das DLR kann mit solchen Fällen umgehen. Jede "Array-orientierte" Metaobjektmethode hat einen "Index" -Postfix:

  1. BindGetIndex - Wert nach Index abrufen
  2. BindSetIndex - Wert nach Index festlegen
  3. BindDeleteIndex - Löscht einen Wert nach Index

Um zu verstehen, wie BindGetIndex und BindSetIndex verwendet werden , stellen Sie sich eine JavaBridge- Wrapper- Klasse vor , die Dateien mit Java-Klassen laden kann und die es Ihnen ermöglicht, sie problemlos aus .NET-Code zu verwenden. Ein solcher Wrapper kann zum Laden der Customer Java-Klasse verwendet werden, die ORM-Code enthält. Mit dem DLR-Metaobjekt kann dieser ORM-Code aus .NET im klassischen C # -Stil aufgerufen werden. Im Folgenden finden Sie einen Beispielcode, der zeigt, wie JavaBridge in der Praxis funktionieren kann:

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

Da die dritte und fünfte Zeile den Zugriffsoperator anhand des Index ([]) verwenden, erkennt der Compiler dies und verwendet die Methoden BindGetIndex und BindSetIndex, wenn er mit dem von JavaBridge zurückgegebenen Metaobjekt arbeitet . Es versteht sich, dass die Implementierung dieser Methoden auf dem zurückgegebenen Objekt die Ausführung der Methode von der JVM über den Java Remote Method Invocation (RMI) anfordert. In diesem Szenario fungiert das DLR als Brücke zwischen C # und einer anderen Sprache mit statischer Typisierung. Ich hoffe, dies verdeutlicht, warum ich das DLR als „Sprache der Sprachen“ bezeichnet habe.

Die BindDeleteMember- Methode ist genau wie BindDeleteIndex nicht für die Verwendung in Sprachen mit statischer Typisierung wie C # und Visual Basic vorgesehen, da sie das Konzept selbst nicht unterstützen. Sie können jedoch zustimmen, eine Operation, die durch die Sprache ausgedrückt wird, in Betracht zu ziehen, wenn dies für Sie nützlich ist. Beispielsweise können Sie BindDeleteMember so implementieren, dass ein Element nach Index auf Null gesetzt wird.

Transformationen und Operatoren


Die letzte Gruppe von DLR-Metaobjektmethoden befasst sich mit der Behandlung von Operatoren und Transformationen.

  1. BindConvert - Konvertiert ein Objekt in einen anderen Typ
  2. BindBinaryOperation - Verwenden eines Binäroperators für zwei Operanden
  3. BindUnaryOperation - Verwenden eines unären Operators für einen Operanden

Die BindConvert- Methode wird verwendet, wenn der Compiler erkennt, dass das Objekt in einen anderen bekannten Typ konvertiert werden muss. Eine implizite Konvertierung erfolgt, wenn das Ergebnis eines dynamischen Aufrufs einer Variablen mit einem statischen Typ zugewiesen wird. Im folgenden C # -Beispiel führt das Zuweisen der Variablen y beispielsweise zu einem impliziten Aufruf von BindConvert :

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

Die Methoden BindBinaryOperation und BindUnaryOperation werden immer verwendet, wenn arithmetische Operationen ("+") oder Inkremente ("++") auftreten. Im obigen Beispiel ruft das Hinzufügen der dynamischen Variablen x zur Konstante 11 die BindBinaryOperation- Methode auf. Denken Sie an dieses kleine Beispiel, wir verwenden es im nächsten Abschnitt, um eine andere wichtige DLR-Klasse namens CallSite zu knallen.

Dynamischer Versand mit CallSite


Wenn Ihre Einführung in das DLR nicht über die Verwendung des dynamischen Schlüsselworts hinausgegangen wäre, hätten Sie wahrscheinlich nie von der Existenz von CallSite in .NET Framework gewusst. Dieser bescheidene Typ, der formal als CallSite < T > bezeichnet wird , befindet sich im System.Runtime.CompilerServices-Namespace . Dies ist die „Energiequelle“ der Metaprogrammierung: Sie enthält alle Arten von Optimierungsmethoden, die dynamischen .NET-Code schnell und effizient machen. Ich werde die Leistungsaspekte von CallSite < T > am Ende des Artikels erwähnen.

Das meiste, was CallSite in dynamischem .NET-Code tut, besteht darin, Code zur Laufzeit zu generieren und zu kompilieren. Es ist wichtig zu beachten, dass die CallSite < T > -Klasse im Namespace liegt, der die Wörter " Runtime " und " CompilerServices " enthält. Wenn das DLR eine "Sprache der Sprachen" ist, ist CallSite < T > eine seiner wichtigsten grammatikalischen Konstruktionen. Schauen wir uns noch einmal unser Beispiel aus dem vorherigen Abschnitt an, um CallSite kennenzulernen und zu erfahren, wie der Compiler sie in Ihren Code einbettet.

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

Wie Sie bereits wissen, werden die Methoden BindBinaryOperaion und BindConvert aufgerufen, um diesen Code auszuführen. Anstatt Ihnen eine lange Liste des vom Compiler generierten zerlegten MSIL-Codes zu zeigen, habe ich ein Diagramm erstellt:



Denken Sie daran, dass der Compiler die Sprachsyntax verwendet, um zu bestimmen, welche dynamischen Typmethoden ausgeführt werden sollen. In unserem Beispiel werden zwei Operationen ausgeführt: Hinzufügen der Variablen x zur Zahl ( Site2 ) und Umwandeln des Ergebnisses in int ( Site1 ). Jede dieser Aktionen wird zu CallSite, die in einem speziellen Container gespeichert wird. Wie Sie im Diagramm sehen können, werden CallSites in umgekehrter Reihenfolge erstellt, aber auf die richtige Weise aufgerufen.

In der Abbildung sehen Sie, dass die Metaobjektmethoden BindConvert und BindBinaryOperation unmittelbar vor den Operationen "create CallSite1" und "create CallSite2" aufgerufen werden. Gebundene Operationen werden jedoch nur ganz am Ende ausgeführt. Ich hoffe, die Visualisierung hilft Ihnen zu verstehen, dass Bindungsmethoden und deren Aufruf im Kontext des DLR unterschiedliche Operationen sind. Darüber hinaus erfolgt die Bindung nur einmal, während der Aufruf so oft wie nötig erfolgt, wobei die bereits initialisierten CallSites zur Optimierung der Leistung wiederverwendet werden.

Folgen Sie dem einfachen Weg


Im Herzen des DLR werden Expressionsbäume verwendet, um Funktionen zu generieren, die an die oben dargestellten zwölf Bindungsmethoden gebunden sind. Viele Entwickler sind ständig mit Ausdrucksbäumen konfrontiert, die LINQ verwenden, aber nur wenige verfügen über ausreichend Erfahrung, um den IDynamicMetaObjectProvider- Vertrag vollständig zu implementieren. Glücklicherweise enthält .NET Framework eine Basisklasse namens DynamicObject , die den größten Teil der Arbeit erledigt .

Um Ihre eigene dynamische Klasse zu erstellen, müssen Sie lediglich von DynamicObject erben und die folgenden zwölf Methoden implementieren:

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

Kommen Ihnen die Methodennamen bekannt vor? Sie müssen, weil Sie gerade die Elemente der Abstract DynamicMetaObject- Klasse studiert haben, die Methoden wie BindCreateInstance und BindInvoke enthalten . Die DynamicMetaObject- Klasse bietet eine Implementierung für IDynamicMetaObjectProvider , die ein DynamicMetaObject von ihrer einzigen Methode zurückgibt. Die Operationen, die der Basisimplementierung des Metaobjekts zugeordnet sind, delegieren ihre Aufrufe einfach an Methoden, die mit "Try" in der DynamicObject- Instanz beginnen. Sie müssen lediglich Methoden wie TryGetMember und TrySetMember in der von DynamicObject geerbten Klasse überladen , während das Metaobjekt die gesamte Drecksarbeit mit Ausdrucksbäumen übernimmt.

Caching


[Weitere Informationen zum Caching finden Sie in meinem vorherigen Artikel zum DLR. ]

Das größte Problem bei der Arbeit mit dynamischen Sprachen für Entwickler ist die Leistung. Das DLR ergreift außerordentliche Maßnahmen, um diese Erfahrungen zu zerstreuen. Ich habe kurz die Tatsache erwähnt, dass sich CallSite < T > in einem Namespace namens System.Runtime.CompilerServices befindet . Im selben Namespace befinden sich mehrere andere Klassen, die mehrstufiges Caching bereitstellen. Mit diesen Typen implementiert das DLR drei Hauptstufen des Caching, um dynamische Vorgänge zu beschleunigen:

  1. Globaler Cache
  2. Lokaler Cache
  3. Polymorpher Delegaten-Cache

Der Cache wird verwendet, um unnötige Ressourcenverschwendung beim Erstellen von Bindungen für eine bestimmte CallSite zu vermeiden. Wenn zwei Objekte vom Typ string an eine dynamische Methode übergeben werden, die int zurückgibt, speichert der globale oder lokale Cache die resultierende Bindung. Dies vereinfacht nachfolgende Anrufe erheblich.

Der Delegat-Cache, der sich in CallSite selbst befindet, wird als polymorph bezeichnet, da diese Delegaten unterschiedliche Formen annehmen können, je nachdem, welcher dynamische Code ausgeführt wird und welche Regeln aus anderen Caches zum Generieren verwendet wurden. Der Delegaten-Cache wird manchmal auch als Inline-Cache bezeichnet. Der Grund für die Verwendung dieses Begriffs besteht darin, dass die vom DLR und ihren Bindemitteln generierten Ausdrücke wie jeder andere .NET-Code in MSIL-Code konvertiert werden, der die JIT-Kompilierung durchläuft. Die Kompilierung zur Laufzeit erfolgt gleichzeitig mit der „normalen“ Ausführung Ihres Programms. Es ist klar, dass die Umwandlung von dynamischem On-the-Fly-Code in kompilierten MSIL-Code während der Programmausführung die Anwendungsleistung erheblich beeinträchtigen kann. Daher sind Caching-Mechanismen von entscheidender Bedeutung.

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


All Articles