Verwalteter Code hinter den Kulissen, Teil 2

Von Peter Koen

In diesem Teil der Artikelserie steigen Sie tiefer in MSIL ein und erfahren, wie Sie bedingten Code schreiben, Schleifen formulieren und Fehler behandeln.

dot.net magazin


Diesen Artikel können Sie dank freundlicher Unterstützung von dot.net magazin auf MSDN Online lesen. dot.net magazin ist ein Partner von MSDN Online.


Zur Partnerübersichtsseite von MSDN Online

Der erste Teil hat die Grundlagen der Ausführung von verwaltetem Code erläutert und eine erste Intermediate-Language-Assembler-Anwendung gezeigt - das berühmt-berüchtigte Hello-World-Beispiel.Im zweiten Teil lernen Sie, wie Sie den Programmfluss steuern, Variablen deklarieren, eigene Methoden erzeugen und diese Methoden aufrufen. Darüber hinaus fügen Sie weitere Metadaten in Ihre Assemblies ein. Objektorientierte Themen wie zum Beispiel das Erstellen eigener Klassen, Aufrufen von Instanzmethoden, Erzeugen von Eigenschaften und Feldern usw. bleiben dem dritten Teil der Serie vorbehalten.

Auf dieser Seite

 Ein einfaches Arithmetikbeispiel
 Flusssteuerung
 Die Beispielanwendung
 Bedingungsanweisungen
 Schleifen in IL
 Ausnahmebehandlung
 Lokale Variablen
 Überwachte Blöcke
 Links & Literatur
 Über den Autor

Ein einfaches Arithmetikbeispiel

Die Funktionalität der Hello-World-Beispielanwendung ist (natürlich) kaum der Rede wert. Dagegen leistet die im Folgenden vorgestellte kleine Arithmetikanwendung echte Knochenarbeit - sie berechnet das Volumen einer Kugel. Das Programm fragt vom Benutzer einen Gleitkommawert für den Radius der Kugel ab und ruft eine Methode auf, die das Volumen berechnet und als Rückgabewert liefert. Am Schluss gibt das Programm den Wert auf dem Bildschirm aus. Zuerst müssen Sie das Manifest für Ihre Assembly an den Anfang der Quellcodedatei schreiben. Gegenüber der Hello-World-Anwendung aus dem ersten Teil enthält das Manifest jetzt ein paar Einträge mehr:

.assembly Sphere
{
   .ver 1:0:0:0
}
.assembly extern mscorlib
{
   .ver 1:0:500:0
}
.module Sphere.exe
.subsystem 0x00000003

Sie müssen den Namen der Assembly, die Sie erstellen, angeben. Dieses Mal enthält der Code aber auch eine Versionsdeklaration. Die .ver-Direktive weist den Compiler an, die Versionsnummer 1.0.0.0 in die Metadaten Ihrer Assembly zu schreiben. Außerdem gibt es eine zweite .assembly-Direktive, jetzt aber mit dem zusätzlichen Schlüsselwort external, was bedeutet, dass dies ein Verweis auf eine externe Assembly ist. Das ist vergleichbar mit dem Parameter/r des C#-Compilers Csc.exe, der andere Assemblies referenziert, die für das Kompilieren der Assembly erforderlich sind. Im Beispiel verweist die .ver-Direktive nach dem zweiten .assembly auf die Version 1.1 des .NET Framework. Wenn Sie den Assembly-Code ohne diese Versionsdirektive schreiben würden, gilt die Standardversion 0.0.0.0 des Framework.

Die .module-Direktive spezifiziert den Namen des Moduls (praktisch einen Dateinamen), wohin der Compiler die kompilierte Assembly schreibt. Als letzte Direktive im Manifest gibt .subsystem mit dem Wert 3 an, dass es sich bei der ausführbaren Datei um eine Konsolenanwendung handelt. Diesen Eintrag können Sie weglassen, da eine Konsolenanwendung ohnehin das Standardziel ist. Nachdem Sie das Manifest fertig gestellt haben, können Sie den restlichen Code schreiben. Er ist nicht lang. Listing 1 gibt ihn komplett wieder.

.method static void main() cil managed
{
  .entrypoint
  .maxstack 2
  ldstr "Radius: "
  call void [mscorlib]System.Console::Write(string)
  call string [mscorlib]System.Console::ReadLine()
  call float64 [mscorlib]System.Double::Parse(string)
  call float64 CalcSphereVolume(float64)
  ldstr "Volumen: "
  call void [mscorlib]System.Console::Write(string)
  call void [mscorlib]System.Console::WriteLine(float64)
  ret
}   
      

Listing 1

Die Methodendeklaration .main ist die gleiche wie im Hello-World-Beispiel, jedoch besitzt jetzt die .maxstack-Direktivex den Wert 2, weil auf dem Stack zwei Elemente gleichzeitig vorhanden sein müssen- um den Speicherbereich zu erstellen, der für das Speichern und Abrufen eines Wertes in eine bzw. aus einer lokale(n) Variable(n) erforderlich ist. Wenn Sie sich noch einmal den ersten Teil dieses Artikels in Erinnerung rufen, fällt Ihnen sicherlich auf, dass die beiden ersten "richtigen" Codezeilen praktisch die gleichen wie im Hello-World-Beispiel sind. An diese schließt sich ein Aufruf der Methode System.Console::ReadLine an, die beim Rücksprung einen String auf den Auswertungs-Stack legt. Der Code ist grundsätzlich mit dem Aufruf von System.Console.ReadLine() in C# äquivalent. Sämtliche Eingaben von der Konsole kommen in Form eines String, der die vom Benutzer eingetippten Zeichen enthält. Deshalb müssen Sie den String-Wert in einen 64-Bit-Gleitkommawert konvertieren. Vielleicht erledigen Sie das sonst mit der Klasse Convert, dieses Beispiel verwendet dagegen die Methode System.Double.Parse, um die unterschiedlichen Typnamen von CLI CTS und Hochsprachen wie zum Beispiel C# zu demonstrieren. Beachten Sie, dass es bislang keine Fehlerprüfung oder -behandlung für den Konvertierungscode gibt. Das holen Sie später nach.

Momentan befindet sich ein float64-Element auf dem Auswertungs-Stack, das den Radius der Kugel angibt. Den Berechnungscode könnte man zwar "inline" schreiben, doch wäre das zu einfach. Stattdessen implementieren Sie eine Methode, die das Volumen der Kugel als obersten Eintrag auf den Auswertungs-Stack zurückgibt. Diese Methode lernen Sie später kennen. Zunächst einmal ist festzustellen, dass der Code die Methode CalcSphere-Volume aufruft, ohne ihr einen voll gekennzeichneten Namen zu geben; der Aufruf spezifiziert nur den Rückgabetyp, den Methodennamen und die Parameterliste. Das hängt damit zusammen, dass Calc-SphereVolume eine globale statische Funktion ist, die in derselben Assembly deklariert ist. Tatsächlich dürfen Sie nicht einmal einen voll gekennzeichneten Namen für Aufrufe innerhalb derselben Assembly angeben (Querverweise zu anderen Assemblies kennzeichnen Sie mit eckigen Klammern).

Die Methode CalcSphereVolume entnimmt dem Stack ein float64-Element und legt bei ihrem Rücksprung ein float-64-Element auf dem Stack ab. Es trifft sich gut, dass derzeit ein float64-Wert von der Umwandlung mit System.Double.Parse auf dem Stack liegt, genau das, was Sie für die Methode CalcSphereVolume brauchen. Der Code für diese Methode lautet wie in Listing 2.

.method static float64 CalcSphereVolume(float64)
                                          cil managed
{
  .maxstack 3


  ldarg.0
  dup
  dup
  mul
  mul
  ldc.r8 3.141592654
  mul
  ret
}      
      

Listing 2

Das sieht ziemlich schaurig aus, oder? Wenn Sie den Code zeilenweise untersuchen, dürfte er nicht schwer zu verstehen sein. Der Operationscode ldarg.0 ist eine Kurzversion von ldarg, des Token für den Operationscode "Argument laden". Normalerweise verlangt ldarg ein Argument mit dem Index des Wertes, den die Operation aus der Argumentliste laden soll. Deshalb erscheint diese Version auch mit zwei Bytes in der Binärdarstellung. Die hier verwendete Kurzversion (ldarg.0) kommt mit einem Byte aus. Kurzversionen existieren für die ersten vier Argumente. Übernimmt Ihre Methode mehr als vier Argumente, müssen Sie sie mit einem Operanden aufrufen, um das korrespondierende Element auf den Auswertungs-Stack zu laden, beispielsweise ldarg 5.

Für die Berechnung des Kugelvolumens nach der (wenig bekannten Koenschen) Gleichung Volumen = Radius? * Pi müssen Sie nun den entsprechenden Code implementieren.

CalcSphereVolume ist eine neue Methode, so dass Sie einen frischen und sauberen Methodenzustand vorfinden und mithin einen unberührten Auswertungs-Stack. ldarg.0 platziert das erste Argument vom Aufrufer auf dem Auswertungs-Stack. Die dup-Anweisung dupliziert den obersten Stack-Eintrag. Demnach sieht der Stack nach der zweiten dup-Anweisung wie folgt aus: Radius, Radius, Radius. Mit den drei Duplikaten berechnen Sie die dritte Potenz des Radiuswertes. Daran schließen sich zwei mul-Operationen an. Eine mul-Operation entnimmt zwei Elemente vom Stack, multipliziert sie und legt das Ergebnis als obersten Eintrag auf den Stack. Das Stack Übergangsdiagramm für die beiden Multiplikationen sieht folgendermaßen aus:

Radius, Radius, Radius --> Radius, Radius?
Radius, Radius^2 --> Radius^3

So weit, so gut. Jetzt enthält der Stack die dritte Potenz des Radius und es ist nur noch dieser Wert mit Pi zu multiplizieren. Allerdings ist Pi nicht im Stack eingetragen. Bevor Sie also die Multiplikation ausführen können, müssen Sie die Konstante Pi auf den Stack laden. Hierfür ist die Anweisung ldc - "lade Konstante" - vorgesehen. Da die Konstante Pi ein float64-Wert (mit acht Byte) ist, verwenden Sie die ldc.r8-Anweisung. Beachten Sie, dass der Typ zwar float64 heißt, aber alle Operationen für diesen Typ die Abkürzung r8 verwenden, die für "real 8 Byte" steht. Nach der Ladeoperation sieht der Auswertungs-Stack folgendermaßen aus: Radius^3, Pi. Jetzt können Sie die beiden Werte mit mul multiplizieren. Diese Operation legt das Ergebnis auf dem Stack ab. Da die Methode einen float64-Wert zurückgeben soll, können Sie einfach retaufrufen, um das letzte Element auf dem Stack an den Aufrufer zurückzugeben.

Hinweis: Die Programmflussregeln besagen, dass der Auswertungs-Stack nach dem Rücksprung aus einer Methode immer leer sein muss. Im Beispiel entfernt ret das letzte Element vom Stack und übergibt es an den Aufrufer. Der Stack ist also leer, wenn der Gültigkeitsbereich der Methode endet.

Flusssteuerung

Neben Eingaben, Ausgaben und Berechnungen müssen Sie in der Lage sein, die grundlegende Flusssteuerung in ILAsm zu implementieren. Dieser Abschnitt zeigt, wie Sie mit Bedingungsanweisungen verzweigen und Code in einer Schleife wiederholt ausführen. Hier sind nur die wichtigsten Teile des Codes angegeben, den vollständigen Quellcode für diesen Abschnitt finden Sie in der Datei FlowControl.il im herunterladbaren Beispielcode.

Die Beispielanwendung

Angenommen, eine Anwendung soll sich wie folgt verhalten: Gibt der Benutzer einen Wert kleiner als null ein, führt sie Aktion A aus, ist der Wert gleich null, ruft sie Aktion B auf, bei Werten größer als null, aber kleiner als zehn soll Aktion C in einer Schleife ausgeführt werden und bei allen anderen Werten kommt Aktion D zum Zuge. Dieses Beispiel zeigt, wie Sie Verzweigungs-(Bedingungs-)Anweisungen verwenden und eine einfache Schleife aufbauen.

Bedingungsanweisungen

Mit Bedingungsanweisungen lässt sich Code abhängig vom Wert eines Elements ausführen. Diese Anweisungen sind den if/else-Kombinationen in den meisten höheren Programmiersprachen ähnlich (Listing3, auf Heft-CD).

Dieses Codefragment enthält zwei neue Elemente: Marken und Verzweigungsoperanden. Marken dienen als Sprungziele. Man kann auch einen relativen Wert angeben, der die Sprungdistanz (Offset) spezifiziert, d.h. wie viele Bytes von dazwischenliegendem Bytecode von der aktuellen Position ausgehend nach vorn odernach hinten im Programm zu überspringen sind. Um die Sprungdistanz zu berechnen, summiert man die Größe aller Operationscodes und Operanden vom Ausgangspunkt zum Endpunkt. Zweifellos ist es wesentlich einfacher und komfortabler, Marken zu verwenden und es dem ILAsm-Compiler zu überlassen, die für den Sprung benötigten Offsets zu berechnen. Die Verzweigungsanweisungen (branch) beginnen alle mit einem b, gefolgt von einer Abkürzung für die Art desSprungs. Der angegebene Code verwendet immer eine Kurzversion mit einem .sam Ende, weil hier sicher ist, dass das Sprungziel nicht weit entfernt liegt. Die Kurzversion benötigt nur ein Byte für den Offset, die Langversion dagegen vier Bytes. Um die Größe der Assembly gering zuhalten, sollte man nach Möglichkeit immer die Kurzversion verwenden.

Alle branch-Anweisungen arbeiten in der gleichen Weise: Sie entnehmen die obersten Werte vom Stack (wenn sie etwas vergleichen müssen), bestimmen das Ergebnis des jeweiligen Bedingungstests und setzen dann mit der Codeausführung an der spezifizierten Marke fort, wenn der Ausdruck true ergibt, oder andernfalls mit der nächsten Zeile. Tabelle 1 zeigt eine Liste aller Verzweigungsoperanden.

Anweisung

Offset-größe

Vom Stack entfernte Elemente

Beschreibung

Br

int32

-

Verzweige um <int32> Bytes von der aktuellen Position aus.

br.s

int8

-


Brfalse
brnull
brzero

int32

int32

Verzweige, wenn das oberste Stack-Element gleich 0 ist.

brfalse.s

int8

int32

Beachten Sie, dass es weder brnull.s noch brzero.s gibt.

Brtrue
brinst

int32

int32

Verzweige, wenn der Wert auf dem Stack ungleich null (oder eine gültige Adresse einer Objektinstanz) ist.

brtrue.s
brinst.s

int8

int32


Beq

int32

*,*

Verzweige, wenn beide Werte gleich sind.

Bge

int32

*,*

Verzweige, wenn der erste Wert größer oder gleich dem zweiten Wert ist.

Bgt

int32

*,*

Verzweige, wenn der erste Wert größer als der zweite Wert ist.

Ble

int32

*,*

Verzweige, wenn der erste Wert kleiner oder gleich dem zweiten Wert ist.

Blt

int32

*,*

Verzweige, wenn der erste Wert kleiner als der zweite Wert ist.

bne.un

int32

*,*

Verzweige, wenn die beiden Werte nicht gleich sind (vorzeichenloser Vergleich).

bge.un

int32

*,*

Verzweige, wenn der erste Wert größer oder gleich dem zweiten Wert ist (vorzeichenloser Vergleich).

bgt.un

int32

*,*

Verzweige, wenn der erste Wert größer als der zweite Wert ist (vorzeichenloser Vergleich).

ble.un

int32

*,*

Verzweige, wenn der erste Wert kleiner oder gleich dem zweiten Wert ist (vorzeichenloser Vergleich).

blt.un

int.32

*,*

Verzweige, wenn der erste Wert vorzeichenloser Vergleich).

beq.s

int8

*,*


bge.s

int8

*,*


bgt.s

int8

*,*


ble.s

int8

*,*


blt.s

int8

*,*


bne.un.s

int8

*,*


bge.un.s

int8

*,*


bgt.un.s

int8

*,*


ble.un.s

int8

*,*


blt.un.s

int8

*,*


Tabelle 1: IL-Verzweigungsoperanden

Der Operationscode pop dient hier dazu, den obersten Eintrag des Auswertungs-Stacks zu verwerfen. Die ret-Anweisung gibt die Steuerung an den Aufrufer zurück, wenn der Code einen Punkt erreicht, an dem weder anderer Code noch eine Bedingung auszuwerten ist. Sehen Sie sich den heruntergeladenen Begleitcode an. Die enthaltenen Stack-Übergangsdiagramme dienen als Codekommentare, damit Sie den Code leichter verfolgen können.

Schleifen in IL

Das nächste Beispiel zeigt eine typische Schleifenkonstruktion. Wie zu Beginn des Artikels festgestellt wurde, besitzt das Programm unter anderem folgende Aufgabe: Gibt der Benutzer einen Wert größer 0 und kleiner 10 ein, soll der Code die entsprechende Anzahl von Schleifendurchläufen ausführen (Listing 4, auf Heft-CD).

Wenn die Programmausführung die Marke Loop Point erreicht, liegt auf dem Auswertungs-Stack ein Integer-Wert, der die Anzahl der noch auszuführenden Schleifen angibt. Zuerst lädt der Code in der Schleife mit der Anweisung ldc.i4.m1den Wert -1 auf den Stack und ruft dann add auf, um den Schleifenzähler zu dekrementieren. Das ließe sich auch mit ldc.i4.1 und sub erreichen und natürlich kann man den Schleifenzähler auch am Ende der Schleife statt zu Beginn dekrementieren.

Als Nächstes kommt die eigentliche Funktionalität der Schleife. Im Beispiel gibt der Code einfach den String looping... bei jedem Schleifendurchlauf aus. Normalerweise wird hier der zu wiederholende Code untergebracht. Nach jedem Schleifendurchlauf wird der Schleifenzähler mit null verglichen, um zu entscheiden, ob das Programm an den Anfang der Schleife zurückspringt oder die Schleife beendet ist und die Programmausführung mit den Codezeilen nach der Schleife fortsetzen soll. Wie bereits gezeigt, holen derartige Vergleiche den Wert vom Auswertungs-Stack. Dadurch verlieren Sie hier aber den Wert des Schleifenzählers. Deshalb klont eine dup-Anweisung den Eintrag an der Spitze des Stack, das heißt den Schleifenzähler, bevor der Vergleich erfolgt. Bei komplizierteren Schleifen ist es besser, den Zählerin einer lokalen Variablen zu speichern -das ist flexibler und erlaubt eine bessere Kontrolle des Auswertungs-Stacks. Das nächste Beispiel zeigt, wie Sie das bewerkstelligen.

Die Anweisung ldc.i4.0 lädt null auf den Auswertungs-Stack. Das Stack-Übergangsdiagramm für diese Quellzeile sieht so aus: Counter, Counter --> Counter, Counter, 0. Die Anweisung bgt.s holt die obersten zwei Einträge vom Auswertungs-Stack und setzt die Programmausführung an derCodezeile fort, die mit LoopPoint markiertist, wenn das tiefer liegende Element auf dem Stack größer als das oberste Element ist. Das heißt, diese Anweisung verzweigt, bis der Zähler den Wert 0 erreicht. Nach der Schleife soll das Programm enden. Aufgrund der dup-Anweisung innerhalb der Schleife befindet sich immer noch der Zähler auf dem Auswertungs-Stack. Da das Programm den Zählerwert nicht mehr benötigt, entfernen Sie ihn einfach mit pop vom Stack und rufen dann erst ret auf.

Ausnahmebehandlung

Errare humanum est - Irren ist menschlich. Und deshalb kommen Sie nicht ohne strukturierte Ausnahmebehandlung aus, wenn Sie sicheren Code schreiben wollen. Prüfen Sie immer die Eingabewerte, bevor Sie sie weiter verarbeiten. Allein dadurch halten Sie sich schon eine Menge Probleme vom Hals. Auch dieser Abschnitt zeigt zunächst den Beispielcode. Er erweitert das Beispiel der Kugelberechnung und macht es damit zu einem würdigen Anwärter für das Profilager. Die Berechnungsmethode ist dieselbe. Geändert hat sich der Teil aus Listing 5 (siehe Heft-CD). Da Sie einen großen Teil dieses Codes bereits aus dem ersten Beispiel kennen, genügt es, die geänderten Passagen - den Fehlerbehandlungscode- zu erläutern.

Lokale Variablen

Wenn Sie überwachte Blöcke verwenden (Codeblöcke, die sich um Ausnahmen kümmern), müssen Sie den Block immer mit einem leeren Auswertungs-Stack verlassen. Da Sie den im überwachten Block berechneten Wert später noch benötigen, speichern Sie ihn in einer lokalen Variablen, um ihn vom Auswertungs-Stack zu entfernen. Eine lokale Variable deklarieren Sie mit der .locals init-Anweisung und geben alle lokalen Variablen in einer durch Komma getrennten und in Klammern eingeschlossenen Liste an. Denken Sie daran, dass CLI einen getrennten Speicherraum für lokale Variablen definiert und Sie alle diese Variablen am Beginn der Methode deklarieren müssen. Dagegen erlauben es die meisten höheren Programmiersprachen, Variablen an beliebigen Stellen im Rumpf der Methode zu definieren. Ein weiterer Unterschied ist, dass Sie in ILAsm keine Variablennamen verwenden müssen wie in höheren Programmiersprachen. Statt dessen adressiert ILAsm die Variablen nach ihrem Index. Zwar können Sie Variablennamen angeben, doch der Compiler übersetzt sie ohnehin in Indizes.

Das obige Beispiel deklariert eine float-64-Variable, die den Radius für die Berechnung aufnimmt. Auf diese Variable greifen Sie mit zwei Methoden zu: stloc speichert das oberste Element vom Auswertungstack in einer Variablen und ldloc legt den Wert einer Variablen auf dem Stack ab. Das Codebeispiel verwendet die Kurzversionen (ldloc.0 und stloc.0), um mit der ersten (und einzigen) lokalen Variablen zu arbeiten.

Überwachte Blöcke

Einen überwachten Block beginnen Sie mit der .try-Anweisung (genau wie in C# oder VB.NET) und grenzen den Block mit geschweiften Klammern ({}) ein. Obwohl diese Klammern deutlich den Beginn und das Ende des überwachten Blocks markieren, müssen Sie einen speziellen Operationscode verwenden, um den überwachten Block tatsächlich zu verlassen - die Anweisung leave (oder die Kurzform leave.s). Die leave-Anweisung bewirkt, dass das Programm den überwachten Block verlässt, ohne eine Ausnahme auszulösen, und an der markierten Positionfort fährt, wenn kein Fehler aufgetreten ist. Bei einem Fehler setzt die Codeauswertung mit der Zeile fort, die die catch-Anweisung enthält. Die catch-Anweisung muss spezifizieren, welche Art von Ausnahme sie abfängt. Man bezeichnet das als Ausnahmefilterung. Dieses einfache Beispiel gibt System.Exception - die Basisklasse aller Ausnahmen - als abzufangenden Ausnahmetyp an und fängt somit Ausnahmen aller Art ab.

Bisher haben Sie noch nichts über die Verwendung von Klasseninstanzen erfahren. Fürs Erste können Sie einen Fehler behandeln, indem Sie einfach das Ausnahmeobjekt vom Auswertungs-Stack abrufen und den Benutzer mit einer Meldung auf den Fehler hinweisen. Alternativ können Sie zu einem Punkt im Code zurückspringen, der den Benutzer dazu auffordert, die Eingabe mit einem anderen Wert zu wiederholen, bis sich schließlich der Code ohne Auslösen einer Ausnahme fertigstellen lässt.

Bislang haben Sie die Grundlagen der funktionalen Programmierung mit ILAsm kennen gelernt. Die hier angegebene Liste der Operationscodes ist nicht einmal annähernd vollständig, doch würde eine detaillierte Erläuterung der ILAsm-Programmierung den Rahmen eines einzelnen Artikels sprengen. Wenn Sie an weiteren Einzelheiten interessiert sind, sollten Sie sich die am Ende genannten Quellen zu Gemüte führen.

Links & Literatur

[1] Sehen Sie sich die folgenden beiden Dateien im Tool-Developers-Guide-Unterverzeichnis Ihrer SDK-Installation an: Partition I Architecture.doc und Partition III CIL.doc.

[2] Serge Lidin: Inside Microsoft .NET IL Assembler,MSPress, 2002

[3] Simon Robinson: Advanced .NET Programming,Wrox, 2002

Über den Autor

Peter Koen ist ein Allroundtalent für fast alle Bereiche im Microsoft-Entwicklerumfeld. Er war bis vor kurzem Microsoft MVP und unabhängiger Berater, Autor und Programmierer. Seit September2004 arbeitet Peter als Technologieberater bei Microsoft Österreich. Außerdem hat Peter die SQL Server User Group in Österreich gegründet.


Anzeigen: