(0) exportieren Drucken
Alle erweitern

Ist COM tot?

Veröffentlicht: 15. Dez 2001 | Aktualisiert: 17. Jun 2004

Von Don Box

Nachdem Microsoft die neue Plattform .NET vorgestellt hat, stellt sich die Frage, ob COM nun gestorben ist. Für mich bedeutet die Einführung von .NET die Weiterentwicklung eines Programmiermodells, das ich in den letzten sieben Jahren schätzen gelernt habe.

Auf dieser Seite

Frischer Wind in der Komponentenwelt
Die Grenzen der Typinformationen
CLR-Erweiterungen
Ist COM nun mausetot?

Diesen Artikel können Sie hier lesen dank freundlicher Unterstützung der Zeitschrift:

Bild04



Frischer Wind in der Komponentenwelt

Die PDC in Orlando im Juli 2000 war für mich die beste Microsoft-Konferenz seit der MTS-PDC in Long Beach im Jahr 1996. Beide Ereignisse stellten wichtige Meilensteine für Microsoft dar, in denen die Entwürfe für die neuen Plattformen und Programmiermodelle vorgestellt wurden. Dass mir persönlich beide PDCs so gut in Erinnerung sind, dürfte wohl an einer Frage liegen, die ich auf beiden PDCs ausführlich beantworten musste: "Bedeutet dies nun, dass COM tot ist?" Meine Antwort in Long Beach war ein klares Nein. Meine Antwort auf der letzten PDC war etwas differenzierter: Nun, es hängt davon ab, wie Sie COM definieren.

COM wird von den Leuten ganz unterschiedlich gesehen. Für mich ist COM ein Programmiermodell, mit dem man Komponenten integrieren kann, die jeweils einen bestimmten Typ darstellen. Das war der Hauptbeitrag von COM für das weite Feld der Komponentensoftware und dieser Beitrag hat die Art und Weise geändert, in der Millionen Programmierer heutzutage die Systeme bauen.

Die Grenzen der Typinformationen

Wer als Programmierer COM benutzt, lädt Programmcode, indem er von konkreten Typen neue Instanzen anlegt. Ist der Code einmal geladen, löst der Programmierer die Eintrittspunkte auf, indem er die Objektreferenzen in neue, abstrakte Typen umwandelt. (Anmerkung der Redaktion: Man hat nur nicht das Wort Typ verwendet, sondern Klasse, genauer gesagt: CoClass und Schnittstelle bzw. Interface. Was mit der Ersetzung dieser Begriffe durch "Typ" gewonnen ist, muss sich noch erweisen...) Um ersteres zu erreichen, bietet COM CoCreateInstance als typorientierte Alternative zur dateiorientierten LoadLibrary-Funktion an. Um letzteres zu bewerkstelligen, bietet COM QueryInterface als typorientierte Alternative zur symbolorientierten GetProcAddress-Funktion an. Wenn Sie dieses Programmiermodell in seiner barocken Schönheit genießen möchten, werfen Sie einen letzten Blick auf den COM- und C++-Code aus Listing L1. Nirgendwo stehen LoadLibrary- oder GetProcAddress-Aufrufe. Trotzdem führt dieser Code zum Laden einer externen Komponente und zur Auflösung der Eintrittspunkte, als seien diese beiden Funktionen aufgerufen worden.

L1 Typischer COM-Code

IAntique *pAntique = 0; 
HRESULT hr = CoCreateInstance(CLSID_Pinto, 0, 
    CLSCTX_ALL, IID_IAntique, (void**)&pAntique); 
if (SUCCEEDED(hr)) { 
  ICar *pCar = 0; 
  hr = pAntique->QueryInterface(IID_ICar,  
                                (void**)&pCar); 
  if (SUCCEEDED(hr)) { 
    hr = pCar->AvoidFuelTankCollision(); 
    if (SUCCEEDED(hr)) { 
      hr = pAntique->Appreciate(); 
    } 
    pCar->Release(); 
  } 
  pAntique->Release(); 
}

Tief im Code aus Listing L1 ist bereits die Zukunft von COM verborgen. Der Code zeigt sehr schön die Spannung zwischen dem Typsystem von COM und dem Typsystem der benutzten Programmiersprache, in diesem Fall C++. Für jede Objektreferenz, die an den Aufrufer übergeben wird, muss nämlich auch eine UUID übergeben werden (in diesem Beispiel IID_IAntique oder IID_ICar). Das liegt einfach daran, dass die Typkennung der Programmiersprache (im Falle von C++ std::type_info) nicht zum Format der Typkennung von COM passt.

Im Laufe der Jahre hat das C++-Team von Microsoft einige neue Methoden zur Überbrückung der Kluft zwischen den Typsystemen von COM und C++ eingeführt, von denen die Erweiterungen __uuidof und __declspec(uuid) wohl die wichtigsten waren. Diese Erweiterungen ermöglichen es dem Compiler, die COM-Typkennung mit einem symbolischen Typnamen von C++ zu verknüpfen. Durch den Einsatz von __uuidof wird der Code etwas übersichtlicher, wie Listing L2 zeigt.

L2 Durch __uuidof wird der Code übersichtlicher

IAntique *pAntique = 0; 
HRESULT hr = CoCreateInstance(__uuidof(Pinto), 0, 
    CLSCTX_ALL, __uuidof(pAntique), (void**)&pAntique); 
if (SUCCEEDED(hr)) { 
  ICar *pCar = 0; 
  hr = pAntique->QueryInterface(__uuidof(pCar),  
                                (void**)&pCar); 
  if (SUCCEEDED(hr)) { 
    hr = pCar->AvoidFuelTankCollision(); 
    if (SUCCEEDED(hr)) { 
      hr = pAntique->Appreciate(); 
    } 
    pCar->Release(); 
  } 
  pAntique->Release(); 
}

Im Gegensatz zum Code aus Listing L1 wird auch dann noch die richtige Schnittstellenkennung an CoCreateInstance übergeben, wenn sich der Typ von pAntique ändert, denn der Operator __uuidof setzt für pAntique immer die richtige Schnittstellenkennung ein.

Trotz des Einsatzes von __uuidof macht der Code im Listing L2 ausgiebigen Gebraucht von der (void **)-Typkonvertierung. Diese Konvertierungen sind in der COM-Programmierung mit C++ ständig anzutreffen und auch notwendig. Damit COM mit C++ zusammenarbeiten kann, müssen Sie das C++-Typsystem außer Gefecht setzen, bis die COM+Objektreferenz wieder nach C++ übersetzt ist. Im Gegensatz dazu erlaubt die virtuelle Maschine (VM) für Java folgende Formulierung:

IAntique antique = new Pinto(); 
ICar car = (ICar)antique; 
car.AvoidFuelTankCollision(); 
antique.Appreciate();


Unter dem Strich ist das Ergebnis dasselbe wie in Listing L2. Beide Beispiele laden eine Komponente anhand ihres Typs und nicht anhand ihres Dateinamens. Beide Beispiele lösen die Eintrittspunkte der Komponente mit Typoperationen auf und nicht mit symbolischen Eintrittspunkten. Der wesentliche Unterschied besteht darin, dass die Microsoft-VM für Java in der Lage ist, das Typsystem von Java elegant mit dem Typsystem von COM zu verbinden. Als Programmierer braucht man das nicht mehr von Hand zu machen. Genau das ist der Grund für die Entwicklung der neuen Plattform - nämlich eine universelle Laufzeitschicht für die Komponentenintegration anzubieten, in die sich jeder Compiler, jedes Werkzeug und jeder Dienst sauber einbinden lässt.

Die PDC von Orlando war zwar als .NET-PDC gedacht, aber für COM-Programmierer findet die wesentliche Umstellung in der "gemeinsamen Laufzeitschicht für alle Sprachen" oder CLR statt. Diese CLR (common language runtime) stellt eine neue Implementierung der Ideen dar, die zuerst im COM-Programmiermodell ihre weite Verbreitung fanden. Dank der CLR laden die Programmierer ihre Komponenten anhand des Typs und nicht anhand des Dateinamens. Sie lösen die Eintrittspunkte durch Typanpassungen auf und nicht mit symbolischen Eintrittspunkten. Das CLR-Programmiermodell unterscheidet sich in vielen wichtigen Aspekten kaum vom COM-Programmiermodell. Wenn das CLR-Programmiermodell aber so sehr dem COM-Programmiermodell ähnelt, warum dann überhaupt der ganze Aufwand? Nun, die Antwort liegt in der Implementierung.

In meinem Artikel "Ein Lagebericht zum Thema COM", (System Journal 06/1998, S. 106) habe ich die fünf wichtigsten Probleme mit COM angeführt, die es damals meiner Ansicht nach gab. Praktisch alle diese Probleme haben mit dem bedauerlichen Zustand der COM-Typinformation zu tun. Im klassischen COM gibt es drei Arten von Informationsformaten: IDL, Typbibliotheken und die vom MIDL generierten /Oicf-Strings, die in die Proxy/Stub-DLLs eingebettet werden.

Keines dieser Formate wurde als Standard betrachtet und es war möglich, in einem Format Informationen unterzubringen, für die es in den anderen beiden keine Repräsentation gab. Aus diesem Grund gab es auch keine Standardmethode zur Typbeschreibung in COM. Das hat nicht nur dem durchschnittlichen COM-Programmierer das Leben erschwert, der möglichst schnell mit dem aktuellen Projekt fertig werden musste, sondern auch den Entwicklern der COM-Infrastruktur. Mit der Zeit haben sich zwar die Typbibliotheken als De-Facto-Standard herauskristallisiert, aber es ist durchaus möglich, in Typbibliotheken Dinge unterzubringen, die sich nicht in IDL einsetzen lassen.

Neben dem fehlenden Standard für die Typinformationen berücksichtigt COM auch nur eine Teilmenge der Komponententypen. COM-Typbibliotheken beschreiben nur die Typen, die von einer Komponente exportiert werden. Mit den Informationen über die exportierten Typen können die COM-Infrastruktur und die Entwicklungswerkzeuge Dinge anbieten wie IntelliSense in Visual Basic oder die deklarative Dienstarchitektur von COM+. Soviel zu den guten Nachrichten.

Leider macht COM gar nicht erst den Versuch, der Außenwelt bekannt zu geben, auf welche externen Komponenten eine Komponente angewiesen ist, um ordnungsgemäß zu arbeiten. Obwohl sich COM alle Mühe gibt, die Anzeige von "dumpbin.exe /exports" überflüssig zu machen, kümmert es sich nicht um das "/imports"-Problem. Durch die fehlenden Angaben über etwaige Abhängigkeiten kann das System praktisch nicht mehr herausfinden, welche DLLs (und welche Versionen) installiert sein müssen, damit eine Komponenten wie geplant arbeiten kann.

Als weitere wichtige Schwäche der COM-Typinformationen ist zu verzeichnen, dass es keine Informationen über die privaten Typen gibt, die in der Komponente intern benutzt werden. Obwohl die COM-Advokaten lange für die Neutralität COMs in puncto Objektrepräsentation warben, verhinderte diese Haltung à la Schweiz lange bestimmte Dienste, zum Beispiel die automatische Serialisierung, die Nachführung von Objektgraphen oder die automatische Überprüfung von Pre/Post-Bedingungen.

Die meisten Entwickler, die sich ernsthaft mit COM beschäftigen, wünschen sich schon lange perfekte Typinformationen. Und das ist genau das, was die gemeinsame Laufzeitschicht endlich bietet.

CLR-Erweiterungen

Das wichtigste Angebot der CLR ist die allgegenwärtige, erweiterbare und sehr genaue Typinformation. In der CLR gibt es für alle Typen zur Laufzeit Typbeschreibungen, also nicht nur für die Typen, die von einer Komponente exportiert werden. In der CLR können Sie sich jedes Objekt herauspicken und jeden Aspekt seiner Typbeschreibung erforschen, einschließlich seiner Repräsentation. Das ist eine wesentliche Abkehr vom klassischen COM, wo die öffentlichen Schnittstellen zwar herauszufinden waren, die Objektrepräsentationen aber unbekannt blieben.

Das CLR-Programmiermodell dreht sich um Typen. Wie schon COM ersetzt auch die CLR den Lader durch einen eigenen Lader, der nun mit den Typen umgehen kann. Außerdem ersetzt die CLR die zugrundeliegende Symboltabelle durch einen eigenen Referenzauflösungsmechanismus auf Typbasis. In dieser Hinsicht ist die CLR funktional mit COM identisch. Wesentlich interessanter ist schon der Aspekt, wie die CLR die Komponentenentwicklung gegenüber COM ausweitet. Im folgenden möchte ich meine 11 Favoriten unter den Erweiterungen kurz vorstellen.

  1. Komponenten sind Bürger erster Klasse
    Im klassischen COM wurde der Begriff "Komponente" relativ schnell überladen und nicht nur für eine DLL benutzt, die eine oder mehr Klassen exportiert, sondern auch für die Objekte selbst oder für deren Klassen. (Mir gefällt die ursprüngliche Definition am besten - eine Komponente im Sinne von COM ist eine DLL). Die CLR richtet das Hauptaugenmerk weniger auf die Komponente, sondern auf die "Baugruppe" (Assembly).

    Eine Baugruppe ist eine Typmenge, die als eine unteilbare Einheit eingesetzt wird. Baugruppen sind logische Typsammlungen, deren Implementierungscode sich über mehrere Module verteilen kann (DLLs oder EXEs). Der Name der Baugruppe bezeichnet auch den Gültigkeitsbereich der enthaltenen Typnamen, so dass keine UUIDs für die einzelnen Typen mehr erforderlich sind. Solange nicht zwei Baugruppen denselben Namen tragen, wird es auch keine Namenskollisionen unter den Komponenten mehr geben.

    Für einzelne Komponenten, die von einer einzigen Anwendung benutzt werden, sind die Dateinamen der Baugruppe hinreichend unverwechselbar. Werden die Komponenten dagegen von vielen Anwendungen benutzt, können die CLR-Baugruppen auch "starke Namen" erhalten (strong names). Eine Baugruppe mit solch einem Namen hat einen öffentlichen Schlüssel, der 128 Byte lang ist und den Entwickler der Komponente identifiziert. Beim Linken solch einer Baugruppe mit einem Client-Programm wird in den Metadaten auch ein 64-Bit-Code abgelegt, der sich aus dem öffentlichen Schlüssel ableitet. Zur Laufzeit wird der öffentliche Schlüssel der Baugruppe zur Überprüfung, ob die korrekte Baugruppe geladen wurde, mit diesem 64-Bit-Code aus den Metadaten des Clients verglichen.

    In COM gibt es für jeden Typ eine UUID mit 128 Bit, die Verwechselungen verhindern soll. In der CLR hat jede Baugruppe einen öffentlichen 128-Byte-Schlüssel, der in Kombination mit lokal unverwechselbaren symbolischen Typnamen für eine globale Unverwechselbarkeit der Typidentifizierer sorgt. Das Ergebnis unter dem Strich ist letztlich dasselbe: die Typen haben global unverwechselbare Identifizierer.

  2. Es gibt nur ein Austauschformat für Metadaten
    Wie schon erwähnt, wurden die Typinformationen von COM entweder in Textform (IDL) oder in Binärform (TLB) ausgetauscht. Im Gegensatz dazu werden CLR-Typinformationen immer in einer einzigen, dokumentierten Binärform ausgetauscht. Alle CLR-fähigen Werkzeuge und Compiler generieren und lesen die Metadaten in diesem Format. Wenn er also eine neue Schnittstelle definiert, arbeitet der Entwickler einfach in seiner gewohnten Programmiersprache weiter und überlässt des dem Compiler, die entsprechende binäre Typbeschreibung zu generieren. Er braucht nicht mehr eine Syntax zur Beschreibung der Typen zu lernen (IDL) und eine zweite zu deren Implementierung (Visual Basic oder C++). Betrachten Sie zum Beispiel den folgenden IDL-Code für COM:

    [ uuid(ABBAABBA-ABBA-ABBA-ABBA-ABBAABBAABBA) ] 
    library MyLib { 
      importlib("stdole32.tlb"); 
      [ uuid(87653090-D0D0-D0D0-D0D0-18121962FADE) ] 
      interface ICalculator : IUnknown { 
        HRESULT Add([in] double n,  
                    [in, out] VARIANT_BOOL *round, 
                    [out] VARIANT_BOOL *overflow, 
                    [out, retval] double *sum); 
      } 
    }
    


    Der entsprechende CLR-Typ lässt sich folgendermaßen in C# ausdrücken:

    namespace MyLib { 
      interface ICalculator { 
        double Add(double n,  
                   ref bool round,  
                   out bool overflow); 
      } 
    }
    


    Kompiliert man nun diesen Quelltext folgendermaßen mit dem C#-Compiler:

    csc.exe /t:library /out:mylib.dll mylib.cs
    

    so lässt sich die Schnittstelle mit dem Schalter /r: leicht in Visual Basic importieren:

    vbc.exe /r:mylib.dll program.vb
    


    Die CLR macht die Definition von Typen keineswegs überflüssig. Sie ermöglicht es dem Entwickler einfach nur, die Typen in der Sprache seiner Wahl zu definieren.

    Neben der Definition eines binären Standardformats für die Typbeschreibungen bietet die CLR auch die entsprechende Unterstützung zum Lesen und Schreiben von Typbeschreibungen an. Listing L3 zeigt ein kleines Programm, das eine Baugruppe mit der Typdefinition für die bereits gezeigte Schnittstelle MyLib.ICalculator generiert. Die von diesem Programm erzeugte Typdefinition lässt sich nicht von der unterscheiden, die der C#-Compiler erzeugt (oder Visual Basic, oder C++, oder Perl, oder Python...).

    L3 Hier wird eine Baugruppe generiert

    //////////////////////////////////////////////////// 
    // 
    // emititf.cs - Dezember 2000, Don Box 
    //  
    // Dieser Code wirft mit System.Reflection.Emit eine Baugruppe aus, 
    // die folgende Schnittstellendefinition enthält: 
    // 
    // namespace MyLib { 
    //   public interface ICalculator { 
    //     double Add(double n, ref double round, out double overflow); 
    //   } 
    // } 
    // 
    using System; 
    using System.Reflection; 
    using System.Reflection.Emit; 
    public class emititf 
    { 
      // Der Haupteintrittspunkt des Programms 
      public static int Main(String[] argv) 
      { 
        // baue eine neue Baugruppe 
        AssemblyBuilder ab = DefineNewAssembly(); 
        // definiere in dieser Baugruppe die Schnittstelle ICalculator 
        TypeBuilder tb = DefineICalculator(ab); 
        // Setze die "Add"-Methode in ICalculator ein... 
        MethodBuilder method = DefineAddMethod(tb); 
        // ...und gib ihr die gewünschten Parameter 
        DefineAddParameters(method); 
        // Alles bei 220 Grad fünf Minuten überbacken... 
        Type t = tb.CreateType(); 
        // ... und fertig ist die Baugruppe. Speichere die Baugruppe 
        // (und das Modul) auf dem Laufwerk. 
        ab.Save("mylib.dll"); 
        return 0; 
      } 
      // Hilfsfunktion für den Bau der Baugruppe "mylib" 
      static AssemblyBuilder DefineNewAssembly() 
      { 
        // neue Baugruppen werden in der Domäne der Anwendung definiert. 
        AppDomain current = AppDomain.CurrentDomain; 
        // neue Baugruppen brauchen einen Namen. Wir benutzen hier einen  
        // einfachen ("schwachen") Namen. 
        AssemblyName an = new AssemblyName(); 
        an.Name = "mylib"; 
        AssemblyBuilderAccess access = AssemblyBuilderAccess.Save; 
        // DefineDynamicAssembly erledigt die ganze Arbeit 
        return current.DefineDynamicAssembly(an, access); 
      } 
      // Hilfsfunktion zur Erstellung einer neuen Schnittstelle mit Namen 
      // "MyLib.ICalculator" 
      static TypeBuilder DefineICalculator(AssemblyBuilder ab) 
      { 
        // Alle Typen leben in einem Modul. Also definiere eines für diese 
        // Baugruppe 
        ModuleBuilder mb = ab.DefineDynamicModule("mylib.dll",  
                                                  "mylib.dll"); 
        // alle Schnittstellen brauchen die Flags Interface und Abstract  
        TypeAttributes attrs =  
            TypeAttributes.Interface|TypeAttributes.Abstract; 
        // öffentliche Schnittstellen brauchen natürlich auch noch das Flag  
        // public 
        attrs |= TypeAttributes.Public; 
        // DefineType erledigt die Arbeit 
        return mb.DefineType("MyLib.ICalculator", attrs); 
      } 
      // Diese Hilfsfunktion erzeugt die Methode 
      // "double Add(double, ref double, out double)" 
      static MethodBuilder DefineAddMethod(TypeBuilder itf) 
      { 
        // Schnittstellenmethoden müssen abstract, virtual und public 
        // sein 
        MethodAttributes attrs = MethodAttributes.Public 
          | MethodAttributes.Abstract | MethodAttributes.Virtual; 
        // die Methoden werden anhand ihres Namens und ihrer 
        // Typsignatur identifiziert 
        Type resultType = typeof(double); 
        Type[] paramTypes = new Type[] {  
          typeof(double),  
          Type.GetType("System.Boolean&"),  
          Type.GetType("System.Boolean&")  
        }; 
        // DefineMethod erledigt die Arbeit 
        return itf.DefineMethod("Add", attrs, resultType, paramTypes); 
      } 
      // diese Hilfsfunktion definiert die Parameternamen und Richtungen 
      static void DefineAddParameters(MethodBuilder method) 
      { 
        // die Parameter 1 und 2 haben keine speziellen Attribute, 
        // sondern einfach nur Namen 
        method.DefineParameter(1, ParameterAttributes.None, "n"); 
        method.DefineParameter(2, ParameterAttributes.None, "round"); 
        // für Parameter 3 muss das Out-Flag gesetzt werden 
        ParameterBuilder pb = method.DefineParameter(3,  
                              ParameterAttributes.Out, "overflow"); 
        // außerdem wird für Parameter 3 das Attribut Interop.Out  
        // gesetzt 
        AddInteropOutAttribute(pb); 
      } 
      // diese Hilfsfunktion gibt einem Parameter das Attribut 
      // [Interop.Out] 
      static void AddInteropOutAttribute(ParameterBuilder param) 
      { 
        // anwenderdefinierte Attribute werden von den Konstruktoren 
        // identifiziert 
        Type attrtype =  
             typeof(System.Runtime.InteropServices.OutAttribute); 
        ConstructorInfo outattrctor = attrtype.GetConstructors()[0]; 
        // CustomAttributeBuilder serialisiert die Konstruktorargumente 
        CustomAttributeBuilder outattr = new         
              CustomAttributeBuilder(outattrctor, new object[0]); 
        // SetCustomAttribute erledigt die Arbeit 
        param.SetCustomAttribute(outattr); 
      } 
    }
    
  3. Metadaten sind Pflicht
    In COM war es möglich, mit C++ private Schnittstellen zu definieren, die nie in IDL oder einer Typbibliothek beschrieben wurden. Ein wesentlicher Grund für solche Konstruktionen dürfte es wohl sein, eine Art Hintertür ins Objekt offen zu halten, die nicht zum dokumentierten Teil der Komponente gehört. Leider musste diese Lösung versagen, wenn man einen kontext-, apartment-, prozess- oder maschinenübergreifenden Zugriff brauchte. Ohne Typinformationen kann das System die Schnittstellenaufrufe einfach nicht weiterleiten.

    In der CLR müssen alle Typen durch entsprechende Typinformationen dokumentiert werden. Dazu gehören auch die privaten Typen, die eigentlich gar nicht für die Zusammenarbeit mit anderen Komponenten gedacht sind. Allerdings ermöglicht die CLR komponenten-private Typen, indem man die Typen (und deren relevanten Teile) so kennzeichnet, dass sie nur in der definierenden Baugruppe zugänglich sind.

    internal interface IBob { 
      void hibob(); 
    }
    


    Im Gegensatz dazu ist die folgende Schnittstelle für alle Baugruppen sichtbar:

    public interface IBob { 
      void hibob(); 
    }
    
  4. Metadaten sind umfassend erweiterbar
    Die COM-Typinformationen waren mit dem IDL-Attribut custom und dem entsprechenden TLB-Format erweiterbar. Die custom-Attribute von COM verknüpfen ein UUID/VARIANT-Paar mit einer Bibliothek, Schnittstelle, Coklasse, Methode, Parameter, Struktur oder einem Feld. Allerdings wurden die custom-Attribute von COM im Lauf der Jahre nur recht selten verwendet, hauptsächlich vom MTS, in dem es möglich ist, den Anfangswert des Transaktionsattributs einer Klasse auf ein wohlbekanntes custom-Attribut zu setzen (definiert in mtxattr.h und in Visual Basic über das Transaction-Property zugänglich). Leider bot Visual Basic keine Möglichkeit zur Definition oder zum Einsatz von zusätzlichen custom-Attributen. Daher waren custom-Attribute für die große Mehrheit der COM-Entwickler ziemlich nutzlos.

    Die CLR-Typinformationen sind von jeder Sprache aus umfassend erweiterbar. In der CLR sind custom-Attribute einfach nur serialisierte Konstruktoraufrufe. Jede Sprache definiert ihre eigene Syntax für die Angabe der Attribute. In C# fügen Sie einfach vor der Definition des Ziels einen Konstruktoraufruf ein, und zwar in eckigen Klammern:

    [ Color("Red") ] 
    class MyClass { }
    


    In Visual Basic .NET fügen Sie den Konstruktoraufruf in spitzen Klammern vor dem Namen des Ziels ein:

    Class <Color("Red")> MyClass 
    End Class
    


    In beiden Fällen geht aus den resultierenden Metadaten hervor, dass Red die Farbe von MyClass ist.

    Zur Definition von neuen anwenderdefinierten Attributen müssen Sie eine neue Klasse definieren, die System.Attribute erweitert und einen öffentlichen Konstruktor hat:

    using System; 
    [ AttributeUsage(AttributeTargets.All) ] 
    public class ColorAttribute : Attribute { 
      public String color; 
      public ColorAttribute(string c) { color = c;} 
    }
    


    Das Attribut AttributeUsage zeigt an, auf welche Art von Metadatenkonstrukt (Klasse, Methode, Property und so weiter) dieses Attribut anwendbar ist. Wenn der Name der Attributklasse auf Attribute endet, kann man das Attribut übrigens mit oder ohne diese Endung anwenden. So sind die beiden folgenden Zeilen in der Wirkung gleich, obwohl die zweite Form nur relativ selten benutzt wird:

    [ Color("Red") ] class MyClass { } 
    [ ColorAttribute("Red") ] class MyClass { }
    


    Anwenderdefinierte Attribute werden per Reflektion zur Laufzeit verfügbar gemacht. Der Code in Listing L4 ermittelt die Farbe, die einer gegebenen Klasse gegeben wurde (sofern vorhanden). Attribute sorgen für eine saubere, typzentrische Erweiterung des CLR-Typbeschreibungsformats.

    L4 Ermitteln eines Anwenderdefinierten Attributs

    using System; 
    String GetColor(Object o) { 
    // ermittle den Typ des Zielobjekts 
      Type t = o.GetType(); 
    // bestimme die Art des gesuchten Attributs 
      Type at = typeof(ColorAttribute);  
    // fordere alle Attribute dieses Typs an 
      Attribute[] rga = t.GetCustomAttributes(at); 
    // falls nicht vorhanden, gib auf 
      if (rga.Length == 0) 
        return null; 
    // sofern vorhanden, konvertieren und Information auslesen 
      ColorAttribute color = (ColorAttribute)rga[0]; 
      return color.color; 
    }
    
  5. Dynamische Aufrufe sind leicht möglich
    Da die Typinformationen sehr präzise und praktisch allgegenwärtig sind, kann die Laufzeitschicht jede Methode jedes Objekts ohne Intervention eines Objekts dynamisch aufrufen. Das bedeutet, das alle CLR-Objekte auch in typenlosen Programmiersprachen einsetzbar sind.

  6. Der Aufbau der Objekte bleibt geheim
    Die CLR baut ganz auf die perfekte Typinformation auf. Das bedeutet, dass kein Typ unbeschrieben bleibt, bis hinunter zu den Typen und Namen der Datenelemente eines Typs. Trotzdem bleibt der genaue Aufbau eines gegebenen Typs und seiner Instanzen im Speicher geheim. Die Positionen, Größen und Ausrichtungen der Datenelemente bleiben geheim, ebenso der genaue Aufbau der Sprungtabellen für die Methodenaufrufe. Wenn ein CLR-Objekt aus der Laufzeitschicht hinaus exportiert werden muss, legt die CLR eine von COM aufrufbare Hüllschicht an, die für die Umsetzung von der klassischen COM-Aufrufkonvention mit IUnknown und __stdcall auf die internen Konventionen der Laufzeitschicht sorgt. Für Anwendungsentwickler, die sich um ihre Produktivität Gedanken machen, sind das gute Nachrichten, denn die Laufzeitschicht regelt vieles anhand der Metadaten. Für die ewig Neugierigen, die noch gerne mit Bits herumpfriemeln und wissen, was ein Taktzyklus ist, bedeutet dies, dass für die dynamische Erzeugung von neuen Typen und zum Abfangen des Zugriffs auf vorhandene Typen neue Verfahren gebraucht werden. System.Reflection.Emit sorgt für die Fähigkeit, neue Typen zu generieren. Und System.Runtime.Remoting sorgt für die Fähigkeit, Zugriffe auf vorhandene Typen abzufangen.

  7. Die CLR-Typhierarchie hat nur eine Wurzel
    Das COM-Typsystem hat zwei Wurzeln. IUnknown war der Wurzeltyp für Objektreferenzen und VARIANT war der Wurzeltyp für alle Werte. Im CLR-Typsystem gibt es kein IUnknown mehr und kein VARIANT. Statt dessen sind sämtliche Typen Erweiterungen von System.Object. Jawohl, auch einfache Grundtypen wie int und double sind Erweiterungen von System.Object. (Genauer gesagt, sind sie als solche Erweiterungen darstellbar.) Die Funktionalität des VARTYPE-Felds ist in der Methode System.Object.GetType zu finden. Und so könnte man den C++-Code aus dem oberen Teil von Listing L5 in C# ungefähr so formulieren, wie es der untere Teil von Listing L5 zeigt. Beachten Sie bitte, dass der C#-Code Operatortests für die Typkompatibilität hat und dass der Konvertierungsoperator die Umwandlung für Objektreferenzen und für Werttypen übernimmt.

    L5 Erweiterungen von System.Object

    C++

    void Process(VARIANT value) { 
      switch (value.vt) { 
        case VT_I2: 
          ProcessAsShort(value.iVal); 
          break; 
        case VT_R8: 
          ProcessAsDouble(value.dblVal); 
          break; 
        case VT_BSTR: 
          ProcessAsString(value.bstrVal); 
          break; 
        case VT_UNKNOWN: 
        case VT_DISPATCH: 
          IFoo *pFoo; 
          IBar *pBar; 
          hr = value.punkVal->QueryInterface(&pFoo); 
          if (SUCCEEDED(hr)) 
            ProcessAsFoo(pFoo); 
          else { 
            hr = value.punkVal->QueryInterface(&pBar); 
            if (SUCCEEDED(hr)) 
              ProcessAsBar(pBar); 
          } 
          break; 
      } 
    }
    

    C#

    void Process(System.Object value) { 
      if (value is short) 
          ProcessAsShort((short)value); 
      else if (value is double) 
          ProcessAsDouble((double)value); 
      else if (value is System.String) 
          ProcessAsString((System.String)value); 
      else if (value is IFoo) 
          ProcessAsFoo((IFoo)value); 
      else if (value is IBar) 
          ProcessAsBar((IBar)value); 
    }
    
  8. Klassen können öffentliche Elemente enthalten
    In COM waren Klassen benannte Implementierungen von einer oder mehreren Schnittstellen. Klassen konnten keine Methoden oder Attribute anbieten, die nicht zu irgendeiner COM-Schnittstelle gehörten. In der CLR können Klassen auch öffentliche Elemente haben. Daher können Methoden eine spezielle Schnittstellenimplementierung benutzen und auch andere Programme ermöglichen, die Schnittstellen vollständig vermeiden.

  9. Mehrfachvererbung von Schnittstellen ist möglich
    COM-Schnittstellen waren nur zur einfachen Vererbung fähig. Der Hauptgrund dafür war die Erhaltung der Binärkompatibilität zwischen verschiedenen Compilern. Das Typsystem der CLR macht es nun möglich, dass Schnittstellen mehr als nur eine Schnittstelle erweitern (in der Sprache des vorigen Jahrtausends hätte man noch gesagt: "mehrere Schnittstellen beerben"). Das erlaubt einen neuen Schnittstellenvertrag, der mehr als nur eine Basisschnittstelle einbezieht. Betrachten Sie zum Beispiel folgende Schnittstellengruppe:

    public interface IMachine { } 
    public interface IAsset { } 
    public interface IRobot : IAsset, IMachine { }
    


    Der Typ IRobot bedeutet, dass eine Objekt eine Maschine und eine Hilfe sein muss, um als Roboter zu gelten. Im klassischen COM gab es nicht die Möglichkeit, solche Beschränkungen zu formulieren, ohne auf zusätzliche Laufzeittests zurückzugreifen.

  10. Delegates als alternativer Komponentenkleber
    In COM erfolgen sämtliche Methodenaufrufe über Objektreferenzen, die zu einem bestimmten Schnittstellentyp gehören. Zur Erweiterung einer Schnittstelle musste man eine neue Schnittstelle von ihr ableiten und die Erweiterungen implementieren, die es in der neuen Schnittstelle geben sollte. Werfen Sie einen Blick auf den folgenden C#-Typ, der zur Erweiterbarkeit eine Schnittstelle benutzt:

    public interface IHook { 
      void DoHookWork(); 
    } 
    public class MyClass 
    { 
      IHook hook; 
      public void RegisterHook(IHook h) { hook = h; } 
      public void f() { 
        if (hook != null) 
          hook.DoHookWork(); 
      } 
    }
    


    Wenn Sie nun MyClass erweitern möchten, implementieren Sie einfach die Schnittstelle IHook.

    public class MyHook : IHook { 
      public void DoHookWork() { 
        System.Console.WriteLine("Hook was called"); 
      } 
    }
    


    Anschließend melden Sie mit RegisterHook eine Instanz an:

    MyClass mc = new MyClass(); 
    mc.RegisterHook(new MyHook()); 
    mc.f(); // ruft MyHook.DoHookWork()
    


    auf
    Wie Sie sehen, lässt sich dieses Verfahren in die CLR übernehmen. Allerdings bietet die CLR auch eine Alternative an, die "Delegierte" genannt wird (Delegates).

    Diese Delegates dienen zur Bindung eines Methodenaufrufs an eine Variable. Sie sind eine Art Kreuzung zwischen C-Funktionszeigern und C++-Zeigern auf Methoden. Delegierte werden normalerweise als eine Art Verbindungskanal zwischen Methodenaufruf und der Zielmethode benutzt. Man kann Delegierte auch als Schnittstellen betrachten, die nur eine Methode haben, mit dem Unterschied, dass der Zieltyp nur eine Methode zu haben braucht, deren Signatur zur Signatur des Delegierten passt. Betrachten Sie dazu die folgende Variante des obigen Beispiels auf Delegiertenbasis:

    public delegate void Hook(); 
    public class MyClassEx 
    { 
      Hook hook; 
      public void RegisterHook(Hook h) { hook = h; } 
      public void f() { 
        if (hook != null) 
          hook(); // Erweiterungsfunktion aufrufen 
      } 
    }
    


    Es wurde keine Schnittstelle, sondern ein einfacher Delegiertentyp definiert. Der Aufruf der Erweiterungsfunktion erfolgt mit einer Syntax, die C-Funktionszeigern ähnelt. Unter der Haube generiert der C#-Compiler einen Aufruf der Invoke-Methode des Delegate-Typs, die wiederum die gewünschte Methode des Zielobjekts aufruft.

    Zur Entwicklung einer Erweiterungsfunktion für MyClassEx brauchen Sie nur eine Methode zu implementieren, deren Signatur mit der Signatur des Hook-Delegierten übereinstimmt.

    public class MyHookEx { 
      public void AnyNameIWant() { 
        System.Console.WriteLine("Hook was called"); 
      } 
    }
    


    Dieser Typ braucht keine explizite Referenz auf den Delegiertentyp Hook. Statt dessen hat er einfach eine Methode, deren Signatur mit der Hook-Signatur übereinstimmt. Dadurch wird dieser Typ ein Kandidat für die Anmeldung als Erweiterungsfunktion von MyClassEx. Zur Anmeldung der Erweiterungsfunktion legen Sie eine Instanz von einem neuen Delegierten an, der auf dem Hook-Delegiertentyp beruht:

    MyClassEx mc = new MyClassEx(); 
    mc.RegisterHook(new Hook(MyHookEx.AnyNameIWant)); 
    mc.f(); // calls MyHookEx.AnyNameIWant()
    


    Beachten Sie bitte die ungewohnte Syntax für die Initialisierung des Delegierten. Unter der Haube generiert C# den Code zur Initialisierung des neuen Delegiertenobjekts mit den Metadaten-Token für die gewünschte Methode.

  11. Aspektorientierte Programmierung
    MTS und COM+ haben die "aspektorientierte Programmierung" im großen Stil eingeführt. Sie haben es den Entwicklern ermöglicht, domänenneutrale Aspekte der Programme aus dem Quelltext herauszunehmen und in deklarativen Attributen unterzubringen. Außerdem haben MTS und COM+ für den Arbeitsbereich eines Objekts den Begriff "Kontext" eingeführt. In diesem Zusammenhang teilen Kontexte die Prozesse in kleinere Einheiten auf und enthalten zudem geordnete Sammlungen von benannten Kontextproperties, die durch Klassenattribute wie Synchronization, ThreadingModel, Transaction und so weiter kontrolliert werden. Die CLR entwickelt dieses Konzept noch weiter und bietet eine wesentlich übersichtlichere Implementierung an.

    In MTS und COM+ war die Menge der Klassenattribute und Kontextproperties fest vorgegeben. In der CLR kann jeder ein neues Klassenattribut definieren, das den Kontext eines Objekts durch neue Properties erweitert. Dadurch ist der normale Entwickler in der Lage, Dienste zu definieren, die über Attribute bzw. Kontextproperties ausgedrückt und durch Abfangmechanismen implementiert werden.

    In COM+ wird der Gültigkeitsbereich von Objektreferenzen durch einen Kontext begrenzt und zur gemeinsamen Nutzung von Objektreferenzen, die in globalen Variablen liegen, war die GIT erforderlich (Global Interface Table). In der CLR wird der Gültigkeitsbereich von Objektreferenzen durch eine AppDomain festgelegt (dem CLR-Äquivalent eines Prozesses) und die Objektreferenzen lassen sich zudem problemlos in globalen Variablen unterbringen, ohne ein zusätzliches Marshaling zu erfordern.

    In COM+ sind alle Objekte an den Kontext gebunden, in dem sie initialisiert wurden, und werden an alle anderen Kontexte automatisch als Referenz weitergegeben (marshal by reference). Mit dem Ergebnis, dass sogar Objekte, die sich keine Spur um irgendwelche Transaktionen oder deklarative Sicherheit kümmern, in einem bestimmten Kontext innerhalb eines Prozesses festliegen. Um das zu vermeiden, aggregieren gemeinsam benutzte Objekte meistens den FTM (Freethreaded Marshaler), der ihnen innerhalb des Prozesses eine gewisse Beweglichkeit in den Kontexten verschafft, aber nichts an der Weiterleitung als Referenz ändert, sobald die Prozessgrenzen überschritten werden. Objekte, die den prozessübergreifenden Zugriff mit einer Kopie des Objekts durchführen wollen, statt mit einem Proxy, implementieren normalerweise IMarshal, um so die Semantik einer Weiterleitung als Wert zu erreichen (marshal by value).

    In der CLR erfolgt die Weiterleitung über AppDomain-Grenzen hinweg als Wert, bei freier Beweglichkeit innerhalb der AppDomains (Bild B1). Das bedeutet, dass ein Objekt normalerweise nie einen Proxy erhält. Statt dessen erfolgt der Zugriff in seiner Original-AppDomain direkt und in AppDomain-übergreifenden Zugriffen auf den geklonten Kopien des Objekts. Wie in Bild B2 gezeigt, sind Klassen, die System.MarshalByRefObject erweitern, frei in den Kontexten einer AppDomain beweglich, werden aber AppDomain-überschreitend als Referenzen weitergeleitet (das entspricht ungefähr der Aggregation des FTM in COM+). Objekte, die System.ContextBoundObject erweitern, liegen im Kontext fest, in dem sie initialisiert wurden (Bild B3). Das entspricht den normalen Umständen in COM+. Diese drei Konfigurationen lassen sich ohne explizite Codierung erreichen, nämlich einfach nur durch den Wechsel des Basistyps einer Klasse.

    Bild01

    B1 Ein CLR-Objekt.

    Bild02

    B2 Ein CLR-MarshalByRefObject

    Bild03

    B3 Ein CLR-ContextBoundObject

Ist COM nun mausetot?

Wie in diesem Artikel deutlich geworden sein sollte, bietet die CLR den Entwicklern, die heute noch mit COM arbeiten, beträchtliche Vorteile. Praktisch alle Aspekte des Programmiermodells von COM wurden überarbeitet (Schnittstellen, Klassen, Attribute, Kontexte und so weiter). Mancher mag COM schon deswegen als obsolet betrachten, weil CLR-Objekte nicht mehr auf IUnknown-konforme Sprungtabellen (vptrs/vtbls) angewiesen sind. Mir persönlich kommt es so vor, als würde die CLR einem Programmiermodell neues Leben einhauchen, mit dem ich die letzten sieben Jahre meines Arbeitslebens verbracht habe. Und ich weiß, dass es andere Programmierer gibt, die diese Einschätzung teilen.

Anzeigen:
© 2014 Microsoft