Freigeben über


Tiefe Einblicke in CLR

Bewährte Methoden für die Interoperabilität mit verwaltetem und systemeigenem Code

Jesse Kaplan

Inhalt

Wann kommt Interoperabilität mit verwaltetem und systemeigenem Code zum Einsatz?
Interoptechnologien: Drei Auswahlmöglichkeiten
Interoptechnologien: P/Invoke
Interoptechnologien: COM Interop
Interoptechnologien: C++/CLI
Überlegungen zur Interoparchitektur
API-Entwurfs- und Entwicklungserfahrung
Leistung und Platzierung der Interopgrenze
Lebensdauerverwaltung

In mancher Hinsicht erscheint es vielleicht etwas seltsam, im MSDN Magazin Anfang 2009 einen Artikel wie diesen zu finden. Die Interoperabilität mit verwaltetem und systemeigenem Code wird in Microsoft .NET Framework, in mehr oder weniger derselben Form, seit der 2002 veröffentlichten Version 1.0 unterstützt. Außerdem können Sie dazu problemlos umfangreiche Dokumente auf API- und Toolebene und Tausende von Seiten ausführlicher Supportdokumentation finden. Was bei all dem jedoch noch fehlt, ist eine umfassende allgemeine architektonische Anleitung, in der beschrieben wird, wann Interoperabilität eingesetzt wird, welche architektonischen Überlegungen zu berücksichtigen sind und welche Interoptechnologie Sie verwenden sollen. Diese Lücke soll nun geschlossen werden, und dabei soll der vorliegende Artikel einen Anfang machen.

Wann kommt Interoperabilität mit verwaltetem und systemeigenem Code zum Einsatz?

Es ist noch nicht viel darüber veröffentlicht worden, wann die Verwendung von verwalteter/systemeigener Interoperabilität in Frage kommt, und vieles von dem, was zu diesem Thema geschrieben wurde, widerspricht sich. Manchmal basieren die Anleitungen nicht einmal auf praktischer Erfahrung. Deshalb soll als Erstes festgehalten werden, dass alle hier aufgeführten Anleitungen aufgrund von Erfahrungen im Interopteam entwickelt wurden. Diese Erfahrungen wurden in Zusammenarbeit mit internen und externen Kunden unterschiedlicher Größe gesammelt.

Aus den Erfahrungen konnten wir drei Produkte zusammenstellen, die als hervorragende Beispiele für eine erfolgreiche Verwendung von Interoperabilität dienen und repräsentativ für die verschiedenen Verwendungsarten von Interoperabilität sind. Visual Studio Tools for Office ist das Toolset zur verwalteten Erweiterbarkeit für Office und die erste Anwendung, die mir in den Sinn kommt, wenn ich an Interop denke. Es steht für die klassische Verwendung der Interop: eine große systemeigene Anwendung, mit der verwaltete Erweiterungen oder Add-Ins ermöglicht werden sollen. Als Nächstes steht auf meiner Liste Windows Media Center, eine Anwendung, die von Grund auf als gemischte verwaltete und systemeigene Anwendung aufgebaut wurde. Windows Media Center wurde hauptsächlich unter Verwendung von verwaltetem Code entwickelt, wobei einige Teile – die direkt mit dem TV Tuner und anderen Hardwaretreibern zu tun haben – in systemeigenem Code erstellt wurden. Abschließend gibt es noch Expression Design, eine Anwendung mit einer großen, vorab vorhandenen systemeigenen Codebasis, die neue verwaltete Technologien nutzen kann, in diesem Fall Windows Presentation Foundation (WPF), um Benutzerfunktionalität der nächsten Generation zu bieten.

Diese drei Anwendungen geben die drei häufigsten Gründe für die Verwendung von Interop vor: um die verwaltete Erweiterbarkeit von bereits vorhandenen systemeigenen Anwendungen zu ermöglichen, um dem Großteil einer Anwendung zu ermöglichen, die Vorteile des verwalteten Codes zu nutzen, während die Teile auf niedrigster Ebene weiterhin in systemeigenem Code geschrieben werden, und um einer bereits vorhandenen systemeigenen Anwendung Benutzerfunktionalität der nächsten Generation hinzuzufügen.

In den Anleitungen der Vergangenheit wurde in diesen Fällen vorgeschlagen, die gesamte Anwendung einfach in verwaltetem Code neu zu schreiben. Dies ist für die meisten vorhandenen Anwendungen einfach keine Option, da dieser Ratschlag nur schwer zu befolgen ist und von vielen ganz einfach abgelehnt wird. Interop ist genau die wichtige Technologie, die es Entwicklern ermöglicht, bereits erstellten systemeigenen Code zu erhalten und dabei trotzdem die neue verwaltete Umgebung nutzen zu können. Wenn Sie planen, Ihre Anwendung aus anderen Gründen neu zu schreiben, ist der verwaltete Code möglicherweise eine gute Wahl für Sie. Aber im Allgemeinen empfiehlt es sich nicht, eine Anwendung neu zu schreiben, lediglich um neue verwaltete Technologien zu verwenden und Interop zu vermeiden.

Interoptechnologien: Drei Auswahlmöglichkeiten

Es gibt drei wichtige Interoptechnologien, die in .NET Framework verfügbar sind, und welche Sie auswählen, wird teilweise durch die Art der für Interop verwendeten API und teilweise durch Ihre jeweiligen Anforderungen und die Notwendigkeit der Kontrollen von Grenzen bestimmt. Beim Plattformaufruf (Platform Invoke, P/Invoke) handelt es sich hauptsächlich um eine Interoptechnologie für verwalteten und systemeigenen Code, die es ermöglicht, systemeigene APIs im C-Stil von verwaltetem Code aufzurufen. COM-Interop ist eine Technologie, die Ihnen entweder ermöglicht, systemeigene COM-Schnittstellen von verwaltetem Code zu nutzen oder systemeigene COM-Schnittstellen von verwalteten APIs zu exportieren. Abschließend gibt es C++/CLI (früherer Name „verwaltetes C++“) zum Erstellen von Assemblys, die eine Mischung aus verwaltetem und systemeigenem C++-kompiliertem Code enthalten. C++/CLI wurde als Brücke zwischen verwaltetem und systemeigenem Code entworfen.

Interoptechnologien: P/Invoke

P/Invoke ist die einfachste der drei Technologien und wurde hauptsächlich für einen verwalteten Zugriff auf APIs im C-Stil entworfen. Mit P/Invoke muss jede API einzeln umschlossen werden. Dies kann eine sehr gute Wahl sein, wenn nur wenige APIs vorliegen, die umschlossen werden müssen, und deren Signaturen nicht sehr komplex sind. Die Verwendung von P/Invoke wird jedoch zunehmend schwieriger, wenn die nicht verwalteten APIs viele Argumente enthalten, die über keine guten verwalteten Äquivalente verfügen, z. B. Strukturen variabler Länge, ungültige *s, überlappende Unionoperatoren und so weiter.

Die .NET Framework-Basisklassenbibliotheken (Base Class Libraries, BCL) enthalten viele Beispiele für APIs, bei denen es sich wirklich nur um „dicke“ Wrapper um eine große Anzahl von P/Invoke-Deklarationen handelt. Fast die gesamte Funktionalität in .NET Framework, die nicht verwaltete Windows-APIs umschließt, ist mit P/Invoke erstellt. Genau genommen ist sogar Windows Forms fast vollständig auf der systemeigenen „ComCtl32.dll“ mittels P/Invoke erstellt.

Es gibt einige sehr wertvolle Ressourcen, die die Verwendung von P/Invoke erheblich erleichtern. Als Erstes enthält die Website „pinvoke.bet“ ein ursprünglich von Adam Nathan vom CLR-Interopteam eingerichtetes Wiki, zu dem Benutzer eine große Anzahl Signaturen für eine Vielzahl an allgemeinen Windows-APIs beigetragen haben.

Es gibt auch ein sehr nützliches Visual Studio-Add-In, das einen einfachen Zugriff auf „pinvoke.net“ von Visual Studio ermöglicht. Für APIs, die nicht auf „pinvoke.net“ zu finden sind, hat das Interopteam – unabhängig davon, ob die APIs aus Ihren eigenen oder anderen Bibliotheken stammen – ein Tool veröffentlicht, das eine P/Invoke-Signatur generiert. Es heißt P/Invoke-Interop-Assistent und erstellt auf Basis einer Headerdatei automatisch Signaturen für systemeigene APIs. Im folgenden Bildschirmfoto sehen Sie das Tool in Aktion.

fig01.gif

Erstellen von Signaturen im P/Invoke-Interop-Assistenten

Interoptechnologien: COM-Interop

COM-Interop ermöglicht Ihnen, entweder COM-Schnittstellen von verwaltetem Code zu nutzen oder verwaltete APIs als COM-Schnittstellen verfügbar zu machen. Sie können das TlbImp-Tool verwenden, um eine verwaltete Bibliothek zu generieren, die verwaltete Schnittstellen verfügbar macht, um mit einer spezifischen „COM tlb“ zu kommunizieren. TlbExp führt die entgegengesetzte Aufgabe durch und generiert eine „COM tlb“ mit Schnittstellen, die den ComVisible-Typen in einer verwalteten Assembly entsprechen.

COM-Interop kann eine sehr gute Lösung für Sie sein, wenn Sie COM bereits innerhalb Ihrer Anwendung oder als deren Erweiterbarkeitsmodell verwenden. Es ist auch die einfachste Möglichkeit, zuverlässige COM-Semantiken zwischen verwaltetem und systemeigenem Code beizubehalten. Insbesondere ist COM-Interop eine hervorragende Wahl, wenn Sie mit einer Visual Basic 6.0-basierten Komponente arbeiten, da die CLR im Grunde die gleichen COM-Regeln befolgt wie Visual Basic 6.0.

COM-Interop ist weniger nützlich, wenn COM in der Anwendung nicht bereits intern verwendet wird oder wenn Sie keine zuverlässige COM-Semantik benötigen und die Leistung für Ihre Anwendung nicht akzeptabel ist.

Microsoft Office ist das bekannteste Beispiel für eine Anwendung, die COM-Interop als Brücke zwischen verwaltetem und systemeigenem Code verwendet. Office eignete sich hervorragend für COM-Interop, da COM als Erweiterbarkeitsmethode bereits seit langem eingesetzt und am häufigsten von Visual Basic for Applications (VBA) oder Visual Basic 6.0 aus verwendet wurde.

Ursprünglich basierte Office vollständig auf TlbImp und der dünnen Interopassembly als verwaltetes Objektmodell. Mit der Zeit wurde jedoch das Produkt Visual Studio-Tools für Office (VSTO) in Visual Studio integriert und bot ein immer umfangreicheres Entwicklungsmodell, in dem viele der in diesem Artikel beschriebenen Prinzipien integriert wurden. Beim heutigen Einsatz des VSTO-Produkts wird manchmal genauso leicht vergessen, dass COM-Interop als Grundlage für VSTO dient, wie vergessen wird, dass P/Invoke die Grundlage für einen Großteil der BCL bildet.

Interoptechnologien: C++/CLI

C++/CLI wurde als Brücke zwischen der systemeigenen und der verwalteten Umgebung entworfen und ermöglicht Ihnen, sowohl verwaltetes als auch systemeigenes C++ in dieselbe Assembly (sogar dieselbe Klasse) zu kompilieren und C++-Standardaufrufe zwischen den beiden Teilen der Assembly durchzuführen. Bei Verwendung von C++/CLI wählen Sie aus, welcher Teil der Assembly verwaltet werden und welcher systemeigen sein soll. Die sich ergebende Assembly ist eine Mischung aus MSIL (Microsoft Intermediate Language, die in allen verwalteten Assemblys zu finden ist) und systemeigenem Assemblycode. C++/CLI ist eine äußerst leistungsfähige Interoptechnologie, die Ihnen fast vollständige Kontrolle über die Interopgrenze gibt. Der Nachteil ist der, dass Sie allerdings gezwungen werden, nahezu vollständige Kontrolle über die Grenze auszuüben.

C++/CLI kann eine gute Brücke sein, wenn eine Überprüfung vom statischen Typ erforderlich ist, wenn genaue Leistung gefordert wird und wenn Sie eine besser vorhersagbare Festlegung benötigen. Wenn P/Invoke oder COM-Interop Ihre Anforderungen erfüllt, ist deren Verwendung im Allgemeinen einfacher, besonders wenn Ihre Entwickler mit C++ nicht vertraut sind.

Es gibt einige Dinge zu bedenken, wenn Sie C++/CLI in Betracht ziehen. Als Erstes muss Folgendes berücksichtigt werden: Wenn Sie planen, C++/CLI zu verwenden, um eine schnellere Version der COM-Interop bereitzustellen, ist COM-Interop langsamer als C++/CLI, weil dadurch sehr viel Arbeit für Sie erledigt wird. Wenn Sie COM nur gelegentlich in Ihrer Anwendung verwenden und keine zuverlässige COM-Interop benötigen, ist dies ein guter Kompromiss.

Wenn Sie jedoch einen großen Teil der COM-Spezifikation verwenden, werden Sie nach dem erneuten Hinzufügen der benötigten COM-Semantik in Ihre C++/CLI-Lösung wahrscheinlich feststellen, dass Sie viel Arbeit geleistet haben und die Leistung nicht besser ist als das, was COM-Interop bietet. Mehrere Microsoft-Teams haben diesen Weg eingeschlagen, nur um wieder zur COM-Interop zurückzukehren.

Die zweite wichtige Überlegung bei der Verwendung von C++/CLI ist die, dass es nur als Brücke zwischen der verwalteten und der systemeigenen Umgebung gedacht ist und keine Technologie sein soll, mit der Sie den Hauptteil Ihrer Anwendung schreiben. Dies ist sicherlich auch möglich, aber Sie werden feststellen, dass die Entwicklerproduktivität weitaus niedriger liegt als in einer reinen C++- oder C#-/Visual Basic-Umgebung und dass die Anwendung viel langsamer startet. Wenn Sie also C++/CLI verwenden, kompilieren Sie mit dem /clr-Schalter nur die Dateien, die Sie benötigen, und verwenden Sie eine Kombination reiner verwalteter oder reiner systemeigener Assemblys, um die zentrale Funktionalität der Anwendung zu erstellen.

Überlegungen zur Interoparchitektur

Nachdem Sie sich für die Verwendung von Interop in Ihrer Anwendung entschieden und die zu verwendende Technologie festgelegt haben, stehen einige grundlegende Überlegungen beim Entwerfen der Lösung an, einschließlich des API-Entwurfs und der Entwicklerfunktionalität für das Programmieren anhand der Interopgrenze. Berücksichtigen Sie außerdem, wo Sie die systemeigenen verwalteten Übergänge platzieren möchten, und die Leistungsauswirkungen, die sich daraus möglicherweise für die Anwendung ergeben. Abschließend müssen Sie die Lebensdauerverwaltung bedenken und ob Sie etwas unternehmen müssen, um die Lücke zwischen der vom Garbage Collector bereinigten verwalteten Umgebung und der manuellen/deterministischen Lebensdauerverwaltung der systemeigenen Umgebung zu schließen.

API-Entwurfs- und Entwicklungserfahrung

Wenn Sie an den API-Entwurf denken, müssen Sie sich mehrere Fragen stellen: Wer wird anhand meiner Interopschicht programmieren? Sollte die Programmiererfahrung verbessert werden, oder sollten eher die Kosten der Grenzerstellung gesenkt werden? Schreiben dieselben Entwickler, die anhand dieser Grenze programmieren, auch den systemeigenen Code? Handelt es sich dabei um andere Entwickler in Ihrem Unternehmen? Handelt es sich um Drittanbieterentwickler, die Ihre Anwendung erweitern oder sie als Dienst verwenden? Wie erfahren sind diese Entwickler? Sind sie mit systemeigenen Modellen vertraut, oder beschränken sie sich nur auf das Schreiben von verwaltetem Code?

Ihre Antworten zu diesen Fragen helfen Ihnen bei der Entscheidung, wo Sie Ihre Anwendung auf dem Kontinuum zwischen einem sehr einfachen Wrapper über dem systemeigenen Code und einem umfangreichen verwalteten Objektmodell, das systemeigenen Code im Hintergrund verwendet, ansiedeln sollten. Mit einem einfachen Wrapper sickern alle systemeigenen Modelle durch. Die Entwickler werden die Grenze und die Tatsache, dass sie anhand einer systemeigenen API programmieren, nicht übersehen können. Mit einem „dickeren“ Wrapper können Sie die Tatsache, dass systemeigener Code im Spiel ist, fast vollständig ausblenden. Die Dateisystem-APIs in der BCL sind ein hervorragendes Beispiel für eine sehr dicke Interopschicht, die ein erstklassiges verwaltetes Objektmodell bietet.

Leistung und Platzierung der Interopgrenze

Bevor Sie zu viel Zeit mit der Optimierung der Anwendung verbringen, sollten Sie unbedingt bestimmen, ob möglicherweise ein Interopleistungsproblem vorliegt. Viele Anwendungen verwenden Interop in leistungskritischen Abschnitten, und Sie müssen in dieser Hinsicht gut aufpassen. Aber in vielen anderen wird Interop als Reaktion auf Mausklicks des Benutzers verwendet, und es treten daher nicht Dutzende, Hunderte oder gar Tausende von Interopübergängen auf, die Verzögerungen für den Benutzer verursachen. Andererseits sollten Sie im Hinblick auf die Leistung Ihrer Interoplösung zwei Ziele vor Augen haben: Die Verringerung der Anzahl der durchgeführten Interopübergänge und die Verringerung der Datenmenge bei jedem Übergang.

Ein bestimmter Interopübergang mit einer bestimmten Datenmenge, die zwischen der verwalteten und systemeigenen Umgebung ausgetauscht wird, hat grundsätzlich Fixkosten. Diese Fixkosten hängen von der ausgewählten Interoptechnologie ab, aber wenn Sie diese Auswahl getroffen haben, weil Sie die Features dieser Technologie benötigten, lässt sich daran nichts ändern. Folglich sollten Sie den Schwerpunkt auf die Interaktionen an der Grenze legen und dann die Menge der ausgetauschten Daten verringern.

Wie Sie dies erreichen, hängt größtenteils von Ihrer Anwendung ab. Eine allgemeine und anpassungsfähige Strategie, mit der viele Erfolg hatten, besteht darin, die Isolierungsgrenze zu verschieben, indem Sie auf der Seite der Grenze, auf der die ausgelastete und datenschwere Schnittstelle definiert wurde, etwas Code schreiben. Die grundlegende Idee besteht darin, eine Abstraktionsschicht zu schreiben, die Aufrufe in die sehr ausgelastete Schnittstelle oder, noch besser, in den Teil der Anwendungslogik verschiebt, der mit dieser API über die Grenze hinaus interagiert und nur Eingaben und Ergebnisse über die Grenze zu übergeben.

Lebensdauerverwaltung

Die Unterschiede in der Lebensdauerverwaltung zwischen der verwalteten und systemeigenen Umgebung sind oft eine der größten Herausforderungen für Interopkunden. Der grundlegende Unterschied zwischen dem Garbage Collector-basierten System in .NET Framework und dem manuellen und deterministischen System in der systemeigenen Umgebung kann sich oft in überraschender Weise manifestieren und ist oft nur schwer zu analysieren.

Das erste Problem, das Sie in einer Interoplösung vermutlich feststellen, ist die äußerst lange Zeit, die einige verwaltete Objekte an ihren systemeigenen Ressourcen festhalten, selbst wenn die verwaltete Umgebung diese nicht mehr benötigt. Dies verursacht oft Probleme, wenn die systemeigene Ressource sehr knapp ist und freigegeben werden sollte, sobald der Aufrufer sie nicht mehr verwendet (Datenbankverbindungen sind ein großartiges Beispiel hierfür).

Wenn diese Ressourcen nicht knapp sind, können Sie sich einfach auf den Garbage Collector verlassen, der den Finalizer eines Objekts aufruft und ermöglicht, dass der Finalizer die systemeigenen Ressourcen (entweder implizit oder explizit) freigibt. Wenn die Ressourcen knapp sind, kann das verwaltete Dispose-Muster sehr nützlich sein. Statt systemeigene Objekte direkt für den verwalteten Code verfügbar zu machen, sollten Sie sie zumindest mit einem einfachen Wrapper umschließen, der IDisposable implementiert und das standardmäßige Dispose-Muster befolgt. Auf diese Weise können Sie bei Ressourcenauslastungsproblemen explizit über diese Objekte in Ihrem verwalteten Code verfügen und die Ressourcen freigeben, sobald sie nicht mehr benötigt werden.

Beim zweiten Problem mit der Lebensdauerverwaltung, das sich häufig auf Anwendungen auswirkt, geht es um ein von Entwicklern oft als hartnäckige Garbage Collection wahrgenommenes Problem: die Speichernutzung des Garbage Collection-Prozesses steigt immer mehr, aber aus irgendeinem Grund wird der Garbage Collector nur selten ausgeführt, und Objekte bleiben erhalten. Oft nehmen Entwickler wiederholte Aufrufe an „GC.Collect“ vor, um eine Lösung des Problems zu erzwingen.

Die tiefere Ursache für dieses Problem ist meist, dass viele sehr kleine verwaltete Objekte vorhanden sind, die sich an sehr großen systemeigenen Datenstrukturen festhalten und diese am Leben halten. Hier geschieht Folgendes: Der Garbage Collector optimiert sich selbst und versucht, möglichst keine Zeit für unnötige oder unnütze Sammlungen aufzuwenden. Abgesehen von der aktuellen Speicherbelastung des Prozesses untersucht der Garbage Collector bei der Entscheidung, eine weitere automatische Speicherbereinigung durchzuführen, wie viel Speicher bei jeder Garbage Collection freigesetzt wird.

In diesem Szenario erkennt der Garbage Collector aber, dass jede Sammlung nur eine kleine Speichermenge freisetzt (denken Sie daran, dass ihm nur bekannt ist, wie viel verwalteter Speicher freigesetzt wird) und erkennt nicht, dass ein Freisetzen dieser kleinen Objekte die Gesamtbelastung möglicherweise erheblich verringert. Dies führt zu einer Situation, in der immer weniger Garbage Collections durchgeführt werden, obwohl die Speichernutzung zunimmt.

Die Lösung für dieses Problem ist, dem Garbage Collector Hinweise in Bezug auf die tatsächlichen Speicherkosten dieser kleinen verwalteten Wrapper gegenüber den systemeigenen Ressourcen zu geben. In .NET Framework 2.0 wurde daher ein API-Paar hinzugefügt, das genau dies ermöglicht. Sie können den gleichen Typ von Wrapper verwenden, mit dem Sie das Dispose-Muster knappen Ressourcen hinzugefügt haben, aber Sie können sie mit einem neuen Zweck versehen, um dem Garbage Collector Hinweise zu geben, anstatt die Ressourcen explizit selbst freisetzen zu müssen.

Rufen Sie dazu im Konstruktor für dieses Objekt einfach die GC.AddMemoryPressure-Methode auf, und geben Sie die ungefähren Kosten im systemeigenen Speicher des systemeigenen Objekts ein. Sie können dann GC.RemoveMemoryPressure in der Finalizer-Methode des Objekts aufrufen. Anhand dieses Paars von Aufrufen kann der Garbage Collector verstehen, was die tatsächlichen Kosten dieser Objekte sind und wie viel Speicher freigesetzt wird, wenn sie bereinigt werden. Stellen Sie aber unbedingt sicher, dass sich die Aufrufe von Add/RemoveMemoryPressure vollkommen im Gleichgewicht befinden.

Bei der dritten häufigen Unterbrechung in der Lebensdauerverwaltung zwischen der verwalteten und systemeigenen Umgebung geht es nicht so sehr um die Verwaltung einzelner Ressourcen oder Objekte als vielmehr um ganze Assemblys oder Bibliotheken. Systemeigene Bibliotheken können problemlos entladen werden, wenn eine Anwendung sie nicht mehr benötigt, aber verwaltete Bibliotheken können nicht von allein entladen werden. Stattdessen enthält die CLR Isolierungseinheiten namens „AppDomains“, die einzeln entladen werden können und beim Entladen alle Assemblys, Objekte und sogar Threads, die in dieser Domäne ausgeführt werden, bereinigen. Wenn Sie eine systemeigene Anwendung erstellen und es gewohnt sind, nicht mehr benötigte Add-Ins zu entladen, werden Sie feststellen, dass Sie durch Verwendung verschiedener AppDomains für die einzelnen verwalteten Add-Ins die gleiche Flexibilität wie beim Entladen einzelner systemeigener Bibliotheken erhalten.

Senden Sie Fragen und Kommentare in englischer Sprache an clrinout@microsoft.com.

Jesse Kaplan ist derzeit Programmmanager im Bereich verwaltete/systemeigene Interoperabilität für das CLR-Team bei Microsoft. In der Vergangenheit war er für die Gebiete Kompatibilität und Erweiterbarkeit verantwortlich.