(0) exportieren Drucken
Alle erweitern
Erweitern Minimieren

Administration von Typen und Anwendungen unter .NET (Teil 1)

Veröffentlicht: 15. Dez 2001 | Aktualisiert: 15. Jun 2004
Von Jeffrey Richter

Mit .NET ist es einfach, verschiedenste Typen und Komponenten aus unterschiedlichen Programmiersprachen zusammenzufassen und zu installieren. Durch Baugruppen, Manifests und Konfigurationsdateien werden Anwendungen transparenter und einfacher zu handhaben.

* * *

Auf dieser Seite

Einsatzziele für das .NET Framework Einsatzziele für das .NET Framework
Verpackung eines Typs in ein Modul Verpackung eines Typs in ein Modul
Unterbringung der Typen in einer Baugruppe / Assembly Unterbringung der Typen in einer Baugruppe / Assembly
Versionsressourcen Versionsressourcen
Einfache Installation (private Baugruppen) Einfache Installation (private Baugruppen)
Einfache administrative Kontrolle (Konfiguration) Einfache administrative Kontrolle (Konfiguration)
Fazit Fazit

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

Bild01

Heutzutage werden neu entwickelte Anwendungen schnell so groß, dass die Entwickler Leistung hinzukaufen müssen. Meistens stammen die Objekttypen in der Anwendung also nicht mehr alle von derselben Mannschaft und schon gar nicht aus derselben Feder. Manche Objekte stammen von anderen Entwicklergruppen aus demselben Haus, manche aus dem Hause Microsoft und eine ganze Reihe wohl von anderen Anbietern.

Solange diese Typen nun mit einer Sprache entwickelt wurden, für die es .NET-fähige Compiler gibt, können die Typen reibungslos zusammenarbeiten. Das nahtlose Zusammenspiel auf der allen Sprachen gemeinsamen Laufzeitschicht (CLR) geht sogar soweit, dass ein Typ dem anderen als Basisklasse dienen kann, und zwar unabhängig von der Sprache, in der die Typen entwickelt werden oder wurden.

In diesem Artikel geht es darum, wie die Typen entwickelt und für den Einsatz in Dateien verpackt werden. Außerdem möchte ich kurz eine Art historischen Rückblick auf manche Probleme geben, die vom .NET Framework gelöst werden.

Einsatzziele für das .NET Framework

Im Laufe der Jahre wurde Windows immer wieder einmal als instabil und kompliziert angesehen. Von der branchenüblichen Übertreibung einmal abgesehen hat diese Einschätzung sicherlich die eine oder andere nachvollziehbare Ursache. So benutzen die Anwendungen praktisch immer dynamisch eingebundene Bibliotheken (DLLs) von Microsoft oder anderen Anbietern. Da eine Anwendung, wie anfangs schon festgestellt, meistens Code von mehreren verschiedenen Entwicklern enthält, kann der Entwickler des einen Codestückchens nie zu hundert Prozent sicher sein, wie andere wohl seinen Code benutzen. Im Prinzip können sich daraus zwar alle möglichen Probleme ergeben, aber in der Praxis spielen diese Probleme nur selten noch nach der Auslieferung der Anwendung an den Kunden eine Rolle, weil sie schon beim Programmtest auffallen und beseitigt werden.

T1 Modul-Metadaten

Metadaten-Definitionstabelle

Beschreibung

ModuleDef

Enthält immer einen Eintrag, der das Modul identifiziert. Der Eintrag umfasst den Dateinamen des Moduls mit Namensendung (ohne Pfad) sowie eine Modulversions-ID (in Form einer GUID, die der Compiler generiert hat). Daher bleibt der ursprüngliche Dateiname bekannt, auch wenn die Datei umbenannt wird.

TypeDef

Enthält für jeden Typ, der im Modul definiert wird, einen Eintrag. Jeder Eintrag nennt den Namen des Typs, den Basistyp und die Flags (public, private und so weiter). Außerdem verweist er auf die dazugehörigen Methoden, die in der MethodDef-Tabelle geführt werden, und auf die Felder (Datenelemente) des Typs, die in der Tabelle FieldDef verzeichnet sind.

MethodDef

Enthält für jede im Modul definierte Methode einen Eintrag. Jeder Eintrag verzeichnet den Namen der Methode, Flags (private, public, virtual, abstract, static, final und so weiter), die Signatur und den Offset für das Modul, in dem der entsprechende MSIL-Code zu finden ist (Microsoft Intermediate Language). Außerdem kann jeder Eintrag auf die ParamDef-Tabelle verweisen, in der weitere Informationen über die Parameter der Methode zu finden sind.

FieldDef

Enthält für jedes im Modul definierte Feld einen Eintrag. Solch ein Eintrag umfasst einen Namen, Flags (private, public und so weiter) und eine Typangabe.

ParamDef

Enthält für jeden im Modul definierten Parameter einen Eintrag. Zu solch einem Eintrag gehören ein Name und Flags (in, out, retval und so weiter).

PropertyDef

Enthält für jedes Property einen Eintrag, das im Modul definiert wird. Der Eintrag enthält einen Namen, Flags, eine Typangabe und das zugeordnete Feld (das übrigens auch null sein kann).

EventDef

Enthält für jedes im Modul definierte Ereignis einen Eintrag mit Namen und Flags.

Allerdings geraten die Anwender in Schwierigkeiten, wenn sich eine der beteiligten Firmen dazu entschließt, ihren Code zu aktualisieren und neue Dateien auszuliefern. Eigentlich sollen diese neuen Dateien zwar abwärtskompatibel zu den alten Versionen sein, aber wer kann das schon so genau sagen? Wenn einer der Anbieter seinen Code aktualisiert, ist es normalerweise nicht möglich, erneut alle bereits ausgelieferten Anwendungen zu testen und die neu auftretenden Fehler zu beseitigen.

Vermutlich haben Sie sich auch schon mit dem Problem herumgeschlagen, dass die Installation einer neuen Anwendung irgendwie eine bereits installierte Anwendung unbrauchbar macht. Diese Art der Instabilität versetzt den typischen Computeranwender in Angst und Schrecken. Er muss nun sorgfältig abwägen, ob er die heiß ersehnte neue Anwendung auf seiner Maschine installieren sollte. Wird es Ärger geben? Wird sich der Ärger beheben lassen? Ich persönlich habe mich schon lange dazu durchgerungen, auf meiner Arbeitsmaschine keine neuen Anwendungen "mal eben" auszuprobieren. Womöglich zerlegt sie mir meine Installation und ich kann nicht mehr arbeiten (Im Zweifelsfall richtet man sich am besten eine Testmaschine ein.).

Außerdem kann sich die Installation von Software unter Windows zu einer komplizierten Angelegenheit auswachsen. Wenn eine neue Anwendung installiert wird, wirkt sie sich meistens auf alle Teile des Systems aus. So werden zum Beispiel neue Dateien in verschiedene Verzeichnisse hineinkopiert, Einträge in der Registrierdatenbank aktualisiert und Verknüpfungen erstellt, sei es auf dem Desktop, im Startmenü oder irgendwo anders. Das Problem besteht nun darin, dass keine Anwendung eine Insel ist. Keine in sich abgeschlossene, von der Umgebung unabhängige Einheit, die keine anderen stört und auch nicht von anderen gestört werden kann. So kann man von einer installierten Anwendung zum Beispiel gar nicht ohne weiteres eine Sicherungskopie anfertigen, weil man praktisch gar nicht genau weiß, welche Dateien alle zur Anwendung gehören. Außerdem darf man die Einträge in der Registrierdatenbank nicht vergessen.

Genauso illusorisch ist die Idee, eine installierte Anwendung auf eine andere Maschine zu kopieren. Man muss sie wohl oder übel auf der Zielmaschine neu installieren, damit auch die Werte in der Registrierdatenbank korrekt gesetzt werden. Und schließlich kann man die Anwendung nicht mehr ohne weiteres entfernen, ohne entweder zuviel des Guten zu tun oder mit dem Gefühl zu leben, dass Teile der Anwendung noch auf der Maschine verblieben sind.

Der dritte Grund für diese Einschätzung von Windows hat mit der Sicherheit zu tun. Bei ihrer Installation bringen neue Anwendungen alle möglichen Dateiarten mit sich, von denen viele von verschiedenen Firmen geschrieben wurden. Außerdem schicken Webanwendungen oft Programmcode in einer Form durch den Draht, bei der sich der Benutzer gar nicht darüber im Klaren ist, dass auf seiner Maschine gerade wieder Programmcode installiert wird. Solcher Code kann heutzutage alle möglichen Dinge tun, auch Dateien löschen oder ungefragt E-Mails verschicken. Wegen des Schadens, der dabei entstehen kann, haben Anwender meiner Ansicht nach zu Recht gewisse Bedenken gegenüber der Installation neuer Anwendungen. Damit der Anwender bei einer solchen Aktion wieder auf sein System vertrauen kann, müssen die erforderlichen Sicherheitsmechanismen vom Betriebssystem bereitgestellt werden, und zwar so, dass der Anwender dem Code von bestimmten Herstellern explizit den Zugriff auf die Systemressourcen erlauben oder ihn sperren kann.

Verpackung eines Typs in ein Modul

In diesem Abschnitt möchte ich kurz untersuchen, wie Sie Ihre Quelldatei mit den verschiedenen Typen in eine Form bringen können, die sich vertreiben und installieren lässt. Lassen Sie uns mit einem Blick auf die folgende minimalistische Anwendung beginnen:

public class App { 
   static public void Main(System.String[] args) { 
      System.Console.WriteLine("Hi"); 
   } 
}


Diese einfache Anwendung definiert einen neuen Typ namens App, der sich mit einer einzigen statischen und öffentlichen Methode namens Main begnügt. In Main gibt es eine Referenz auf einen anderen Typ namens System.Console. Bei System.Console handelt es sich um einen Typ, der von Microsoft implementiert wurde. Der entsprechende MSIL-Code (Microsoft Intermediate Language) implementiert die Methoden dieses Typs in der Datei MSCorLib.dll. Unter dem Strich definiert dieses winzige Progrämmchen also einen Typ selbst und benutzt einen weiteren Typ, der von einer anderen Firma stammt.

Um diesem Beispiel eine lauffähige Form zu geben, bringen Sie die Codezeilen in einer Quelldatei unter, nennen diese zum Beispiel App.cs und geben dann auf der Kommandozeile folgenden Befehl:

csc.exe /out:App.exe /t:exe /r:MSCorLib.dll App.cs


Dieser Befehl weist den C#-Compiler mit dem Schalter /out:App.exe an, eine ausführbare Datei namens App.exe zu erzeugen. Bei der Art der zu generierenden Datei handelt es sich um eine Win32-Konsolenanwendung (/t[arget]:exe).

Bei der Bearbeitung dieses Quelltextes stellt der C#-Compiler fest, dass der Code die Methode WriteLine des Typs System.Console aufruft. An dieser Stelle möchte sich der Compiler davon überzeugen, dass es diesen Typ irgendwo gibt, dass er eine WriteLine-Methode hat und dass die Typen der Parameter von WriteLine zum Typ des Arguments passen, mit dem die Methode aufgerufen wird. Damit der C#-Compiler glücklich ist, muss ich ihm die erforderlichen Baugruppen angeben (Assemblies - dazu später mehr), damit er die Referenzen auf externe Typen auflösen und überprüfen kann. Auf der obigen Kommandozeile habe ich den Schalter /r[eference]:MSCorLib.dll angegeben. Damit sage ich dem Compiler, dass er in der Datei MSCorLib.dll nach externen Typen suchen und sie im Programm verfügbar machen soll.

Die MSCorLib.dll ist insofern eine spezielle Datei, als sie die ganzen Grundtypen enthält, wie Bytes, Integer, Zeichen, Strings und so weiter. Diese Typen werden so oft eingesetzt, dass der C#-Compiler von sich aus automatisch in dieser Baugruppe nachschaut. Mit anderen Worten, die folgende Zeile ist praktisch mit der obigen Zeile identisch, obwohl der /r-Schalter fehlt:

csc.exe /out:App.exe /t:exe App.cs


Falls Sie aus irgendeinem Grund verhindern möchten, dass der C#-Compiler die Baugruppe MSCorLib.dll benutzt, können Sie ihn mit dem Schalter /nostdlib daran hindern. So wird sich der Compiler nach dem Aufruf mit der folgenden Zeile mit einer Fehlermeldung beschweren, weil er App.cs nicht kompilieren kann:

csc.exe /out:App.exe /t:exe /nostdlib App.cs


Schauen wir uns nun die App.exe-Datei etwas genauer an, die der C#-Compiler generiert hat. Was genau stellt diese Datei dar? Nun, sie hat das übliche PE-Format für portable ausführbare Dateien (portable executable). Das bedeutet, dass man diese Datei auf einer Maschine, die 32-Bit- oder 64-Bit-Windows fährt, laden und einsetzen kann. Zumindest im Prinzip. Unter Windows gibt es zwei verschiedene Grundtypen von Anwendungen, nämlich die Konsolenanwendungen, deren Programmoberfläche im Wesentlichen textorientiert ist (CUI, console user interface), und die GUI-Anwendungen mit einer grafischen Schnittstelle zum Anwender. Da ich den Schalter /t:exe angegeben habe, generiert der C#-Compiler eine CUI-Anwendung. Der Schalter /t:winexe veranlasst ihn zur Erzeugung einer GUI-Anwendung.

Damit wäre geklärt, welche Art von PE-Datei ich hergestellt habe. Aber was genau ist eigentlich in dieser Datei drin? In einer "verwalteten PE-Datei" (managed PE file) gibt es drei wesentliche Komponenten, nämlich den CLR-Kopf, die Metadaten und den MSIL-Code. Beim CLR-Kopf handelt es sich um einen kleinen Datenblock, der für Module typisch ist, die auf die CLR angewiesen sind (managed modules). Im Kopf wird die CLR-Version, für die das Modul erstellt wurde, mit Haupt- und Nebennummer angegeben. Außerdem enthält er einige Flags und ein MethodDef-Token (darauf komme ich später noch zurück), aus dem die Eintrittspunktmethode des Moduls hervorgeht, sofern es sich um eine ausführbare CUI- oder GUI-Datei handelt, und eine digitale Signatur mit einem starken Namen (auch dazu später mehr). Und schließlich gibt der Kopf noch Größe und Offset von bestimmten Metadatentabellen an, die ebenfalls im Modul enthalten sind.

Bei den Metadaten handelt es sich um einen Block mit Binärdaten, der sich aus mehreren Tabellen zusammensetzt. Tabelle T1 beschreibt einige der gebräuchlicheren Tabellen, die im Metadatenblock zu finden sind.

Die Metadatentabellen aus Tabelle T1 beschreiben Dinge, die im Modul implementiert werden. Es gibt auch noch Metadaten-Referenztabellen, in denen die Dinge verzeichnet sind, die zwar in diesem Modul benutzt werden, aber in anderen Baugruppen liegen. Tabelle T2 zeigt einige von den gebräuchlicheren Referenztabellen.

T2 Die Metadaten für Referenzen auf andere Baugruppen

Metadaten-Referenztabelle

Beschreibung

AssemblyRef

Enthält für jede Baugruppe, die vom Modul benutzt wird, einen Eintrag. Ein Eintrag enthält die Informationen, die zur Einbindung der Baugruppe erforderlich sind: den Namen der Baugruppe (ohne Pfad und Endung), die Versionsnummer, Kulturkreis (culture) und ein öffentliches Schlüsselzeichen (normalerweise ein kurzer Hash-Wert, der den öffentlichen Schlüssel der betreffenden Baugruppe identifiziert). Außerdem enthält ein Eintrag noch einige Flags und einen Hash-Wert.

ModuleRef

Enthält für jedes PE-Modul einen Eintrag, das Typen implementiert, die von diesem Modul benutzt werden. Ein Eintrag besteht aus dem Dateinamen des Moduls samt Namensendung (ohne Pfad). Diese Tabelle dient zur Einbindung von Typen, die in anderen Modulen aus der aufrufenden Baugruppe implementiert werden.

TypeRef

Enthält für jeden Typ, der vom Modul verwendet wird, einen Eintrag. Der Eintrag umfasst den Namen des Typs und eine Referenz auf den Ort, an dem der Typ zu finden ist. Wird der Typ in einer anderen Baugruppe implementiert, so verweist der Eintrag auf einen AssemblyRef-Eintrag. Wird der Typ dagegen in einem Modul aus der aufrufenden Baugruppe implementiert, so verweist der Eintrag auf einen ModuleRef-Eintrag.

MemberRef

Enthält für jedes Feld, jede Methode, jedes Property und jede Ereignismethode, die im Modul verwendet wird, einen Eintrag. Ein Eintrag umfasst den Namen und die Signatur und verweist auf den TypeRef-Eintrag des Typs, in dem das betreffende Feld oder die Methode implementiert wird.

Es gibt noch viel mehr Tabellen, als ich in den Tabellen T1 und T2 gezeigt habe. Ich wollte Ihnen damit nur einen Eindruck davon vermitteln, welche Art von Information der Compiler in den Metadaten unterbringt.

Es gibt eine Reihe von Werkzeugen, mit denen Sie sich die Metadaten in einer verwalteten PE-Datei ansehen können. Mein persönlicher Favorit ist der ILDasm.exe, der MSIL-Disassembler. Wenn Sie sich die Metadatentabellen von App.exe anschauen möchten, geben Sie auf der Kommandozeile folgenden Befehl:

ILDasm /Adv App.exe


Diese Zeile startet den ILDasm.exe und veranlasst ihn, die Baugruppe App.exe zu laden. Der Schalter /Adv weist den ILDasm an, einige zusätzliche Menüpunkte verfügbar zu machen (zu finden im View-Menü). Wenn Sie die Metadaten in einer halbwegs menschenfreundlichen Form sehen möchten, wählen Sie den Menüpunkt View.MetaInfo.Show! Dann gibt der ILDasm die Informationen in einer Form aus, wie in Listing L1 gezeigt.

L1 Anzeige der Metadaten in lesbarer Form

Version of runtime against which the binary is built : 2000.14.2007.0 
ScopeName : assmain.exe 
MVID      : {82B5C4CB-7A48-4D64-A96F-E77C9AC60A82} 
=========================================================== 
Global functions 
---------------------------- 
Global fields 
---------------------------- 
Global MemberRefs 
---------------------------- 
TypeDef #1 
---------------------------- 
    TypDefName: App  (02000002) 
    Flags     : [Public] [AutoLayout] [Class] [AnsiClass]  (00000001) 
    Version   : 0:0:0:0 
    Extends   : 01000001 [TypeRef] System.Object 
    Method #1 [ENTRYPOINT] 
---------------------------- 
        MethodName: Main (06000001) 
        Flags     : [Public] [Static] [HideBySig] [ReuseSlot]  
                    (00000096) 
        RVA       : 0x00001050 
        ImplFlags : [IL] [Managed]  (00000000) 
        CallCnvntn: [DEFAULT] 
        ReturnType: Void 
        1 Arguments 
            Argument #1:  SZArray String 
        1 Parameters 
            (1) MethodToken : (06000001) Name : args flags:  
                              [none] (00000000) default:  
    Method #2  
---------------------------- 
        MethodName: .ctor (06000002) 
        Flags     : [Public] [HideBySig] [ReuseSlot] [SpecialName]  
                             [RTSpecialName] [.ctor]  (00001886) 
        RVA       : 0x0000105c 
        ImplFlags : [IL] [Managed]  (00000000) 
        CallCnvntn: [DEFAULT] 
        hasThis  
        ReturnType: Void 
        No arguments. 
TypeRef #1 (01000001) 
---------------------------- 
Token:             0x01000001 
ResolutionScope:   0x23000001 
TypeRefName:       System.Object 
    MemberRef #1 
---------------------------- 
        Member: (0a000003) .ctor:  
        CallCnvntn: [DEFAULT] 
        hasThis  
        ReturnType: Void 
        No arguments. 
TypeRef #2 (01000002) 
---------------------------- 
Token:             0x01000002 
ResolutionScope:   0x23000001 
TypeRefName:       System.Diagnostics.DebuggableAttribute 
    MemberRef #1 
---------------------------- 
        Member: (0a000001) .ctor:  
        CallCnvntn: [DEFAULT] 
        hasThis  
        ReturnType: Void 
        2 Arguments 
            Argument #1:  Boolean 
            Argument #2:  Boolean 
TypeRef #3 (01000003) 
---------------------------- 
Token:             0x01000003 
ResolutionScope:   0x23000001 
TypeRefName:       System.Console 
    MemberRef #1 
---------------------------- 
        Member: (0a000002) WriteLine:  
        CallCnvntn: [DEFAULT] 
        ReturnType: Void 
        1 Arguments 
            Argument #1:  String 
Assembly 
---------------------------- 
    Token: 0x20000001 
    Name : assmain 
    Originator Blob : <null> 
    Hash Algorithm : 0x00008004 
    Major Version: 0x00000000 
    Minor Version: 0x00000000 
    Revision Number: 0x00000000 
    Build Number: 0x00000000 
    Locale: <null> 
    Configuration: <null> 
    Title :  
    Description :  
    Flags : [SideBySideCompatible]  (00000000) 
    CustomAttribute #1 (0c000001) 
---------------------------- 
        CustomAttribute Type: 0a000001 
        CustomAttributeName: System.Diagnostics.DebuggableAttribute:: 
                                    instance void .ctor(bool,bool) 
        Value Blob length : 6 
        Value     : 01 00 00 01 00 00  
        ctor args: () 
AssemblyRef #1 
---------------------------- 
    Token: 0x23000001 
    Originator Blob: 03 68 91 16 d3 a4 ae 33  
    Name: mscorlib 
    Major Version: 0x000007d0 
    Minor Version: 0x0000000e 
    Revision Number: 0x000007d7 
    Build Number: 0x00000000 
    Locale: <null> 
    Configuration: <null> 
    HashValue Blob: 04 9c cc 43 f0 57 2d 68 35 29 43 62 10 69 75 89  
                    9b 34 42 e2  
    ExecutionLocation token: 0x29000000 
    Flags: [none] (00000000) 
User Strings 
---------------------------- 
70000001 : ( 2) L"Hi"

Zum Glück wertet der ILDasm die Metadatentabellen auch schon teilweise aus und kombiniert die Informationen an den passenden Stellen, so dass Sie die rohen Tabellendaten nicht selbst zu analysieren brauchen. Aus Listing L1 geht zum Beispiel hervor, dass der ILDasm bei der Anzeige eines TypeDef-Eintrags auch die entsprechenden Informationen über die Methoden angibt, bevor der erste TypeRef-Eintrag angezeigt wird.

Aber keine Angst, man braucht wirklich nicht alles zu verstehen, was der ILDasm an Informationen auswirft. Wichtig ist im Moment nur, dass die App.exe eine TypeDef enthält, deren Namen App lautet. Diese Typdefinition nennt eine öffentlich zugängliche Klasse, die sich von System.Object ableitet (das ist ein Typ aus einer anderen Baugruppe). Außerdem definiert der Typ App zwei Methoden, nämlich Main und .ctor (das ist der Konstruktor).

Main ist eine statische öffentliche Methode, deren Code als MSIL vorliegt (im Gegensatz zum Binärcode der entsprechenden CPU, zum Beispiel x86). Main hat den Rückgabetyp void und einen einzigen Parameter namens args, bei dem es sich um ein Stringarray handelt. Die Konstruktormethode, die immer mit dem Namen ctor angezeigt wird, ist öffentlich und liegt ebenfalls als MSIL-Code vor. Der Konstruktor hat den Rückgabetyp void und keine Parameter. Aber er hat einen this-Zeiger, der beim Aufruf der Methode auf den Speicherblock verweist, in dem das Objekt angelegt werden soll.

Ich kann ihnen nur dringend empfehlen, mit dem ILDasm zu experimentieren. Er zeigt erstaunlich viele Informationen an. Je mehr Sie darüber wissen, was Sie da eigentlich sehen, desto besser verstehen Sie die CLR.

Schauen wir uns spaßeshalber noch einige Angaben über die Baugruppe App.exe an. Wenn Sie den Menüpunkt View.Statistics wählen, zeigt der ILDasm einige weitere Daten an (Listing L2).

L2 Der Menüpunkt View.Statistics gibt über die Baugruppe Auskunft

File size            : 2560 
 PE header size       : 512 (456 used)    (20.00%) 
 PE additional info   : 883               (34.49%) 
 Num.of PE sections   : 2 
 COM+ header size     : 72                ( 2.81%) 
 COM+ meta-data size  : 620               (24.22%) 
 COM+ additional info : 0                 ( 0.00%) 
 COM+ method headers  : 2                 ( 0.08%) 
 Managed code         : 18                ( 0.70%) 
 Data                 : 1024              (40.00%) 
 Unaccounted          : -571              (-22.30%) 
 Num.of PE sections   : 2 
   .text    - 1024 
   .rsrc    - 1024 
 COM+ meta-data size  : 620 
   Module        -    1 (6 bytes) 
   TypeDef       -    2 (48 bytes)    0 interfaces, 0 explicit layout 
   TypeRef       -    3 (18 bytes) 
   MethodDef     -    2 (28 bytes)    0 abstract, 0 native, 2 bodies 
   MemberRef     -    3 (18 bytes) 
   ParamDef      -    1 (6 bytes) 
   CustomValue   -    1 (6 bytes) 
   Assembly      -    1 (30 bytes) 
   AssemblyRef   -    1 (24 bytes) 
   Strings       -   200 bytes 
   Blobs         -    60 bytes 
   UserStrings   -     8 bytes 
   Guids         -    16 bytes 
   Uncategorized -   152 bytes 
 COM+ method headers : 2 
   Num.of method bodies  - 2 
   Num.of fat headers    - 0 
   Num.of tiny headers   - 2 
 Managed code : 18 
   Ave method size - 9

Sie sehen die Dateigröße (in Bytes) und die Größenangaben ihrer verschiedenen Bestandteile (ebenfalls in Bytes und prozentual). In einer ausgesprochen winzigen Anwendung wie App.cs machen der PE-Kopf und die Metadaten den Hauptteil der Datei aus. Tatsächlich gibt sich der MSIL-Code mit gerade einmal 18 Bytes zufrieden. Sobald die Anwendung wächst, wird sie natürlich die meisten Typen und Referenzen auf andere Typen und Baugruppen öfters verwenden, so dass die Metadaten- und Kopfinformationen im Vergleich zur Gesamtgröße der Datei weniger ins Gewicht fallen.

Unterbringung der Typen in einer Baugruppe / Assembly

Das kleine App.exe-Programm, von dem im vorigen Abschnitt die Rede war, ist nicht nur eine PE-Datei mit Metadaten. Es ist zudem eine "Baugruppe" (Assembly). Auf der .NET-Plattform ist solch eine Baugruppe die Einheit für die Wiederverwendung, Versionsfortschreibung, Sicherheit und für die Installation. Um die eigenen Typen also verpacken und einsetzen zu können, müssen sie in Module untergebracht werden, die zu einer Baugruppe gehören. In vielen Fällen wird die Baugruppe nur aus einer einzigen Datei bestehen, wie beim Programm App.exe. Allerdings kann sich eine Baugruppe auch aus mehreren Dateien zusammensetzen, zum Beispiel aus einigen PE-Dateien mit Metadaten und aus einigen Ressourcedateien wie .gif- oder .jpg-Dateien. Die Entscheidung liegt bei Ihnen. Vielleicht vereinfacht es das Verständnis, wenn man sich eine Baugruppe einfach als eine Art logischer EXE oder DLL vorstellt.

Ich kann es praktisch schon hören, wie sich viele von Ihnen fragen, liebe Leser, warum Microsoft das Konzept der Baugruppen eingeführt hat. Der Grund liegt darin, dass eine Baugruppe die Entkopplung der logischen Bedeutung und der realen Unterbringung der Typen ermöglicht. Eine Baugruppe kann zum Beispiel aus mehreren Typen bestehen. Nun können Sie die häufiger benutzten Typen ein einer Datei zusammenfassen und die weniger häufig benutzten Typen in einer anderen Datei unterbringen. Wenn Ihre Baugruppe zum Beispiel für den Einsatz aus dem Internet heruntergeladen wird, kann es durchaus geschehen, dass die weniger gebräuchlichen Typen in der zweiten Datei gar nicht heruntergeladen werden, falls der Client diese Typen gar nicht benutzt. Ein Hersteller zum Beispiel, der sich auf UI-Steuerelemente spezialisiert, könnte die Entscheidung treffen, Active Accessibility-Typen in einem separaten Modul unterzubringen (damit der das Logo von Microsoft erhält). Nur solche Anwender, die auf diese zusätzlichen Leistungen angewiesen sind, würden das Modul auch herunterladen müssen. Die anderen Anwender brauchen es nicht und laden es auch nicht herunter. Unter dem Strich ist die Baugruppe (Assembly) die Einheit, in der man die betreffende Software einsetzt und neue Versionen entwickelt, während die Datei oder das Modul die Einheit ist, in der man die Software herunterlädt oder vertreibt.

Aus der Sicht des Verbrauchers (also bei externer Betrachtung) ist eine Baugruppe eine Sammlung von exportierten Typen und Ressourcen, die einen bestimmten Namen und eine Versionsnummer hat. Aus der Sicht des Entwicklers der Baugruppe (bei interner Betrachtung) ist eine Baugruppe eine Sammlung aus einer oder mehreren Dateien - von einer einzelnen PE-Datei bis hin zu einer Sammlung von PE-Dateien, Ressourcedateien, HTML-Seiten, GIFs und so weiter - die Typen und Ressourcen implementiert.

Zur Erstellung einer Baugruppe müssen Sie eine PE-Datei als Träger des Manifests bestimmen. Als Alternative könnten Sie auch eine separate PE-Datei anlegen, die nichts außer dem Manifest enthält. Das "Manifest" besteht aus einer Reihe von Tabellen, in denen die Identität, der vorgesehene Kulturkreis (culture), die Dateien und die öffentlichen exportieren Typen der Baugruppe beschrieben werden, und natürlich alle Dateien, aus denen sich die Baugruppe zusammensetzt.

Außerdem geht aus dem Manifest noch hervor, von welchen anderen Baugruppen diese Baugruppe abhängig ist. Die Auswahl einer PE-Datei als Träger des Manifests bedeutet also, dass die Metadaten dieser PE-Datei einige zusätzliche Tabellen aufnehmen müssen. Tabelle T3 beschreibt einige der Metadatentabellen, die zum Manifest gehören.

T3 Die Manifesttabellen in Metadaten

Manifest-Tabelle

Beschreibung

AssemblyDef

Enthält einen einzelnen Eintrag, sofern dieses Modul eine Baugruppe (Assembly) beschreibt. Der Eintrag umfasst den Namen der Baugruppe (ohne Pfad und Endung), Kulturkreis (culture), Version (major, minor, build und revision), Flags, den Hash-Algorithmus und den öffentlichen Schlüssel des Herausgebers.

FileDef

Enthält Einträge für jede PE und Quelltextdatei, die zur Baugruppe gehört. Der Eintrag umfasst den Namen der Datei mit Namensendung (ohne Pfad), einen Hash-Wert und Flags. Falls die Baugruppe nur aus ihrer eigenen Datei besteht, hat diese Tabelle keine Einträge.

ManifestResourceDef

Enthält für jede Ressource einen Eintrag, die zur Baugruppe gehört. Der Eintrag umfasst den Namen der Ressource, Flags (public, private) und einen Index für die FileDef-Tabelle. In der FileDef-Tabelle wird die Datei beschrieben, in welcher die Ressource-Datei oder der -Stream liegt. Sofern es sich bei der Ressource nicht um eine eigenständige Datei handelt (wie JPEG oder GIF), liegt die Ressource als Stream in einer PE-Datei vor. Bei einer eingebetteten Ressource nennt der Eintrag auch den Offset, bei dem der Ressource-Datenstrom in der PE-Datei beginnt.

ExportedTypesDef

Enthält für jeden öffentlichen Typ, der von den PE-Modulen der Baugruppe exportiert wird, jeweils einen Eintrag. Darin wird der Name des Typs angegeben, ein Index für die FileDef-Tabelle (er zeigt die Datei, in welcher der Typ implementiert wird) und ein Index für die TypeDef-Tabelle. Zur Platzersparnis werden die Typen, die von der Manifestdatei exportiert werden, nicht in dieser Tabelle wiederholt, da die Typinformationen bereits in der TypeDef-Tabelle verfügbar sind.

AssemblyProcessorDef

Enthält für jede CPU einen Eintrag, die von den Modulen der Baugruppe unterstützt wird. Solch ein Eintrag besteht aus einer Prozessorkennung wie PROCESSOR_INTEL_PENTIUM, PROCESSOR_INTEL_IA64 und so weiter (wie in der WinNT.h definiert). Normalerweise ist diese Tabelle leer. Ihre Angaben werden von der Laufzeitschicht ignoriert. Allerdings könnten die Daten interessant werden, weil manche Module Binärcode enthalten können.

AssemblyOSDef

Enthält einen Eintrag, der das Betriebssystem nennt, für das die Module aus der Baugruppe vorgesehen sind. In jedem Eintrag gibt es eine Plattform-ID (wie VER_PLATFORM_WIN32_WINDOWS, VER_PLATFORM_WIN32_NT oder VER_PLATFORM_WIN32_CE) und die Haupt- und Nebennummern der Betriebssystemversion. Normalerweise ist diese Tabelle leer. Ihre Angaben werden von der Laufzeitschicht ignoriert. Allerdings könnten ihre Informationen interessant werden, wenn manche Typen oder Methoden nur auf bestimmten Betriebssystemen verfügbar sind.

Durch die Existenz eines Manifests dieser Art beschreiben sich die Baugruppen praktisch von selbst. Durch das Manifest weiß die Datei , welche anderen Dateien noch zur Baugruppe gehören. Diese Dateien wiederum wissen nicht, dass sie zu einer Baugruppe gehören.

Der C#-Compiler erzeugt eine Baugruppe, wenn Sie einen der folgenden drei Kommandozeilenschalter angeben: /t[arget]:exe, /t[arget]:winexe oder /t[arget]:library. Jeder dieser drei Schalter veranlasst den Compiler, eine PE-Datei zu generieren und das Manifest in dieser Datei unterzubringen. Bei der resultierenden Datei handelt es sich entweder um eine ausführbare CUI-Datei, eine ausführbare GUI-Datei oder um eine DLL.

Neben diesen Schaltern versteht der C#-Compiler auch den Schalter /t[arget]:module. Dieser Schalter weist den Compiler an, eine PE-Datei zu erzeugen, die keine Manifesttabellen enthält. Bei der generierten PE-Datei handelt es sich immer um eine DLL. Diese Datei muss in eine Baugruppe aufgenommen werden, damit die in ihr untergebrachten Typen verfügbar sind.

Im .NET führen viele Wege in eine Baugruppe. Zumindest für ein Modul. Wenn Sie den C#-Compiler benutzen, um die PE-Datei mit dem Manifest zu generieren, dann können Sie den Schalter /addmodule angeben. Nehmen wir an, Sie hätten zwei Quelltextdateien, nämlich RUT.cs mit selten benutzten Typen und FUT.cs mit den häufig benutzen Typen.

Da der RUT-Inhalt nur selten benutzt wird, bringt man ihn am besten in einem eigenen Modul unter. Dann brauchen die Benutzer der Baugruppe gar nicht auf dieses Modul zuzugreifen, wenn sie die darin enthaltenen Typen nicht benutzen.

csc /out:RUT.mod /t:module RUT.cs


Beachten Sie bitte, dass die RUT-Moduldatei eine Standard-DLL im PE-Format ist. Ich gebe ihr nur die Namensendung .mod, um damit anzudeuten, dass es sich um ein Modul handelt, das in eine Baugruppe aufgenommen werden muss, damit ihr Inhalt zugänglich wird.

Nun möchte ich die häufig benutzten Typen kompilieren und in einem separaten Modul unterbringen. Da diese Typen häufig benutzt werden, erhält dieses Modul auch noch das Manifest der Baugruppe. Und weil dieses Modul nun die gesamte Baugruppe repräsentiert, bin ich mit dem automatisch abgeleiteten Namen FUT.dll natürlich nicht zufrieden, sondern ändere den Dateinamen auf JeffTypes.dll ab:

csc /out:JeffTypes.dll /t:library /addmodule:RUT.mod FUT.cs


Die obige Zeile weist den C#-Compiler an, die Datei FUT.cs zu kompilieren und daraus die Datei JeffTypes.dll zu erzeugen. Da der Schalter /t:library angegeben wurde, sorgt der Compiler zudem dafür, dass die Manifesttabellen in die resultierende DLL eingebaut werden. Außerdem sorgt er wegen des Schalters /addmodule:RUT.mod dafür, dass die Datei RUT.mod als Bestandteil der Baugruppe betrachtet wird. Genauer gesagt, der /addmodule-Schalter weist den Compiler an, die Datei in die FileDef-Tabelle ein zutragen und die von RUT.mod exportierten öffentlichen Typen in die Tabelle ExportedTypesDef einzutragen. Sobald der Compiler mit seiner Arbeit fertig ist, haben Sie eine Baugruppe mit zwei lauffähigen Dateien (Bild B1).

Bild02

B1 Das RUT-Modul und JeffTypes.dll bilden eine Baugruppe

Das Modul RUT.mod enthält den MSIL-Code, der durch die Kompilierung von RUT.cs entstand. Außerdem enthält das Modul Metadatentabellen, in denen die in RUT.cs definierten Typen, Methoden, Felder, Properties, Ereignisse und so weiter beschrieben werden. Außerdem beschreiben diese Metadatentabellen die Typen, Methoden und so weiter, die von RUT.cs benutzt werden. Die JeffTypes.dll ist eine separate Datei. Ähnlich wie RUT.mod enthält diese Datei den MSIL-Code, der beim Kompilieren von FUT.cs entstand, sowie vergleichbare Definitions- und Referenztabellen als Metadaten. Allerdings enthält die JeffTypes.dll zusätzlich die Metadatentabellen, die das Manifest ausmachen. Deswegen ist JeffTypes.dll eine Baugruppe. Die zusätzlichen Manifesttabellen beschreiben die Dateien, aus denen sich die Baugruppe zusammensetzt (die Datei JeffTypes.dll selbst und die Datei RUT.mod). Außerdem erfassen die Manifesttabellen auch alle öffentlichen Typen, die von JeffTypes.dll und RUT.mod exportiert werden.

In der Praxis enthalten die Manifesttabellen aber nicht die Typen, die von der PE-Datei exportiert werden, in welcher das Manifest liegt. Diese kleine Optimierung hat den Zweck, den Platzbedarf des Manifests zu verringern.

Es ist also eigentlich nicht ganz korrekt, wenn man sagt, dass die Manifesttabellen alle öffentlichen Typen verzeichnen, die von JeffTypes.dll und RUT.mod exportiert werden. Aber das ist letztlich nur ein unwesentliches Detail.

Sobald die Baugruppe JeffTypes.dll fertig ist, können Sie sich die Manifesttabellen im ILDasm.exe anschauen und sich davon überzeugen, dass es in dieser Baugruppendatei tatsächlich Referenzen auf die Typen aus der Datei RUT.mod gibt. Damit das nicht nur leeres Gerede bleiben muss, gibt es im Beispielcode für diesen Artikel ein Projekt, dass aus den Dateien RUT.cs und FUT.cs die Baugruppe JeffTypes.dll zusammenbaut. Wenn Sie diese Projekt kompilieren und die Metadaten mit dem ILDasm untersuchen, werden Sie auch auf die Tabellen FileDef und ExportedTypesDef stoßen. Aus Listing L3 geht hervor, wie solche Tabellen aussehen.

L3 Die Ausgaben vom ILDasm

File #1 
---------------------------- 
    Token: 0x26000001 
    Name : RUT.mod 
    HashValue Blob : bb 11 9e 19 82 8a a5 d1 3e 40 16 9c c9 8a d9 db  
                     4d 4e 9f e6  
    Flags : [none] (00000000) 
ComType #1 
---------------------------- 
    Token: 0x27000001 
    Name: ARarelyUsedType 
    Description token:  
    Implementation token: 0x26000001 
    TypeDef token: 0x02000002 
    ExecutionLocation token: 0x29000000 
    Flags     : [Public] [AutoLayout] [Class] [AnsiClass]  (00000001)


Aus diesen Daten lässt sich ablesen, dass RUT.mod eine Datei ist, die als Bestandteil der Baugruppe angesehen wird. Aus der ComType-Tabelle (sie wurde für die Beta 2 umbenannt) kann man erkennen, dass es einen öffentlichen exportierten Typ gibt, nämlich ARarelyUsedType. Das Implementierungssymbol für diesen Typ ist 0x26000001. Das bedeutet, dass der MSIL-Code für den Typ in der Datei RUT.mod liegt.

Falls Sie neugierig geworden sind: Solche Symbole oder "Token" sind Vier-Byte-Werte. Das höchstwertige Byte gibt die Art des Symbols an (0x01=TypeRef, 0x02=TypeDef, 0x26=FileRef, 0x27=ExportedType). Eine vollständige Liste finden Sie übrigens in der Datei CorHdr.h aus dem .NET Framework-SDK, und zwar in Form des Aufzählungstyps CorTokenType. Die unteren drei Bytes des Symbols geben einfach nur die entsprechende Zeile in der dazugehörigen Metadatentabelle an. Das Implementierungssymbol 0x26000001 bezieht sich also auf die zweite Zeile der Tabelle FileDef.

Wenn sie nun einen Client entwickeln, der als Verbraucher der Typen aus der Baugruppe JeffTypes.dll auftritt, müssen Sie diesen Client mit dem Compilerschalter /r[eference]:JeffTypes.dll kompilieren. Damit der Client später lauffähig ist, muss die allen Sprachen gemeinsame Laufzeitschicht auf die JeffTypes.dll zugreifen können. Die RUT.mod wird dagegen nur dann gebraucht, wenn der Client auch solche Typen benutzt, die in der Datei RUT.mod definiert werden.

Statt mit dem C#-Compiler können Sie die Baugruppen auch mit dem Baugruppenlinker AL.exe zusammenbauen. AL.exe ist dann sehr nützlich, wenn Sie eine Baugruppe aus Modulen zusammenstellen, die mit anderen Compilern erzeugt wurden (falls Ihr Compiler nicht das Äquivalent des C#-Compilerschalters /addmodule anbietet), oder wenn Sie beim Kompilieren der Module einfach noch nicht wissen, woraus sich die Baugruppe im Einzelnen zusammensetzt. Mit AL.exe können Sie auch Baugruppen zusammenstellen, die nur Ressourcen enthalten (satellite assemblies). Solche "Satelliten" verwendet man zum Beispiel bei der Lokalisation.

Das Programm AL.exe kann EXE oder DLL-Dateien im PE-Format erzeugen, die nichts außer dem Manifest enthalten, in dem die Typen aus den anderen Modulen beschrieben werden. Wenn Sie sich das in der Praxis anschauen möchten, ändern Sie einfach den Weg etwas ab, auf dem die Baugruppe JeffTypes.dll zusammengestellt wird:

csc /out:RUT.mod /t:module RUT.cs 
csc /out:FUT.mod /t:module FUT.cs 
al  /out:JeffTypes.dll /t:lib FUT.mod RUT.mod


Bild B2 zeigt die Dateien, die durch diese drei Befehle entstehen. Laut Bild B2 habe ich diesmal zwei separate Module angelegt, nämlich die Dateien RUT.mod und FUT.mod. Keines dieser Module ist eine Baugruppe. Anschließend habe ich eine dritte Datei namens JeffTypes.dll produziert. Es handelt sich um eine kleine DLL-Datei im PE-Format (wegen des Schalters /t[ype]:lib), die zwar keinen MSIL-Code enthält, wohl aber die Metadatentabellen des Manifests. Aus diesen Tabellen geht hervor, dass auch RUT.mod und FUT.mod zur Baugruppe gehören.

Bild03

B2 Die Module RUT und FUT und die JeffTypes.dll

Der Baugruppenlinker kann auch CUI- und GUI-Dateien im PE-Format erzeugen (mit den Kommandozeilenschaltern /t[ype]:exe oder /t[ype]:win). Das ist aber eher ungewöhnlich, weil es bedeutet, dass die EXE-Datei gerade genug MSIL-Code enthält, um eine Methode aus einem anderen Modul aufrufen zu können. Dieser MSIL-Code wird von AL.exe generiert, wenn man ihn mit dem Schalter /main aufruft.

   csc /out:App.mod /t:module /r:JeffTypes.dll App.cs 
   al  /out:App.exe /t:exe /main:App.Main app.mod


Die erste dieser beiden Zeilen sorgt für die Kompilierung der Datei App.cs zu einem Modul. Die zweite Zeile produziert dann eine kleine PE-Datei namens App.exe, die das Manifest enthält. Außerdem baut AL.exe wegen des Schalters /main:App.Main eine kleine globale Funktion ein. Diese Funktion wird __EntryPoint genannt und enthält folgenden MSIL-Code:

.method privatescope static void __EntryPoint() il managed 
{ 
  .entrypoint 
  // Codegröße       8 (0x8) 
  .maxstack  8 
  IL_0000:  tail. 
  IL_0002:  call       void [.module 'App.mod']App::Main() 
  IL_0007:  ret 
} // Ende der Methode 'Global  
  // Functions::__EntryPoint'

Wie Sie sehen, ruft dieser Code einfach die Main-Methode des Typs App auf, der in der Datei App.mod zu finden ist.

Wie Sie ebenfalls sehen, ist der Schalter /main von AL.exe nicht übermäßig nützlich, weil es einfach sehr unwahrscheinlich ist, dass Sie jemals eine Baugruppe für eine Anwendung zusammenstellen werden, deren Eintrittspunkt nicht in der PE-Datei mit den Manifesttabellen liegt. Ich habe den Schalter nur der Vollständigkeit halber erwähnt.

Wenn Sie mit dem AL.exe eine Baugruppe zusammenstellen, können Sie mit dem Schalter /embed[resource] auch Ressourcedateien (nicht-PE-Dateien) in die Baugruppe aufnehmen. Dieser Schalter sorgt dafür, dass die angegebene Datei mit den Ressourcedaten in die resultierende PE-Datei übernommen wird. Natürlich werden auch die Manifesttabellen entsprechend aktualisiert und weisen auf die Existenz der Ressourcen hin. Außerdem bietet AL.exe den Schalter /link[resource] an, mit dem sich ebenfalls eine Ressourcendatei angeben lässt. Allerdings bewirkt /link[resource] nur eine entsprechende Aktualisierung der Manifesttabellen, die nun auf die zusätzliche Ressourcendatei hinweisen. Die Ressourcendatei wird nicht in die PE-Datei der Baugruppe aufgenommen, sondern verbleibt als separate Datei und muss daher zusammen mit den anderen Dateien der Baugruppe mitgeliefert werden.

Wie AL.exe ermöglicht auch CSC.exe die Einbindung von Ressourcen in eine Baugruppe, die mit dem C#-Compiler erstellt wird. Der Schalter /res[ource] des C#-Compilers sorgt für die Aufnahme der Ressourcedatei in die resultierende PE-Datei, während der Compilerschalter /linkres[ource] für den Eintrag einer eigenständigen Ressourcedatei in die Manifesttabellen sorgt.

Eine letzte Anmerkung zum Thema Ressourcen: Es ist möglich, Ressourcen vom Standard-Win32 in eine Baugruppe aufzunehmen. Sie können das tun, indem Sie den Pfadnamen der .res-Datei mit dem Schalter /win32res angeben, wenn Sie mit AL.exe oder CSC.exe arbeiten. Außerdem gibt es noch eine schnelle und einfache Methode zur Aufnahme einer Win32-Icon-Ressource in eine Baugruppendatei: geben Sie den Pfadnamen der .ico-Datei mit dem Schalter /win32icon an (AL.exe und CSC.exe).

Versionsressourcen

Wenn der AL.exe oder der CSC.exe eine Baugruppendatei erzeugen, bauen sie auch eine Versionsressource in die Datei ein, wie man sie von Win32 her kennt. Die Anwender können sich die Ressource im Eigenschaftsdialog der Datei anschauen (Bild B3). Außerdem können Sie die Werte der Versionsressource im Ressourceneditor vom Visual Studio.NET anzeigen und ändern (Bild B4).

Bild04

B3 Die Versionsangaben erscheinen im Eigenschaftsdialog der Datei.

Bild05

B4 Die Versionsangaben im Ressourceneditor.

Bei der Zusammenstellung einer Baugruppe setzen Sie die Ressourcenfelder mit Hilfe von anwenderdefinierten Attributen, die Sie im Code angeben. Listing L4 zeigt als Beispiel den Code, der für die Festlegung der Versionsinformationen aus Bild B4 erforderlich ist.

L4 Generiere die Versionsinformationen

using System.Reflection; 
// Lege die Werte für die Felder CompanyName, LegalCopyright und 
// LegalTrademarks fest. 
[assembly:AssemblyCompany("The Jeffrey Richter Company")] 
[assembly:AssemblyCopyright("Copyright (c) 2000 Jeffrey Richter")] 
[assembly:AssemblyTrademark("FUT & RUT are a registered 
trademark of the Jeffrey Richter Company")] 
// Lege die Werte für die Felder ProductName und 
ProductVersion fest. 
[assembly:AssemblyProduct("Jeffrey Richter Type Library")] 
[assembly:AssemblyInformationalVersion("2.0.*")] 
// Lege die Werte für die Felder FileVersion, FileDescription und 
// Comments fest. 
[assembly:AssemblyVersion("1.0.*")] 
[assembly:AssemblyTitle("Jeff's FUT & RUT assembly")] 
[assembly:AssemblyDescription( 
    "This assembly contains Jeff's FUTs & RUTs")]


Tabelle T4 beschreibt die Felder der Versionsressource und nennt die dazugehörigen anwenderdefinierten Attribute. Wenn Sie die Baugruppe mit AL.exe zusammenstellen, können Sie diese Angaben auch mit den entsprechenden Schaltern auf der Kommandozeile machen. Tabelle T4 nennt daher auch die entsprechenden Kommandozeilenschalter für die einzelnen Ressourcenfelder. Der C#-Compiler versteht diese Schalter allerdings nicht und der allgemein übliche Weg zur Festlegung dieser Werte führt über die anwenderdefinierten Attribute.

T4 Die Felder der Versionsressource und die dazugehörigen Attribute

Versionsressource

AL-Schalter

Attribut/Kommentar

FILEVERSION

/version

System.Reflection.AssemblyVersionAttribute.

PRODUCTVERSION

/productversion

System.Reflection.AssemblyInformationalVersionAttribute.

FILEFLAGSMASK

(keiner)

Dieses Feld wird immer auf VS_FFI_FILEFLAGSMASK gesetzt (in WinVer.h als 0x0000003F definiert).

FILEFLAGS

(keiner)

Immer 0.

FILEOS

(keiner)

Derzeit immer VOS__WINDOWS32.

FILETYPE

/type

Auf VFT_APP setzen, falls /type:exe oder /type:win angegeben wird. Auf VFT_DLL setzen, wenn /type:lib angegeben wird.

FILESUBTYPE

(keiner)

Immer auf VFT2_UNKNOWN setzen (dieses Feld hat für VFT_APP und VFT_DLL keine Bedeutung).

Comments

/description

System.Reflection.AssemblyDescriptionAttribute.

CompanyName

/company

System.Reflection.AssemblyCompanyAttribute.

FileDescription

/title

System.Reflection.AssemblyTitleAttribute.

FileVersion

/version

System.Reflection.AssemblyVersionAttribute.

InternalName

/out

Auf den Namen setzen, der für die Zieldatei angegeben wird (ohne Namensendung).

LegalCopyright

/copyright

System.Reflection.AssemblyCopyrightAttribute.

LegalTrademarks

/trademark

System.Reflection.AssemblyTrademarkAttribute.

OriginalFilename

/out

Auf den Namen der Zieldatei setzen (ohne Pfad).

PrivateBuild

(keiner)

Immer leer.

ProductName

/product

System.Reflection.AssemblyProductAttribute.

ProductVersion

/productversion

System.Reflection.AssemblyInformationalVersionAttribute.

SpecialBuild

(keiner)

Immer leer.

Neben den Versionsattributen können Sie auch noch mit den AL-Schaltern /proc[essor] und /os die Einträge in die Manifesttabellen AssemblyProcessorDef und AssemblyOSDef festlegen. Diese Angaben lassen sich auch mit Hilfe der Attribute AssemblyOperatingSystemAttribute und AssemblyProcessorAttribute machen. Diese beiden Attribute wurden im Namensraum System.Reflection definiert. Wie schon erwähnt, werden die Manifesttabellen AssemblyProcessorDef und AssemblyOSDef derzeit von der CLR ignoriert. Also dürften diese Attribute wohl nur selten Verwendung finden, wenn überhaupt.

Einfache Installation (private Baugruppen)

Im vorigen Abschnitt war die Rede davon, wie man Module baut und diese Module zu einer Baugruppe zusammenfasst. Nun bin ich soweit, dass ich die Baugruppen verpacken und einsetzen kann, damit die Benutzer mit der Anwendung arbeiten können.

Baugruppen schreiben keine spezielle Verpackung vor. Am einfachsten verpackt man sie so, dass alle Dateien direkt kopiert werden. So können Sie die Dateien der Baugruppe zum Bespiel auf einer CD unterbringen und zudem noch eine Stapeldatei für den Anwender vorbereiten, mit der sich die Dateien einfach von der CD auf die Festplatte des Anwenders kopieren lassen. Da die Baugruppen selbst alle Angaben über die zusätzlich erforderlichen Typen in sich tragen, können die Benutzer einfach diese Anwendung starten und die Laufzeitschicht wird die angesprochenen Baugruppen im Verzeichnis der Anwendung suchen. Es sind keine Einträge in der Registrierdatenbank oder im Active Directory erforderlich, damit die Anwendung lauffähig wird. Und wenn die Anwendung nicht mehr gebraucht wird und vom System verschwinden soll, löscht man einfach alle diese Dateien. Fertig!

Natürlich können Sie die Dateien der Baugruppe auch mit Hilfe eines anderen Mechanismus verpacken und installieren, zum Beispiel als .cab-Dateien (typisches Einsatzgebiet sind Internet-Szenarien, damit die Dateien komprimiert werden und sich die Ladezeiten verkürzen). Sie können die Dateien der Baugruppe auch in einer MSI-Datei für den Installationsdienst von Windows unterbringen (MSIExec.exe Version 1.5 und höher).

Mit einer Stapeldatei oder einem einfachen Installationsprogramm kann man die Anwendung zwar auf der Maschine des Benutzers unterbringen, aber für die Erstellung von Verknüpfungen auf dem Desktop, im Startmenü oder der Schnellstartleiste ist eine aufwendigere Installationssoftware erforderlich. Außerdem können Sie die Anwendung zwar sehr leicht in einem Backup sichern und bei Bedarf wieder aufziehen oder von einer Maschine zur anderen verlegen, aber die bestehenden Verknüpfungen bedürfen einer speziellen Behandlung.

Baugruppen, die im selben Verzeichnis wie die Anwendung untergebracht werden, nennt man private Baugruppen, weil diese Dateien nicht auch von anderen Anwendungen benutzt werden (es sei denn, diese andere Anwendung wird im selben Verzeichnis installiert). Private Baugruppen sind für Entwickler, Anwender und Administratoren ein großer Gewinn, weil man ihretwegen weder die Registrierdatenbank noch die Einträge im Active Directory zu aktualisieren braucht. Baugruppen beschreiben sich ja vollständig selbst. Jeder Typ, der in einer Baugruppe angesprochen wird, hat entsprechende Metadaten, die der Laufzeitschicht die Suche nach der Baugruppe ermöglichen, in welcher der Typ zu finden ist. Außerdem gilt der Typ nur im Kontext der aufrufenden Baugruppe. Das bedeutet, dass eine Anwendung immer genau den gewünschten Typ einbinden wird, mit dem sie entwickelt und getestet wurde. Die Laufzeitschicht kann der Anwendung keine andere Baugruppe unterschieben, die zufällig einen Typ desselben Namens anbietet. Darin liegt ein wichtiger Unterschied zum unverwalteten COM, bei dem die Typen in der Registrierdatenbank erfasst werden und damit für jede Anwendung auf der Maschine zugänglich sind.

Einfache administrative Kontrolle (Konfiguration)

Die Ausführung einer Anwendung kann Aspekte aufweisen, die am besten vom Benutzer oder vom Administrator entschieden werden. So kann der Administrator zum Beispiel beschließen, die Dateien einer bestimmten Baugruppe auf der Festplatte des Benutzers zu installieren oder Informationen aus dem Manifest der Baugruppe zu überschreiben. Außerdem bringen die verschiedenen Versionen und die Installation auf fernen Maschinen Probleme mit sich, die gelöst werden wollen. Darauf werde ich noch im nächsten Teil dieser kleinen Artikelserie zu sprechen kommen.

Zur administrativen Kontrolle über eine Anwendung könnte man eine Konfigurationsdatei im Anwendungsverzeichnis unterbringen. Diese Datei wird vom Administrator erstellt und aktualisiert. Die Laufzeitschicht informiert sich in der Konfigurationsdatei darüber, wie sie mit der Anwendung verfahren soll. Solche Konfigurationsdateien werden in XML erstellt und lassen sich mit der betreffenden Anwendung verknüpfen, mit einem bestimmten Anwender oder mit der Maschine. Durch den Einsatz einer separaten Datei (im Gegensatz zur Registrierdatenbank) lassen sich die entsprechenden Konfigurationen leichter sichern. Außerdem kann der Administrator die Anwendung wesentlich leichter auf andere Maschinen kopieren. Er kopiert einfach die erforderlichen Dateien und auch die Konfigurationsdateien.

Nehmen wir an, der Administrator möchte Ihre Anwendung zwar einsetzen, aber die Dateien der Baugruppe JeffTypes in einem anderen Verzeichnis unterbringen als die Anwendung. Die Verzeichnisstruktur könnte so aussehen:

AppDir-Verzeichnis (mit den Baugruppen der Anwendung) 
   AuxFiles-Unterverzeichnis (mit der Baugrupppe JeffTypes)


Nach der Verlegung der Dateien an den gewünschten neuen Ort wird die Laufzeitschicht die JeffTypes-Dateien nicht mehr finden können. Um das zu verhindern legt der Administrator eine XML-Konfigurationsdatei an. Der Name dieser Datei muss dem Namen der Hauptbaugruppe von der Anwendung entsprechen, mit der Endung .cfg. In diesem Beispiel lautet der Name also App.cfg. Die Konfigurationsdatei könnte so aussehen:

<?xml version ="1.0"?> 
<Configuration> 
   <AppDomain PrivatePath="AuxFiles"/> 
</Configuration>


Sobald die Laufzeitschicht nun eine Baugruppe sucht, schaut sie zuerst im Anwendungsverzeichnis nach. Wird sie dort nicht fündig, sucht sie im Unterverzeichnis AuxFiles weiter. Im Property PrivatePath können Sie auch mehrere Pfade angeben, jeweils durch ein Semikolon voneinander getrennt. Jeder dieser Pfade bezieht sich auf das Startverzeichnis der Anwendung. Absolute Pfade lassen sich nicht einsetzen. Sämtliche beteiligten Baugruppen müssen entweder im Startverzeichnis der Anwendung liegen oder in einem Unterverzeichnis von diesem Startverzeichnis.

Fazit

Windows steht nicht ganz zu Unrecht in dem Ruf, die Installation und Verwaltung von Software nicht gerade zum Kinderspiel zu machen. Im Lauf der Jahre hat sich Microsoft einige Hilfen einfallen lassen, um dieses Problem zu lösen oder zumindest abzuschwächen (zum Beispiel Versionsressourcen und die relativ neue Installationsmaschine MSI). Obwohl das sicherlich eine Hilfe war und ist, sind die Probleme damit aber nicht aus der Welt. Ich persönlich bezweifele auch, dass sie sich jemals vollständig aus der Welt schaffen lassen. Aber Metadaten und Baugruppen (Assemblies) sind ein großer Schritt in Richtung Systemstabilität. Und private Baugruppen, die keine Einträge in der Registrierdatenbank erfordern, sind ein großer Schritt in Richtung einfachere Verwaltung.

Im nächsten Teil möchte ich Ihnen erläutern, wie man Baugruppen erstellt, die sich von mehreren Anwendungen benutzen lassen. Außerdem werde ich darauf zu sprechen kommen, wie die CLR die Baugruppen sucht.


Anzeigen:
© 2014 Microsoft