Behandeln der Sprachinteroperabilität mit Microsoft .NET Framework

Veröffentlicht: 08. Dez 2000 | Aktualisiert: 09. Nov 2004
Von Damien Watkins

In diesem Artikel werden die Interoperabilitätsfunktionen von Microsoft .NET Framework beschrieben.

Auf dieser Seite

Einführung
Gemeinsame Sprachlaufzeit
CLR-Typsystem
Zuweisungskompatibilität
Integrierte Typen
Metadaten
Erweiterbarkeit von Metadaten
Baugruppen und Manifeste
Ausführungssystem
Verwaltet kontra nichtverwaltet
Gemeinsame Sprachspezifikation
Schlussfolgerung

Einführung

Durch den zunehmenden Einsatz von verteilten Systemen sind die Systementwickler gezwungen, sich mit den Problemen der Interoperabilität auseinander zu setzen. Die Interoperabilitätsprobleme bestehen seit vielen Jahren, und es wurden mehrere Standards und Architekturen entwickelt, um einige dieser Probleme zu lösen, allerdings mit unterschiedlichem Erfolg. Hierzu gehören:

  • Die Darstellungsstandards, z.B. XDR (External Data Representation; externe Datendarstellung) und NDR (Network Data Representation; Netzwerkdatendarstellung), lösen das Problem der Übergabe von Datentypen zwischen verschiedenen Computern. Diese Standards gleichen andere Probleme aus, z.B. Big-Endian- und Little-Endian-Probleme und unterschiedliche Wortgrößen.

  • Die Architekturstandards, z.B. die in verteilten Computerumgebungen (DCE = Distributed Computing Environment) verwendeten Remoteprozeduraufrufe (RPC), die allgemeine Objektanfragen-Brokerarchitektur (CORBA = Common Object Request Broker Architecture) der Objektverwaltungsgruppe (OMG = Object Management Group) und das Komponentenobjektmodell (COM = Component Object Model) von Microsoft nehmen sich dem Problem der sprach-, prozess- und computerübergreifenden Methodenaufrufe an.

  • Die Sprachstandards, z.B. ANSI C, ermöglichen die Verteilung von Quellcode auf Compiler und Computer.

  • Die Ausführungsumgebungen, die z.B. von den virtuellen Computern (VM =Virtual Machines) von SmallTalk und Java bereitgestellt werden, ermöglichen es, dass der Code auf verschiedenen physikalischen Computern ausgeführt werden kann, indem eine standardisierte Umgebung zur Ausführung bereitgestellt wird.

Jeder dieser Ansätze bietet den Anwendungsentwicklern zwar erhebliche Vorteile, aber keiner löst oder widmet sich allen Problemen, die sich aus einer verteilten Computerumgebung ergeben. Ein Problem, dem noch nicht genug Beachtung geschenkt wurde, ist die Sprachinteroperabilität. Die Sprachinteroperabilität bezieht sich nicht nur auf ein standardisiertes Aufrufmodell, z.B. COM und CORBA, sondern auf ein Schema, dass es den Klassen und Objekten in einer Sprache ermöglicht, als Elemente erster Klasse in einer anderen Sprache verwendet zu werden. Im Idealfall sollte es möglich sein, dass der Python-Code ein C++-Objekt instanziiert, das von einer Eiffel-Klasse erbt. In diesem Artikel wird eine Architektur untersucht, die diese Anforderungen erfüllt: das Microsoft .NET Framework.

Gemeinsame Sprachlaufzeit

Die gemeinsame Sprachlaufzeit (CLR, Common Language Runtime) ist eine wichtige Komponente der .NET Framework-Architektur. Die Laufzeit besteht aus drei Hauptkomponenten:

  1. Einem Typsystem, das viele der Typen und Operationen moderner Programmiersprachen unterstützt.

  2. Einem Metadatensystem, mit dem Metadaten einschließlich Typen zur Kompilierungszeit beibehalten und dann vom Ausführungssystem zur Laufzeit abgefragt werden können.

  3. Einem Ausführungssystem, das .NET Framework-Programme ausführt. Dieses System verwendet die Metadaten-Systeminformationen, um Dienste durchzuführen, z.B. die Speicherverwaltung.

Die Laufzeit kann als eine Sammlung der Features zahlreicher Programmmiersprachen betrachtet werden. Die Laufzeit definiert Standardtypen und ermöglicht es den Entwicklern, eigene Typen zu definieren und Funktionsprototypen und Implementierungen für ihre Typen anzugeben. Die Laufzeit unterstützt einen objektorientierten Programmierstil, einschließlich Features wie Klassifizierung, Vererbung und Polymorphismus. Die Laufzeit unterstützt auch nichtobjektorientierte Sprachen.

Bild01

Abbildung 1. Elemente der gemeinsamen Sprachlaufzeit

Abbildung 1 zeigt die Beziehung zwischen den Elementen der Laufzeit. Die oben im Diagramm angezeigte Quellcodedatei kann die Definition eines neuen Typs enthalten, der in mehreren Programmiersprachen definiert sein kann, z.B. in C++. Dieser neue Typ erbt von einem in der .NET-Bibliothek enthaltenen Typ, z.B. dem Object-Typ. Wenn diese Datei von einem .NET Framework-C++-Compiler kompiliert wird, wird die bei diesem Vorgang generierte Microsoft Intermediate Language (MSIL) zusammen mit den Metadaten des neuen Typs in einer Datei gespeichert. Das verwendete Metadatenformat ist unabhängig von der Programmiersprache, in der der Typ definiert wurde. Sobald die MSIL für diesen neuen Typ vorhanden ist, können andere Quellcodedateien, die i.d.R. in anderen Programmiersprachen geschrieben wurden (z.B. C#, Eiffel, Python oder Visual Basic), diese Datei importieren. Der C++-Typ kann dann so in einer Python-Quellcodedatei verwendet werden, als handele es sich um einen Python-Typ. Der Prozess zum Importieren von Datentypen kann innerhalb verschiedener Sprachen mehrmals wiederholt werden. Dies wird durch den Pfeil veranschaulicht, der von der MSIL zurück zu einer Quellcodedatei führt. Das Ausführungssystem kompiliert, lädt und führt eine MSIL-Datei zur Laufzeit aus. Wenn Verweise auf einen in einer anderen MSIL-Datei definierten Typ vorhanden sind, wird die entsprechende MSIL-Datei geladen. Anschließend werden die Metadaten gelesen und die Instanzen des neuen Typs für die Laufzeit offen gelegt.

CLR-Typsystem

Wenn eine Sprachinteroperabilität angestrebt wird, bleiben die grundlegenden Probleme erhalten. Zunächst muss eine Form der Vereinbarung in Bezug auf die Darstellung der Datentypen übernommen werden. Gemäß CORBA werden die Konzepte von Objekt und Typ in der Objektverwaltungsarchitektur definiert, und die CORBA-Spezifikation bestimmt diese Konzepte in quantitativer Form. Im .NET Framework werden die im System verwendeten Typen durch das Typsystem der gemeinsamen Sprachlaufzeit beschrieben. Abbildung 2 zeigt eine vereinfachte Darstellung dieses Typsystems.

Bild02

Abbildung 2. Typsystem der gemeinsamen Sprachlaufzeit

Das Typsystem der gemeinsamen Sprachlaufzeit ist in zwei Untersysteme unterteilt:

  1. Wertetypen

  2. Verweistypen

Einer der Hauptunterschiede zwischen den Werte- und Verweistypen besteht darin, dass die Wertetypen nicht über ein Identitätskonzept verfügen. Ein Wertetyp ist eine Folge von Bits im Speicher. Ein Beispiel für einen Wertetyp ist eine 32-Bit-Ganzzahl mit Vorzeichen. Zwei beliebige 32-Bit-Ganzzahlen mit Vorzeichen haben denselben Wert, wenn sie dieselbe Zahl enthalten. Das heißt, die Folge von Bits ist identisch. Verweistypen sind eine Kombination aus einem Speicherort (der Identität) und einer Folge von Bits. Verweistypen sind mit den konventionellen C++-Klassen vergleichbar, wo zwei Objekte derselben Klasse dieselben Datenwerte enthalten können (dieselbe Bitfolge), aber nicht als identisch angesehen werden, da sie auf zwei unterschiedliche Objekte verweisen.

In der .NET Framework-Terminologie wird eine Instanz eines Werte- oder Verweistyps als ein Wert bezeichnet. Jeder Wert besitzt einen genauen Typ, und dieser Typ definiert die Methoden, die für diesen Wert aufgerufen werden können. Zur Laufzeit besteht nicht immer die Möglichkeit, den Typ eines Wertes anhand einer Überprüfung zu ermitteln; so können z.B. Wertetypen nicht auf ihren genauen Typ hin überprüft werden.

Wertetypen

Obwohl es sich bei vielen integrierten Datentypen um Wertetypen handelt, ist die Anzahl der Wertetypen nicht auf die integrierten Datentypen begrenzt. Wertetypen werden oft einem bestimmten Laufzeitstack zugeordnet und können daher lokale Variablen, Parameter und Rückgabewerte von Funktionen sein. Aus diesem Grund sollten die benutzerdefinierten Wertetypen, genau wie die integrierten Wertetypen, möglichst klein sein und maximal 12 bis 16 Bytes umfassen. Die benutzerdefinierten Strukturen und Klassen können Wertetypen sein, die ggf. Folgendes enthalten:

  • Methoden (sowohl Klasse als auch Instanz)

  • Felder (sowohl Klasse als auch Instanz)

  • Eigenschaften

  • Ereignisse

Auf fundamentaler Ebene entsprechen sowohl die Eigenschaften als auch die Ereignisse den Methoden. Eigenschaften sind eine syntaktische Abkürzung; sie stellen die Methoden Set und Get in den logischen Feldern einer Klasse dar. Ereignisse werden verwendet, um die asynchronen Änderungen in einem überwachten Objekt offen zu legen. Clients überwachen das Aufrufen von Ereignissen. Wenn ein Ereignis aufgerufen wird, erfolgt auf dem Client ein Methodenaufruf.

Ein Wertetyp kann nicht von einem anderen Wertetyp erben, d.h. laut .NET Framework-Terminologie, dass ein Wertetyp versiegelt ist. Obwohl ein Wertetyp oft einem Stack zugeordnet wird, kann ein Wertetyp auch einem Heap zugeordnet werden, z.B. wenn es sich bei dem Wertetyp um ein Datenelement eines Objekttyps handelt.

Für alle Wertetypen ist ein entsprechender Objekttyp vorhanden, der als verpackter Typ bezeichnet wird. Werte eines beliebigen Wertetyps können verpackt und entpackt werden. Beim Verpacken eines Wertetyps werden die Daten aus dem Wert in ein Objekt mit einem entsprechenden Verpackungstyp kopiert, der einem Garbagecollectionheap zugeordnet ist. Beim Entpacken eines Wertetyps werden die Daten aus dem verpackten Objekt in einen Wert kopiert. Da es sich bei dem Verpackungstyp um einen Objekttyp handelt, kann dieser Typ Schnittstellentypen unterstützen und somit die unverpackte Darstellung um zusätzliche Funktionen erweitern. Die Objekt-, Verweis- und Schnittstellentypen werden ausführlicher im Abschnitt "Verweistypen" beschrieben.

Der folgende Code veranschaulicht die Definition und Verwendung eines Wertetyps in verwaltetem C++-Code. Dieser Wertetyp ist eine Klasse, die nur zwei ganze Zahlen als Datenelemente enthält. Da Datentypen, wie bereits erwähnt, dem Stack zugeordnet sind und nach Wert übergeben werden, sollten sie i.d.R. möglichst klein sein. Der Code veranschaulicht zudem, dass ein Wertetyp nicht dem Heap zugeordnet werden kann. Es tritt ein Fehler auf, wenn versucht wird, dem Heap einen VTPoint zuzuordnen. In dem Beispiel wird der Wert auf dem Heap durch Casten entpackt.

#using <mscorlib.dll> 
using namespace System; 
__value public class VTPoint 
{ 
public: 
  int m_x, m_y; 
}; 
int main(void) 
{ 
  VTPoint a;                          // auf Stack 
  VTPoint * ptr = new VTPoint();      // ungültig 
  __box VTPoint *VTptr = __box(a);              // verpacken 
  VTptr->m_x = 42; 
  VTPoint b = *dynamic_cast<VTPoint*>(VTptr);   // entpacken 
  b.m_y = 42; 
  Console::Write("Werte: b.m_y sollte 42 sein, ist "); 
  Console::WriteLine(b.m_x); 
  Console::Write("Werte: b.m_y sollte 42 sein, ist "); 
  Console::WriteLine(b.m_y); 
  return 0; 
} 

Verweistypen

Verweistypen sind eine Kombination aus Speicherort (auch als Identität bezeichnet) und einer Folge von Bits. Ein Speicherort kennzeichnet einen Speicherbereich, in dem die Werte gespeichert werden können, und die für diesen Speicherort zulässigen Wertetypen. Speicherorte sind typsicher, da in einem Speicherort nur zuweisungskompatible Typen gespeichert werden können. Der Abschnitt "Zuweisungskompatibilität" enthält ein Beispiel hierzu.

Anhand des vorherigen Codes wird deutlich, dass ein Objekttyp immer ein Verweistyp ist, aber dass es sich nicht bei allen Verweistypen um Objekttypen handelt. Ein Beispiel für ein Objekttyp ist der integrierte Verweistyp String (Zeichenfolge). Die gemeinsame Sprachlaufzeit verwendet den Begriff Objekt, um auf Werte eines Objekttyps zu verweisen. Da String ein Objekttyp ist, sind alle Instanzen vom Typ String Objekte. Die genauen Typen für alle Objekte werden als Objekttypen bezeichnet. Objekttypen werden immer dem Garbagecollectionheap zugeordnet.

Der folgende Code veranschaulicht die Definition eines Objekttyps mit der Bezeichnung Point und die Verwendung dieses Objekttyps in C++. Bei dieser Definition werden Eigenschaften verwendet, die es den Clients ermöglichen, so auf ein logisches Datenelement zuzugreifen, als handele es sich um ein öffentliches Feld der Klasse. Der Beispielcode veranschaulicht zudem, dass ein Verweistyp nicht dem Stack zugeordnet werden kann. Wenn dem Stack der Objekttyp Point zugeordnet wird, schlägt die Kompilierung fehl. Darüber hinaus erkennt man anhand des Codes einen vermeintlich direkten Zugriff auf die Eigenschaften des Objekts, als handele es sich um öffentliche Attribute. Dies ist eine syntaktische Beschönigung. Der Compiler fügt jedoch Aufrufe für die Methoden get_ und set_ ein, die je nach Bedarf für die Klasse definiert werden.

#using <mscorlib.dll> 
using namespace System; 
__gc class Point 
{ 
  private: 
    int m_x, m_y; 
  public: 
    Point() 
    { 
      m_x = m_y = 0; 
    }   
    __property int get_X() 
   { 
      return m_x;  
   } 
    __property void set_X(int value)  
   {  
      m_x = value; 
    }; 
    __property int get_Y() 
   { 
      return m_y;  
   } 
    __property void set_Y(int value)  
   {  
      m_y = value; 
    }; 
}; 
int main(void) 
{ 
   Point a;                        // ungültig 
   Point* ptr = new Point(); 
   ptr->X = 42; 
   ptr->Y = 42; 
   Console::Write("ptr->X ist "); 
   Console::WriteLine(ptr->X); 
   Console::Write("ptr->Y ist "); 
   Console::WriteLine(ptr->Y); 
   return 0; 
} 

Objekttypen

Alle Objekttypen erben, entweder direkt oder indirekt, von der Object-Klasse. Obwohl dies im vorherigen Code nicht explizit angegeben ist, erbt die Point-Klasse direkt von der Object-Klasse. Diese Klasse stellt eine Reihe von Methoden bereit, die folglich für alle Objekte aufgerufen werden können. Zu diesen Methoden gehören:

  • Equals. Gibt den Wert TRUE zurück, wenn zwei Objekte identisch sind. Diese Methode kann ggf. durch Untertypen außer Kraft gesetzt werden, um einen Vergleich zum Prüfen der Identität oder Gleichheit bereitzustellen.

  • Finalize. Diese Methode wird durch den Garbagecollector aufgerufen, bevor der Speicher eines Objekts freigegeben wird.

  • GetHashCode. Gibt den Hashcode für ein Objekt zurück.

  • GetType. Gibt den Objekttyp für dieses Objekt zurück. Gewährt den Zugriff auf die Metadaten des Objekts.

  • MemberwiseClone. Gibt eine "abgespeckte" Kopie des Objekts zurück.

  • ToString. Gibt eine Zeichenfolgendarstellung des Objekts zurück.

Die meisten der für die Object-Klasse definierten Methoden sind öffentliche Methoden. Der Zugriff auf MemberwiseClone und Finalize ist jedoch geschützt. Nur Untertypen können auf diese Methoden zugreifen.

Der folgende Code veranschaulicht die öffentlichen Methoden, die für ein Objekt vom Typ Point aufgerufen werden können. Die Ausgabe wurde mit einem einfachen Programm namens Reflector generiert, das im Lieferumfang des .NET Framework SDKs enthalten ist. Das Programm Reflector ruft das Type-Objekt der Point-Klasse ab und zeigt dann die Prototypen der öffentlichen Methoden des Objekts an. Neben den Eigenschaftszugriffsmethoden werden vier öffentliche Methoden gezeigt, die von der Object-Klasse geerbt werden. Die beiden zugriffsgeschützten Methoden werden nicht angezeigt. In der Ausgabe wird zudem die Verwendung von integrierten Typen gezeigt, z.B. Boolean, Int32 und String.

C:\Programme 
GWSSDK\Samples\Reflector\VC>Reflector -m -v Point 
Class: Point 
Methods (8) 
        Int32 GetHashCode () 
        Boolean Equals (System.Object) 
        System.String ToString () 
        Int32 get_X () 
        Void set_X (Int32) 
        Int32 get_Y () 
        Void set_Y (Int32) 
        System.Type GetType () 

Schnittstellentypen

Hinter dem Programmieren mit Schnittstellentypen verbirgt sich ein sehr leistungsstarkes Konzept. Objektorientierte Programmierer sind mit diesem Konzept vertraut, bei dem ein abgeleiteter Typ durch einen Basistyp ersetzt wird. Es ist jedoch möglich, dass zwei Klassen nicht durch Vererbung verknüpft sind, aber dieselben Funktionen gemeinsam nutzen. So können z.B. viele Klassen Methoden enthalten, mit denen die Statusdaten der Klassen im permanenten Speicher gespeichert werden können. Zu diesem Zweck unterstützen Klassen, die nicht durch Vererbung miteinander verknüpft sind, ggf. gemeinsame Schnittstellen. Dies ermöglicht es den Programmierern, Codeanweisungen für das gemeinsame Verhalten der Klassen zu erstellen, die auf dem gemeinsam genutzten Schnittstellentyp und nicht auf den genauen Typen der Klassen basieren.

Ein Schnittstellentyp ist eine Teilspezifikation eines Typs. Er stellt eine Art von Vertrag dar, der die Implementierer verpflichtet, Implementierungen der in der Schnittstelle vorhandenen Methoden bereitzustellen. Objekttypen können viele Schnittstellentypen unterstützen. Ein Schnittstellentyp wird i.d.R. von vielen verschiedenen Objekttypen unterstützt. Per Definition kann ein Schnittstellentyp nie ein Objekttyp oder ein genauer Typ sein. Schnittstellentypen können andere Schnittstellentypen unterstützen.

Ein Schnittstellentyp kann Folgendes enthalten:

  • Methoden (sowohl Klasse als auch Instanz)

  • Statische Felder

  • Eigenschaften

  • Ereignisse

Einer der Hauptunterschiede zwischen einem Schnittstellentyp und einem Objekttyp (oder einer Wertetypklasse) besteht darin, dass ein Schnittstellentyp keine Instanzenfelder enthalten kann.

Der folgende Code veranschaulicht den benutzerdefinierten Schnittstellentyp IPoint. Dieser Schnittstellentyp deklariert zwei Attribute (hierfür sind in allen Implementoren vier Zugriffsmethoden erforderlich). Aufgrund der Vererbung von IPoint stimmt die Point-Klasse zu, diese vier abstrakten Methoden zu implementieren. Der Rest der Definition der Point-Klasse unterscheidet sich nicht von dem Code im Abschnitt "Verweistypen".

#using <mscorlib.dll> 
using namespace System; 
__gc __interface IPoint 
{ 
   __property int get_X(); 
   __property void set_X(int value); 
   __property int get_Y(); 
   __property void set_Y(int value); 
}; 
__gc class Point: public IPoint 
{ 
  ... 
}; 


Die gemeinsame Sprachlaufzeit stellt eine Reihe von Schnittstellentypen bereit. Der folgende Code veranschaulicht die Verwendung der IEnumerator-Schnittstelle, die von Arrayobjekten unterstützt werden. Das Array von

Point*s

ermöglicht es den Clients, über den Array zu enumerieren, indem eine IEnumerator-Schnittstelle angegeben wird. Die IEnumerator-Schnittstelle unterstützt drei Methoden:

  • Current. Gibt das aktuelle Objekt zurück.

  • MoveNext. Verschiebt den Enumerator zum nächsten Objekt.

  • Reset. Setzt den Enumerator auf die Anfangsposition zurück.

int main(void) 
{ 
  Point *points[] = new Point*[5]; 
  for(int i = 0, length = points->Count; i < length; i++) 
  { 
    points[i] = new Point(i, i); 
  } 
  Collections::IEnumerator *e = points->GetEnumerator(); 
  while(e->MoveNext()) 
  { 
    Object *o = e->Current; 
   Point *p = dynamic_cast<Point*>(o); 
   Console::WriteLine(p->X); 
  } 
  return 0; 
} 


Arrayobjekte unterstützen viele andere nützliche Methoden, z.B. Clear, GetLength und Sort. Arrayobjekte unterstützen zudem die Synchronisierung. Die Synchronisierung wird durch Methoden bereitgestellt, z.B. IsSynchronized und SyncRoot.

Zeigertypen

Mit Hilfe von Zeigertypen kann der Speicherort von Codeanweisungen oder eines Wertes angegeben werden. Funktionale Zeiger beziehen sich auf Code, verwaltete Zeiger können auf Objekte zeigen, die sich auf dem Garbagecollectionheap befinden, und nichtverwaltete Zeiger können lokale Variablen und Parameter adressieren. Bei der Semantik für diese verschiedenen Typen bestehen deutliche Unterschiede. So müssen z.B. Zeiger auf verwaltete Objekte beim Garbagecollector registriert werden, damit die Zeiger aktualisiert werden können, wenn Objekte auf dem Heap verschoben werden. Bei den Zeigern auf lokale Variablen müssen unterschiedliche Aspekte hinsichtlich der Lebensdauer beachtet werden.

In diesem Artikel werden die Zeigertypen nicht ausführlich behandelt. Dies hat zwei Gründe. Zunächst einmal sind fundierte Kenntnisse über die .NET Framework-Architektur erforderlich, um die Probleme zu verstehen, die sich aus der Semantik der Zeiger ergeben. Als zweiter Grund ist zu nennen, dass in den meisten Programmiersprachen das Vorhandensein von Zeigern so abstrahiert wird, dass sie für die Programmierer nicht sichtbar sind.

Zuweisungskompatibilität

Da wir uns, wenn auch nicht sehr ausführlich, mit den Konzepten von Objekt- und Schnittstellentypen befasst haben, können wir jetzt das Thema Zuweisungskompatibilität erörtern. Es folgt eine einfache Definition der Zuweisungskompatibilität für Verweistypen:

  • Ein Speicherort vom Typ T, wobei T entweder ein Objekt- oder ein Schnittstellentyp ist, kann ein beliebiges Objekt mit einem genauen Typ von T enthalten, ein Untertyp von T sein oder die Schnittstelle T unterstützen.

Der folgende Code veranschaulicht die Aspekte der Zuweisungskompatibilität. Da sowohl Points und Strings Objekttypen sind, erben sie von Object. Daher sind beide mit Object zuweisungskompatibel. Points und Strings sind jedoch nicht untereinander zuweisungskompatibel. Zudem implementiert die String-Klasse die IComparable-Schnittstelle, so dass Strings im Gegensatz zu Points mit der IComparable-Schnittstelle zuweisungskompatibel sind.

int main(void) 
{ 
  Point* p = new Point(); 
  String* s = S"Damien"; 
  Object* o; 
  o = p;          // gültig, p ist ein 'Object' 
  o = s;          // gültig, s ist ein 'Object' 
  p = s;          // ungültig, s ist kein 'Point'  
  s = p;          // ungültig, p ist kein 'String' 
  IComparable *i; 
  i = s;          // gültig, 'Strings' unterstützen IComparable 
  i = p;          // ungültig, 'Points' unterstützen IComparable nicht 
  return 0; 
} 

Integrierte Typen

Zu den integrierten Typen gehören Werte- und Verweistypen. Oftmals enthält die IL (Intermediate Language) besondere Anweisungen für integrierte Typen, so dass sie vom Ausführungsmodul effizient behandelt werden können. Zu den integrierten Typen gehören u.a.:

  • Boolesche Werte

  • Zeichen (16-Bit-Unicode)

  • Ganze Zahlen, sowohl mit als auch ohne Vorzeichen (8-, 16-, 32- und 64-Bit)

  • Gleitkommazahlen (32- und 64-Bit)

  • Object-Typ

  • String-Typ

  • Computerabhängige ganze Zahlen, sowohl mit als auch ohne Vorzeichen

  • Computerabhängiges Gleitkomma

Object und String sind integrierte Verweistypen, während es sich bei den anderen integrierten Typen um Wertetypen handelt. Eine computerabhängige ganze Zahl mit Vorzeichen kann den größten Wert enthalten, den ein Arrayindex auf einem Computer unterstützt. Eine computerabhängige ganze Zahl ohne Vorzeichen kann den größten Adresswert enthalten, den ein Computer unterstützt.

Metadaten

Metadaten stellen die wichtige Verbindung zwischen dem Laufzeitsystemtyp und dem Ausführungsmodul dar. Zurzeit treten bei vielen komponentenbasierten Programmiersystemen zwei große Probleme auf:

  1. Informationen über die Komponenten (Metadaten) werden oft nicht zusammen mit der Komponente gespeichert. Stattdessen werden die Metadaten häufig in Sekundärdateien gespeichert, z.B. in IDL (Interface Definition Language)-Dateien, Typbibliotheken, Schnittstellenrepositorys, Implementierungsrepositorys und in der Registrierung. Die gemeinsame Sprachlaufzeit von Microsoft .NET Framework speichert die Metadaten zusammen mit den Typen, um dieses Problem zu vermeiden.

  2. Die Metadateneinrichtungen sind sehr einfach aufgebaut. Die meisten Einrichtungen ermöglichen es den Entwicklern lediglich, die Syntax einer Schnittstelle anzugeben, aber nicht deren Semantik. Ein Beweis für die Existenz dieses Problems ist die Anzahl der "IDL"-Erweiterungen, die in vielen Systemen vorhanden sind, z.B. die ODL (Object Definition Language) von TINAC. Die gemeinsame Sprachlaufzeit von .NET Framework löst dieses Problem, indem ein standardisiertes Erweiterungssystem für Metadaten bereitgestellt wird, das als benutzerdefiniertes Attributsystem bezeichnet wird.

Die Compiler für .NET Framework verwenden Metadaten, um die von ihnen erstellten Typen zu beschreiben. Hierfür sind zwei Gründe zu nennen:

  1. Die Metadaten ermöglichen es, dass Typen in einer Sprache definiert und in einer anderen Sprache verwendet werden können. Dadurch wird die Sprachinteroperabilität in der gemeinsamen Sprachlaufzeit sichergestellt.

  2. Das Ausführungsmodul benötigt Metadaten, um die Objekte zu verwalten. Zum Verwalten von Objekten müssen auch andere Voraussetzungen erfüllt werden, z.B. ist eine Speicherverwaltung erforderlich.

Metadaten werden in den Dateien im Binärformat gespeichert. Dieses Format ist jedoch nicht festgelegt, so dass sich die Entwickler nicht auf das binäre Layout verlassen sollten. Stattdessen gibt es eine Reihe von Methoden zum Lesen und Schreiben von Metadaten.

Erweiterbarkeit von Metadaten

Die Metadateneinrichtung von .NET Framework kann durch benutzerdefinierte Attribute erweitert werden. Der folgende C#-Code veranschaulicht die Verwendung von benutzerdefinierten Attributen sowie die Sprachinteroperabilität. Die erste Klasse, die im folgenden Code definiert wird, die Author-Klasse, ist ein benutzerdefiniertes Attribut, das durch die Vererbungsspezifikation festgelegt wird. Diese Klasse enthält ein Feld mit einer Zeichenfolge, die den Namen des Autors angibt. Zudem enthält die Klasse zwei Methoden (einschließlich eines Konstruktors) und setzt die ToString-Methode außer Kraft.

using System.Reflection; 
public class Author: Attribute 
{ 
  using System; 
  public readonly string name; 
  public Author(string name)  
  { 
   this.name = name; 
  } 
  public override String ToString()  
  { 
    return String.Format("Autor: {0}", name); 
  } 
} 
[Author("Damien Watkins")] 
public class CSPoint: Point 
{ 
  public static void Main() 
  { 
    MemberInfo info = typeof(CSPoint);  
    object[] attributes = info.GetCustomAttributes(); 
    Console.WriteLine("Benutzerdefinierte Attribute:"); 
    for (int i = 0; i < attributes.Length; i++)  
    { 
      System.Console.WriteLine("Attribut "  
         + i + ": ist " + attributes[i].ToString()); 
    } 
  } 
} 


Die nächste in dieser Abbildung definierte Klasse ist eine C#-Klasse, die von der C++-Klasse Point erbt. Vor der Definition der Klasse wird angegeben, dass diese Klasse ein benutzerdefiniertes Attribut vom Typ Author besitzt. Die Main-Methode in der CSPoint-Klasse verwendet die GetCustomAttributes-Methode, um auf ein Array von benutzerdefinierten Attributen für diese Klasse zuzugreifen und diese dann auf dem Bildschirm anzuzeigen.

Baugruppen und Manifeste

Eine Baugruppe ist eine Auflistung von Typen und Codeanweisungen der gemeinsamen Sprachlaufzeit. Mit Hilfe von Baugruppen können verwandte Komponenten gruppiert und, als Nebeneffekt, in einem Namespace platziert werden. Wie die .NET Framework-Typen sind auch die Baugruppen selbstbeschreibend. Die Baugruppeninformationen sind im Manifest der Baugruppe enthalten. Ein Manifest enthält folgende Informationen:

  • Name und Versionsinformationen

  • Eine Liste von Typen und deren Speicherort in der Baugruppe

  • Abhängigkeitsinformationen

#using <mscorlib.dll> 
using namespace System; 
using namespace System::Reflection; 
int main(void) 
{ 
   Type *t= Type::GetType("System.String"); 
   Assembly *a = Assembly::GetAssembly(t); 
   AssemblyName *an = a->GetName(false); 
   Console::WriteLine("AssemblyName->Name: {0}", an->Name); 
   Console::WriteLine("AssemblyName->Codebase:\n\t {0}", an->CodeBase); 
   String *rn[] = a->GetManifestResourceNames(); 
   Console::WriteLine("Ressourcenname(n):"); 
   for(int i = 0; i < rn->Count; i++) 
      Console::WriteLine(S"\t {0}", rn[i]); 
   Module *m[] = a->GetModules(); 
   Console::WriteLine("Modulname(n):"); 
   for(int i = 0; i < m->Count; i++) 
      Console::WriteLine(S"\t {0}", m[i]->FullyQualifiedName); 
   return 0; 
} 


Der vorstehende Code veranschaulicht einige Aspekte von Baugruppen. Das Programm ruft zunächst das Typobjekt der String-Klasse ab und verwendet dann den Objekttyp, um auf die Baugruppeninformationen zuzugreifen. Der folgende Code zeigt die von diesem Programm generierte Ausgabe.

C:\Project7\Exercises\assembly>StringAssembly 
AssemblyName->Name: mscorlib 
AssemblyName->Codebase: 
         file://C:/WINNT/ComPlus/v2000.14.1809/mscorlib.dll 
Ressourcenname(n): 
         mscorlib.resources 
         prcp.nlp 
         sortkey.nlp 
         ctype.nlp 
         sorttbls.nlp 
         culture.nlp 
         l_except.nlp 
         bopomofo.nlp 
         region.nlp 
         xjis.nlp 
         big5.nlp 
         l_intl.nlp 
         ksc.nlp 
         charinfo.nlp 
         prc.nlp 
Modulname(n): 
         C:\WINNT\ComPlus\v2000.14.1809\mscorlib.dll 

Ausführungssystem

Das Ausführungsmodul der gemeinsamen Sprachlaufzeit muss sicherstellen, dass der Code so wie erforderlich ausgeführt wird. Das Ausführungsmodul stellt Einrichtungen für den MSIL-Code bereit, z.B.:

  • Laden und Prüfen des Codes

  • Ausnahmeverwaltung

  • Just-In-Time-Kompilierung (JIT)

  • Speicherverwaltung

  • Sicherheit

IL (Intermediate Language)

Die .NET Framework-kompatiblen Compiler übersetzen den Quellcode in MSIL-Anweisungen und Metadaten. MSIL-Anweisungen sind mit den Anweisungen in der Baugruppensprache vergleichbar, aber dieser Code ist nicht spezifisch für eine einzelne physikalische CPU. Stattdessen wird MSIL sekundär in den systemeigenen Code kompiliert. Da MSIL computerunabhängig ist, kann MSIL zwischen einzelnen Computern verschoben werden. Die Übersetzung von MSIL in den systemeigenen Code kann in jeder Phase vor der Ausführung des Codes erfolgen.

Das Ausführungsmodul stellt mehrere JIT-Compiler bereit. MSIL kann in systemeigenen Code kompiliert werden,

  • wenn MSIL zum ersten Mal auf einem Computer installiert wird. -Oder-

  • nachdem MSIL in den Speicher geladen und bevor die MSIL-Anweisungen ausgeführt wurden.

Der folgende Code veranschaulicht den MSIL-Beispielcode für die Point-Klasse. MSIL ist eine intuitive Sprache, mit der i.d.R. folgende Informationen abgerufen werden können:

  • Der Name der Klasse (Point) und die Vererbung von System.Object.

  • Die Definition der Felder der Klasse (m_x und m_y).

  • Die Definition des Konstruktors (.ctor) und anderer Methoden (get_X und set_X).

... 
.class auto ansi Point extends ['mscorlib']System.Object 
{ 
  .field private int32 m_x 
  .field private int32 m_y 
  .method public specialname rtspecialname  
          instance void .ctor() il managed 
  { 
    // Codegröße       21 (0x15) 
    .maxstack  2 
    IL_0000:  ldarg.0 
    IL_0001:  call       instance void ['mscorlib']System.Object::.ctor() 
    IL_0006:  ldarg.0 
    IL_0007:  ldc.i4.0 
    IL_0008:  stfld      int32 Point::m_y 
    IL_000d:  ldarg.0 
    IL_000e:  ldc.i4.0 
    IL_000f:  stfld      int32 Point::m_x 
    IL_0014:  ret 
  } // Ende der Methode 'Point::.ctor' 
  .method public instance int32 get_X() il managed 
  ... 
  .method public instance void  set_X(int32 'value') il managed 
    .property int32 Y() 
  { 
    .set instance void set_Y(int32) 
    .get instance int32 get_Y() 
  } // Ende der Eigenschaft 'Point::Y' 
  ... 
... 

Verwaltet kontra nichtverwaltet

Die gemeinsame Sprachlaufzeit verwendet die Begriffe verwaltet und nichtverwaltet, um Code, Daten und Zeiger zu qualifizieren. Mit den Begriffen "verwaltet" oder "nichtverwaltet" wird gekennzeichnet, in welchem Umfang die Laufzeit die verschiedenen Aspekte des Programms kontrolliert. Alle verwalteten Elemente werden streng von der gemeinsamen Sprachlaufzeit kontrolliert.

Verwalteter Code stellt dem Ausführungsmodul die Informationen (Metadaten) bereit, die erforderlich sind, um eine sichere Ausführung des Moduls zu gewährleisten. Eine sichere Ausführung umfasst verschiedene Aspekte der Programmausführung, z.B. Debuggen, Interoperabilität zwischen den Sprachen, Speicherverwaltung und Sicherheit. Diese Informationen werden dem Ausführungsmodul nur durch den verwalteten Code bereitgestellt. Daher kann das Ausführungsmodul diese Dienste bei nichtverwaltetem Code nicht bereitstellen. Der größte Teil der auf den Windows-Plattformen ausgeführten Codeanweisungen ist nichtverwalteter Code.

Die Tatsache, dass verwalteter und nichtverwalteter Code in demselben Programm vorhanden sein können, stellt nicht zwangsläufig ein Problem dar. Bei der Interaktion zwischen verwaltetem und nichtverwaltetem Code muss jedoch mit besonderer Vorsicht vorgegangen werden. So kann der Garbagecollector z.B. nur ausgeführt werden, wenn alle Threads unterbrochen sind. Wenn ein Thread nichtverwalteten Code ausführt, kann das Ausführungsmodul den Thread nicht unterbrechen und somit kann keine Garbagecollection stattfinden. Ein weiterer Problembereich ist die Ausnahmebehandlung. Verwalteter Code gibt eine Ausnahme der gemeinsamen Sprachlaufzeit von .NET Framework aus, während der systemeigene Code ggf. eine WIN32-Ausnahme ausgibt. Wenn Ausnahmen zwischen verwaltetem und nichtverwaltetem Code verbreitet werden, muss die Ausnahme geändert werden, um dem geplanten Modell zu entsprechen.

Verwaltete Daten beschreiben Werte, die durch die gemeinsame Sprachlaufzeit dem Garbagecollectionheap zugeordnet wurden. Auf verwaltete Daten kann nur durch verwalteten Code zugegriffen werden, aber nichtverwaltete Daten sind sowohl für verwalteten als auch nichtverwalteten Code verfügbar.

Gemeinsame Sprachspezifikation

Wenn die gemeinsame Sprachlaufzeit als eine Sammlung vieler Programmiersprachenfeatures beschrieben werden kann, dann ist die gemeinsame Sprachspezifikation (CLS = Common Language Specification) eine Teilmenge dieser Features. Die Teilmenge stellt nicht die Gruppe aller Features dar, die in allen Programmiersprachen verfügbar sind. Sie ist vielmehr eine Gruppe von Features, die von vielen Programmiersprachen bereitgestellt wird.

Warum benötigen wir eine CLS? Der Hauptgrund besteht darin, dass die meisten Programmiersprachen ggf. aus bestimmten Gründen nicht alle diese Laufzeitfeatures unterstützen. Die CLS stellt eine Gruppe von Kompatibilitätsanforderungen dar, die von den meisten Programmiersprachen erfüllt werden können, sofern die Compilerentwickler eine Interoperabilität erzielen möchten. Diese funktionale Teilmenge ist zudem die Gruppe von Features, deren Unterstützung von den meisten .NET Framework-kompatiblen Tools angestrebt wird, z.B. von ASP+ (Active Server Pages+).

Die Größe der CLS im Vergleich zur gemeinsamen Sprachlaufzeit ist ein wichtiger Aspekt. Per Definition kann die CLS nicht größer als die Laufzeit sein. Wenn die CLS jedoch zu klein ist, stellt sie nicht genügend Funktionen bereit, um eine nützliche Wirkung zu erzielen. Derzeit erscheint es so, dass die CLS die Laufzeit vorwiegend unterstützt als ersetzt. Von dieser Perspektive aus betrachtet, ist es sinnvoller, die CLS danach zu beurteilen, in welchem Umfang sie die Laufzeit ersetzt.

Schlussfolgerung

Dieser Artikel gibt einen Überblick über die Hauptkomponenten von .NET Framework. Ein zentrales Thema dieses Artikels ist die Sprachintegration. Die gemeinsame Sprachlaufzeit vereinfacht die Integration der Programmiersprachen. So können die Programmierer Typen und Bibliotheken in einer Sprache verwenden und definieren und diese Typen dann nahtlos in andere Sprachen integrieren.

Danksagungen

Dieser Artikel basiert auf einem in Kürze erscheinenden Buch, das Mark Hammond und ich zum Thema Microsoft .NET geschrieben haben. Wir möchten den vielen Personen danken, die uns beim Schreiben dieses Buches unterstützt haben. Unser Dank geht an James Plamondon, der uns einen frühen Zugriff auf die Technologie ermöglicht hat, und an Brad Abrams und Jim Miller für die zahlreichen Diskussionen und Erläuterungen zum Thema .NET Framework. Wir möchten Jon Nicponski und Steve Christianson danken, die sich sorgfältig um die kleinen Details gekümmert haben, und auch allen Mitgliedern des Projekts 7. Ihre Gespräche und Zusammenkünfte gaben den Anstoß für einige unserer Ideen. Schließlich möchten wir den Personen danken, die die Architektur und die Compiler entwickelt und die Onlinedokumentation erstellt haben, mit der wir uns einen Einblick in das .NET Framework verschaffen konnten. Wir hoffen, dass dieser Artikel den Entwicklern einen Einstieg in das .NET Framework bietet.

Anzeigen: