Assemblies kurz und knapp

Veröffentlicht: 28. Jul 2002 | Aktualisiert: 22. Jun 2004

Von Frank Eller und Senaj Lelic

Das .NET Framework ändert die Art der Entwicklung von Software auf gravierende Weise. Der Entwickler sieht sich mit neuen Begriffen konfrontiert. Einer davon ist die Bezeichnung "Assembly". Um diesen Begriff ranken sich Erläuterungen, die schon fast als mythisch zu bezeichnen sind.

Dieser Artikel soll Ihnen helfen, zu verstehen, was eine Assembly eigentlich ist. So kompliziert sich die Definition auch anhört, im Prinzip ist es ganz einfach. Wenn man es einmal verstanden hat.

Auf dieser Seite

 Die Definition der Assembly
 Assembly, EXE und DLL
 Assemblies und Module
 Fazit

Die Definition der Assembly

Wenn Sie im .NET Framework SDK bzw. in der Online-Hilfe des Visual Studio .NET nach dem Begriff Assembly suchen, erhalten Sie möglicherweise folgende Definition:

"Assemblies sind die Grundbausteine von .NET Framework-Anwendungen. Sie bilden die Basiseinheit für Weitergabe, Versionskontrolle, Wiederverwendung, Aktivierungs-Scoping und Sicherheitsberechtigungen. Eine Assembly ist eine Auflistung von Typen und Ressourcen, die so erstellt wurden, dass sie zusammenarbeiten und eine logische funktionelle Einheit bilden. Eine Assembly stellt der Common Language Runtime die für das Erkennen von Typimplementierungen erforderlichen Informationen zur Verfügung. Für die Common Language Runtime sind Typen nur im Kontext einer Assembly vorhanden."

Alles klar? Dieser Abschnitt enthält eine Menge Informationen, er sagt jedoch nicht, was eine Assembly technisch gesehen eigentlich ist. Um es kurz zu sagen: Wenn Sie mit dem .NET Framework programmieren, d.h. eine Anwendung oder eine DLL programmieren, die das .NET Framework zur Laufzeit benötigt, dann erzeugen Sie eine Assembly. Assemblies sind also nichts weiter als Dateien mit der Endung .exe bzw. .dll.

Jetzt werden Sie sich sicherlich fragen, warum man denn einen neuen Begriff für diese seit vielen Jahren bekannten Dateien gewählt hat. Der Grund dafür ist, dass sie mit den Ihnen bekannten .exe-Dateien (oder .dll-Dateien) eigentlich nichts gemein haben als die Endung. Der Grundaufbau einer Assembly ist vollkommen anders als der einer ausführbaren Datei, die beispielsweise von Visual C++ erzeugt wurde. Sehen wir uns an, wo die Unterschiede liegen.

 

Assembly, EXE und DLL

Ausführbare Dateien vor .NET enthielten den fertigen Programmcode in binärer Form. Diese Dateien waren bereits von sich aus lauffähig, allerdings nur unter dem Betriebssystem, für das sie geschrieben wurden. Das .NET Framework folgt einem ganz anderen Konzept. Hier gibt es die Common Language Runtime (CLR), die den erzeugten Code auf Typsicherheit und Korrektheit überprüft und ihn dann an die so genannten Jitter (Just-In-Time-Compiler) weitergibt. Diese Jitter übersetzen das Programm dann in den eigentlichen Maschinencode.

Um diese Überprüfung zu ermöglichen, übersetzen die Compiler des .NET Frameworks ein Programm nicht in binären, sondern in den so genannten Intermediate Language Code (IL-Code). Hier kommt dann auch ein Konzept von .NET zum tragen, das in seiner Art bisher einzigartig ist, nämlich die Möglichkeit, mit unterschiedlichen Sprachen auf der gleichen Basis zu arbeiten.

Die Compiler übersetzen ein Programm unabhängig von der Sprache, in der es geschrieben wurde, in den gleichen IL-Code, der für alle Sprachen einheitlich ist. Oder anders ausgedrückt: Wenn Sie eine Methode mit VB.NET schreiben, und dann die gleiche Methode mit C#, werden die Compiler exakt den gleichen IL-Code produzieren, obwohl es sich um unterschiedliche Sprachen handelt.

Aus diesem Grund ist es auch möglich, eine in VB.NET geschriebene DLL in C# zu verwenden. Da der Inhalt der DLL in Form von IL-Code vorliegt, der sprachenunabhängig ist, ist es natürlich auch egal, in welcher Sprache die DLL ursprünglich erfasst wurde.

Nun stellt sich die Frage, wie die CLR den Code überprüfen kann, bzw. wie das .NET Framework herausfindet, welche Klassen, Methoden und Eigenschaften in einer DLL (oder einer .exe-Datei, bei der es sich ja auch um eine Assembly handelt) enthalten sind.

Diese Informationen müssen ja in jedem Fall ermittelt werden, sowohl zur Überprüfung von Abhängigkeiten (die die CLR durchführt) als auch zum Verwenden der in einer DLL enthaltenen Funktionen (in einer beliebigen .NET-Sprache).

Die Lösung dieses Problems besteht darin, dass der Compiler weitere Informationen in einer Assembly platziert, in Form eines so genannten Manifests. Darin finden sich Informationen zu den enthaltenen Klassen, zur Anwendung selbst, zu Abhängigkeiten von anderen Assemblies usw.

In althergebrachten ausführbaren Dateien waren diese Informationen nicht enthalten. Unter .NET ermöglichen sie aber, alles über eine Assembly herauszufinden - was sowohl für die Laufzeitumgebung gilt, als auch für den Programmierer. Der Zugriff auf die in einer Assembly enthaltenen Informationen erfolgt über die so genannte Reflection, mit der Sie unweigerlich bereits Kontakt hatten, nämlich in Form von IntelliSense unter Visual Studio .NET.

Halten wir fest: Eine Assembly ist grundsätzlich eine ausführbare Datei unter .NET. Sie unterscheidet sich von bisherigen .exe- oder .dll-Dateien im Wesentlichen durch zwei Punkte:

  • Eine Assembly enthält Code in einem Zwischenformat, dem Intermediate Language Format, während klassische .exe- und .dll-Dateien binären Code enthalten, der sofort ausgeführt werden kann. Der Code einer Assembly wird zur Laufzeit durch so genannte Jitter in binären Code übersetzt.

  • Eine Assmbly enthält ein so genanntes Manifest, das weitere Informationen beinhaltet. Darin sind Abhängigkeiten der enthaltenen Klassen untereinander, Abhängigkeiten zu anderen Assemblies und weitere Informationen enthalten. Eine Assembly beschreibt sich damit selbst, da auf die Informationen verhältnismäßig leicht zugegriffen werden kann. Klassische ausführbare Dateien enthalten lediglich binären Code ohne irgendwelche weiteren Informationen.

Abbildung 1 zeigt das Manifest einer Assembly (in diesem Fall ein Programm namens NetReflector). Das Programm wurde mithilfe von ILDASM.EXE disassembliert.

Bild01

Abbildung1: Das Manifest einer .NET Assembly

In dieser Abbildung sehen Sie bereits Abhängigkeiten von anderen Assemblies, z.B. mscorlib, System oder System.Windows.Forms sowie erste Informationen über die Assembly selbst.

 

Assemblies und Module

Eine Assembly ist also wie wir festgestellt haben eine Datei, die ausführbaren Code und ein Manifest beinhaltet. Wenn Sie sich in der Online-Hilfe des .NET Frameworks schon einmal zu diesem Thema umgesehen haben, oder wenn Sie sich insbesondere für Reflection interessieren, wird Ihnen aufgefallen sein, dass es auch so genannte Module gibt. Was aber unterscheidet ein Modul von einer Assembly?

Ein Modul ist im Prinzip auch eine DLL, allerdings enthält ein Modul kein Manifest. Stattdessen können mehrere Module in einer Assembly zusammengefasst werden, die Sie dann weitergeben. Diese Assembly enthält dann das Manifest für alle enthaltenen Module. Das Manifest wird in das erste in der Assembly enthaltene Modul hineingeschrieben.

Somit haben Sie die Möglichkeit, mehrere voneinander unabhängige Modul-DLLs zu erzeugen und diese später zu einer einzigen Assembly zusammenzufassen. Leider wird diese Möglichkeit derzeit vom Visual Studio .NET nicht unterstützt, sehr wohl aber vom Kommandozeilencompiler csc.exe. Dieser enthält eine recht umfangreiche Anzahl an Optionsschaltern, die Sie verwenden können. Einer davon ist /target. Hiermit geben Sie an, welche Art von Datei erzeugt werden soll:

Compilerschalter

Erzeugte Datei

/target:exe

Konsolenanwendung, .exe

/target:library

Eine DLL-Datei, also eine Assembly mit Manifest

/target:module

Eine DLL ohne Manifest, die später mit anderen zu einer einzigen DLL (die dann eine Assembly bildet) zusammengefasst werden kann.

/target:winexe

Eine Windowsanwendung

Erzeugte Module wiederum können Sie zur späteren fertigen Assembly über den Compilerschalter /addmodule hinzufügen. Dabei müssen Sie allerdings darauf achten, dass die Module im gleichen Verzeichnis liegen und dass es sich wirklich um Module handelt und nicht um Assemblies. In diesem Fall würde /addmodule nicht funktionieren. Mehrere Modulnamen werden über Semikola getrennt.

Ein kleines Beispiel soll verdeutlichen, wie das Ganze funktioniert. Erstellen Sie zwei voneinander unabhängige Dateien, die unterschiedliche Namespaces aufweisen. Mithilfe der unterschiedlichen Namenspaces werden wir später zeigen, dass innerhalb der Assembly in der Tat zwei Module zusammengefasst wurden. Der Quellcode der beiden Module ist beliebig, in diesem Falle mit C# geschrieben:

Modul 1: Sorter.cs

using System;
namespace SorterNamespace
{
  public class Sorter
  {
  public Sorter() {}
    private void Swap( ref int a, ref int b ) {
      int c = a;
      a = b;
      b = c;
    }
    public void DoSort( ref int[] aArray ) {
      bool hasChanged;
      do {
        hasChanged = false;
        for ( int i=0; i<aArray.Length-1; i++ ) {
          if ( aArray[i] > aArray[i+1] ) {
            Swap( ref aArray[i], ref aArray[i+1] );
            hasChanged = true;
          }
        }
      } while ( hasChanged );
    }
  }
}

Modul 2: Ggt.cs

using System;
namespace GgtNamespace
{
  public class Ggt
  {
  public Ggt() {}
    public int GGt( int a, int b ) {
      int helpVal = a;
      while ( helpVal > 1 ) {
        if ( ( ( a % helpVal ) == 0 ) && ( ( b % helpVal ) == 0 ) )
          break;
        else
          helpVal--;
      }
      return helpVal;
    }
  }
}

Zunächst werden diese beiden Module nun jeweils kompiliert, über die Kommandozeile mit dem Compilerschalter /target:module. Da es sich hier nicht um Dateien handelt, die sofort eingesetzt werden können, sollten Sie sich angewöhnen in einem solchen Fall nicht die Dateiendung .dll zu vergeben. Das könnte zu Missverständnissen führen, bzw. Sie können später Assemblies und Module nicht mehr unterscheiden. Im Beispiel wurde deshalb die Endung .modul gewählt.

csc /target:module out:sorter.modul sorter.cs
csc /target:module out:ggt.modul ggt.cs

Jetzt bleibt nur noch, beide zu einer DLL (inklusive Manifest, also einer Assembly) zusammenzufügen:

csc /target:library /addmodule:sorter.modul;ggt.modul /out:testlib.dll

Wenn Sie diese Library nun in einem Ihrer Projekte einbinden, werden Sie zwei Namespaces darin finden - SorterNamespace und GgtNamespace. Beide Module wurden zu einer Assembly zusammengefasst und der Compiler hat das Manifest hineingeschrieben.

 

Fazit

Sie sehen, dass die Unterschiede zur Welt vor .NET eigentlich gar nicht so groß sind. Lediglich der Begriff Modul ist hinzugekommen, für DLLs, die zwar ausführbaren Code beinhalten aber kein Manifest. Damit sind diese natürlich unter .NET nicht ausführbar, denn mithilfe der im Manifest gespeicherten Daten arbeitet auch die Laufzeitumgebung. Solange Sie mit dem Visual Studio arbeiten, ist das aber eigentlich unerheblich. Mit Visual Studio .NET ist es nicht möglich, Module zu erzeugen - hier wird beim Kompilieren einer DLL immer auch ein Manifest geschrieben.