© Martin Korneffel,
Stuttgart 2005 +++ email: trac@n.zgs.de
+++ web: www.s-line.de/homepages/trac
ComInterop
Einführung
Die .NET Laufzeitumgebung weist deutliche
Unterschiede zu den klassischen Laufzeitumgebungen wie C/C++ oder COM
auf. So hat z.B. die COM- Laufzeitumgebung eine völlig andere
Speicherverwaltung wie die .NET Laufzeitumgebung. Auch die
fundamentalen Datentypen wie Strings haben in beiden
Laufzeitumgebungen unterschiedliche Binärformate. Ein COM-
Objekt ist deshalb aus der .NET - Welt nicht direkt ansteuerbar.
Umgekehrt gilt das gleiche. Es müssen zwischen der .NET und den
klassischen Laufzeitumgebungen Brücken geschlagen werden.
PInvoke vs. COM Interop
Die zwei Mechanismen zum Datentransfer zwischen
verwalteter und unverwalteter Welt sind der Plattformaufruf (PInvoke)
für klassische DLL's und die COM Interop.
Der PInvoke- Mechanismus ist eingeschränkter
als die COM Interop, wie folgende Übersicht zeigt:
Plattformaufruf
|
COM Interop
|
|
|
Nur .NET Client kann eine unverwaltete
Methode aufrufen, nicht umgekehrt
Kein Aufruf von unverwalteten Klassen oder
Objekten möglich
|
.NET Client kann COM Objekte instanziieren
und über Wrapper- Klassen steuern
COM Client kann .NET Objekte instanziieren
und über COM- Schnittstellen steuern, die ein Wrapper
bereitstellt
|
Interop- Marshalling: Daten zwischen verwalteter
und unverwalteter Welt austauschen
Unter marshalling (engl. rangieren) wird der
Transfer der Daten über Methodenparameter zwischen verwalteter
und unverwalteter Welt verstanden. Dieser erfolgt automatisiert nach
einem Regelwerk. Durch spezielle Attribute kann das Regelwerk vom
Entwickler übersteuert werden.
Richtung des Datenfluss definieren
Eine Methode kann reine Eingabe, Ausgabe und
Ein/Ausgabeparamter besitzen. Dabei werden Parameterspezifikationen
wie folgt interpretiert:
|
In
|
Out
|
In/Out
|
VB.NET
|
ByVal
|
ByRef
|
ByRef
|
C#
|
-
|
out
|
ref
|
Die Datenflussrichtung beeinflusst den Aufwand und
die Sicherheit beim Marshalling. Ein In- Wertetyp muß höchstens
in den Stack/Heap vom Server kopiert werden. In/Out Wertetypen
erfordern zusätzlich das Zurückkopieren des Ergebnisses in
den Client- Stack/Heap.
Der Entwickler kann den Datenfluss mittels
Attribute feiner steuern:
InAttribute()
OutAttribute()
Blitfähige Typen
Wertetypen in der verwalteten Welt unterliegen
einer ähnlichen Speicherverwaltung (Stack) wie in der
unverwalteten. Wenn die Binärformate sich zwischen beiden Welten
nicht unterscheiden, dann können die verwalteten Werte durch
eine 1:1 Kopie aus den nichtverwalteten Werten erstellt werden und
umgekehrt. Man spricht auch von blitfähigen Typen.
Folgende verwaltete Typen sind blitfähig:
void, byte, int, float, *(Pointer)
MarshalAsAttribute
Das Resultat eines Datentransfers zwischen
verwalteter und unverwalteter Welt ist nicht immer eindeutig. So kann
ein verwaltetes Boolean in verschiedene unverwaltete Darstellungen
überführt werden. Mit dem MarshalAsAttribute kann
das Zielformat definiert werden
void TesteZahl([In] string zahl, [Out] [MarshalAs(UnmanagedType.VariantBool)] bool gueltig)
PInvoke- Plattformaufruf
Soll eine Methode aus einer unverwalteten DLL
aufgerufen werden, dann muß der Einsprungpunkt zuvor deklariert
werden mittels eines Attributes. Im folgenden Beispiel wird der
Aufruf einer Messagebox aus der user32.dll des Betriebssystems
demonstriert:
using System;
using System.Collections.Generic;
using System.Text;
using System.Runtime.InteropServices;
namespace u18_Interop
{
class Program
{
// Einsprungpunkt wird deklariert
[DllImport("user32.dll")]
//private static extern Int32 MessageBox(IntPtr hwnd, String ptext, String pcaption, Int32 utype);
private static extern Int32 MessageBox(IntPtr hwnd, StringBuilder ptext, StringBuilder pcaption, Int32 utype);
static void Main(string[] args)
{
// Messagebox aufrufen
//MessageBox(new IntPtr(0), "Hallo Welt", "Win32- MsgBox", 0);
StringBuilder b1 = new StringBuilder("Hallo Welt");
StringBuilder b2 = new StringBuilder("bla");
MessageBox(new IntPtr(0), b1, b2, 0);
}
}
}
COM Interop
In COM werden Objekte instanziiert und über
Schnittstellen gesteuert. COM Anwendungen stellen Server als auch
Clients dar. Schnittstellen und Objekte haben unter COM ein ähnlich
Bedeutung wie unter .NET. Der Interop- Mechanismus muss COM
Schnittstellen in die .NET Welt einbetten, und .NET Klassen über
Schnittstellen aus der COM- Welt steuern.
Dies geschieht durch sog. Wrapper.
COM- Schnittstellen werden in die .NET Welt über
Runtime Callable Wrapper (RCW) als
Klassen eingebettet.
.NET Objekte werden durch Com Callable Wrapper
(CCW) in der COM Welt über Schnittstellen steuerbar.
RCW
RCW steht für Runtime Callable Wrapper.
Ein RCW ist eine .Net Objekt, das eine COM Schnittstelle in der
verwalteten Welt kapselt.
Generiert wird der RCW
durch die Common Language Runtime (CLR) zur Laufzeit beim Zugriff auf
eine COM- Schnittstelle. Dabei werden IL- Metadaten über die
COM- Schnittstelle ausgewertet, die zuvor mit dem tlbimp.exe
Tool aus den COM Typbibliotheken des COM Servers gewonnen wurden. Das
tlbimp.exe Tool erzeugt eine sog. Interop Assembly.
Die Interop- Assembly
wird in Visual- Studio automatisch erstellt, wenn ein Verweis auf
eine mittels tlbimp.exe erstellte Interop-Assembly
gesetzt wird.
Beispiel 1: Aufruf des Internet- Explorers
Verweis im Projekt auf Com- Komponente
Microsoft Internet Controls anlegen-> Erzeugt RCW unter
Namespace SHDocVw
Instanziieren der RCW- Klasse
SHDocVw.InternetExplorerClass
Laden der Website "www.tracs.de"
mittels der Navigate- Methode
Warten, bis Seite
geladen wurde mittels Busy- Methode
Explorerfenster
anzeigen durch setzen von Visible auf true
VB
Module Module1
Sub Main()
Try
Dim ie As New SHDocVw.InternetExplorer()
' Vollbildansicht
ie.TheaterMode = True
' Webseite aufrufen
ie.Navigate("http://www.tracs.de")
' Warten, bis der Ladevorgang beendet wurde
While (ie.Busy)
System.Threading.Thread.Sleep(100)
End While
' Browserfenster sichbar machen
ie.Visible = True
Console.WriteLine("Beenden ?")
Console.ReadLine()
' Browser schließen
ie.Quit()
Catch ex As Exception
Console.WriteLine("Fehler: {0}", ex.Message)
End Try
End Sub
End Module
C#
using System;
using SHDocVm;
namespace ComIeTest {
class Class1 {
[STAThread]
static void Main(string args[]) {
InternetExplorerClass ie = new InternetExplorerClass();
string empty = String.Empty;
ie.navigate("http://www.google.de", ref empty, ref empty, ref empty, ref empty);
ie.visible = true;
Console.WriteLine("Gelesen ?");
Console.ReadLine();
ie.Quit();
}
}
}
Beispiel 2: Auflisten von Browserfenstern
SHDocVw.InternetExplorer browser = null;
string filnam;
SHDocVw.ShellWindows shellWindows =
new SHDocVw.ShellWindowsClass();
foreach (SHDocVw.InternetExplorer ie
in shellWindows)
{
filnam = Path.GetFileNameWithoutExtension(
ie.FullName).ToLower();
if (filnam.Equals("iexplore"))
{
browser = ie;
break; // i hate 'break' but it's easy here
}
}
Einbinden des Webbrowsers als ActiveX- Control
ActiveX Controls werden im Visual Studio der
Toolbox hinzugefügt. Z.B. kann der Microsoft Webbrowser als
Control eingefügt werden, um mit einer Winform- Anwendung einen
spezialisierten Browser zu entwickeln.
In der Form- Load-
Methode der WinForm kann dann der Webserver wie folgt mit einer
Startseite geladen werden:
private AxSHDocVw.AxWebBrowser axWebBrowser1;
private void Form1_Load(object sender, System.EventArgs e)
{
object empty = String.Empty;
this.axWebBrowser1.Navigate("http://localhost/trac", ref empty, ref empty, ref empty, ref empty);
this.axWebBrowser1.Visible=true;
}
Debuggen vom COM Servern über .NET Clients
Um aus .NET Clients Com- Server zu debuggen, muß
im .NET Projekteigenschaften folgender Schalter gesetzt werden:
Konfigurationseigenschaften/Debuggen/Nicht verwaltetes debuggen aktiv = true
Office- Automation mit .NET
Die MS- Office Anwendungen bieten ihre
Funktionalität über COM- Komponenten zwecks Automatisierung
an. So sind z.B. über eine Dialogbox Messwerte eingegeben
werden. Anschließend kann per Knopfdruck eine Auswertung in
einem Excel- Arbeitsblatt erstellt werden.
Um die Zusammenarbeit mit .NET zu
Optimieren, wird das Officepaket mit optimierten COM Wrapper-
Assemblies ausgeliefert, genannt Primary Interop Assemblies
(PIA). Sie können mit dem
Installationsprogramm von Office 2003 nachträglich hinzugefügt
werden über [Features
hinzufügen oder entfernen]/[Erweiterte Anpassung von
Anwendungen]/Excel/.NET-Programmunterstützung.
Namespaces
Die PIA für Excel spannt folgenden Namespace
auf:
Microsoft.Office.Interop.Excel
Beispiel
Imports Microsoft.Office.Interop.Excel
Module Module1
Sub Main()
Dim ExApp As New Application()
With ExApp
.Visible = True
.Workbooks.Add()
.Range("A1").Value = "Messwerte"
For n As Short = 2 To 13
ExApp.ActiveSheet.Cells(1, n).Value = New Random().Next(1, 100)
Next
End With
Dim R As Range = ExApp.ActiveSheet.Range("A1:L1")
Dim ch As ChartObject = ExApp.ActiveSheet.ChartObjects.Add(10, 30, 500, 300)
With ch.Chart
.ChartType = XlChartType.xl3DColumn
.SetSourceData(R)
.HasTitle = True
.ChartTitle.Characters.Text = "Ein Zufallschart"
End With
End Sub
End Module
CCW
Wenn bestehende COM- oder VB6 Applikationen mit
.NET Komponenten aufgerüstet werden sollen, müssen diese
für COM- Interop in der Registry verzeichnet werden. Dazu werden
in der Registry unter Prog- und Class- ID's der Speicherort der
Assembly mit den NET- Klassen beschrieben.
Wenn eine registrierte .NET- Klasse von einem COM-
Client instanziiert wird, dann instanziiert die CLR ein .NET Objekt.
Anhand der Metadaten der .NET Klasse wird für das .NET Objekt
(.NET Server) ein Com Callable Wrapper (CCW) generiert.
Die COM- Clients können
über eine gewöhnlichen Schnittstellenzeiger auf den CCW
zugreifen, wie auf jedes andere COM- Objekt. Der CCW ist ein Proxy,
der jeden Methodenaufruf an das .NET Objekt in der CLR weiterleitet.
Klassenschnittstellen
Der CCW wird von der CLR aus den Metadaten der
Klasse automatisch erzeugt. Diese Metadaten werden auch als
Klassenschnittstelle bezeichnet. Die Klassenschnittstelle kann
vom Entwickler beeinflusst werden mittels des Attributes
ClassInterfaceType:
<ClassInterface(ClassInterfaceType.None|AutoDispatch|AutoDual)>
Public Class DirTreeVb
'…
End Class
Wird ClassInterfaceType.AutoDispatch oder AutoDual
eingestellt, dann erzeugt die CLR den CCW aus den aktuellen Metadaten
der Klassenimplementierung.
Wird hingegen ClassInterfaceType.None
eingestellt, dann wird der CCW aus
den Metadaten der Schnittstellen gewonnen, die die .NET Klasse
explizit implementiert. Dies ist die empfohlene Einstellung, da so
die Implementierungsdetails der .NET Klasse vor den COM- Clients
verborgen wird. Dies fördert die Stabilität im Mix
COM/.NET, da früh bindende COM- Clients nicht mehr sicher laufen
können, wenn die Klassenimplementierung geändert wird.
Schnittstellen für
Ereignissenken
COM und .NET
unterscheiden sich bei der Implementierung von Ereignissen und
Ereignishandler.
In .NET sind Events
Objekte, deren Typ von der System.Delegate Klasse abgeleitet ist.
Diese verwalten die Einsprungpunkte der Eventhandler.
In COM werden
Events durch Objekte mit der Schnittstelle
IConnectionPoint
implementiert. Diese verwalten
Schnittstellenzeiger vom Typ ISinkEvents. Dieser
Mechanismus wird auch als Verbindungspunkt bezeichnet.
Um COM-
Eventhandler an .NET Events zu binden, muß ein COM
Verbindungspunkt im .NET Objekt eingerichtet werden. Dies erfolgt
durch das Attribut
ComSourceInterfaces
:
<ClassInterface(ClassInterfaceType.None), _
ComSourceInterfaces(GetType(Schnittstellenname)), _
ProgId("VBKurs.DirTreeVb")> _
Public Class DirTreeVb
Implements IDirTreeVb
'…
End Class
Mittels ComSourceInterfaces können bis zu vier Schnittstellen
für COM- Ereignissenken definiert werden. Auch die
Schnittstellen der Ereignissenken werden in .NET definiert. Um sie in
der COM- Welt identifizierbar zu machen, muss ihnen jeweils eine GUID
zugewiesen werden:
Imports System.Runtime.InteropServices
' Com Event Sink Schnittstelle definieren
' Durch diese Schnittstelle können Com- Clients Eventhandler registrieren für die DirTreeVb
' Ereignisse
<Guid("8AAE6562-EA03-410b-9D4F-CF42CF0CCDB0"), InterfaceType(ComInterfaceType.InterfaceIsIDispatch)> _
Public Interface DirTreeVbEvents
Sub EventProgressCom(ByVal CountDirs As Integer, ByVal CountFiles As Integer)
Sub EventEndScanDirCom(ByVal CountDirs As Integer, ByVal CountFiles As Integer)
End Interface
Insgesamt ergibt sich folgendes
Schema für die Implementierung einer .NET Klasse, die auch von
COM Clients nutzbar ist:
Beispiel:
' Empfohlene Vorgehensweise: Automatische Generierung der Com- Schnittstelle abschalten
' Stattdessen explizite Implementierung einer Verwalteten Schnittstelle, die als Vorlage
' für die ComSchnittstelle dient
' Über ComSourceInterface wird die Schnittstelle der Ereignisquelle für die Com- Automatisierung
' definiert.
<ClassInterface(ClassInterfaceType.None), _
ComSourceInterfaces(GetType(DirTreeVbEvents)), _
ProgId("VBKurs.DirTreeVb")> _
Public Class DirTreeVb
Implements IDirTreeVb
'-----------------------------------------------------------------------------
' Member zur Ausgabe des Arbeitsfortschrittes
' Ereignis: Arbeitsfortschritt
Public Event EventProgressCom As DGEventProgressComClient
' Generator für Arbeitsfortschrittmeldungen: Kann in abgeleiteten Klassen
' überschrieben werden, um detailiertere Arbeitsfortschrittmeldungen, die von
' DirTreeProgressInfo abgeleitet sind, zu erzeugen
Protected Overridable Function MakeProgressInfo() As DirTreeProgressInfo
Return New DirTreeProgressInfo(m_dir_count, m_file_count)
End Function
'Ereignis: Scan beendet
Public Event EventEndScanDirCom As DGEventProgressComClient
'-----------------------------------------------------------------------------------
' Konstruktoren
' Defaultkonstruktor. Wurde zwecks Com- Interoperabilität hinzugefügt
Private logHnd As mko.SystemEventLogHnd
Public Sub New()
log = New mko.CLogVb
' ...
End Sub
' Routine, die den rekursiven Dateibaumdurchlauf in einem gesicherten Kontext startet
Public Function scanDir(ByVal root_path As String) As Boolean Implements IDirTreeVb.scanDir
' ...
End Function
End Class
Einschränkungen
Klassen, die einen CCW- erhalten sollen, müssen
folgende Einschränkungen genügen:
sie müssen Public sein
sie müssen
einen parameterlosen Konstruktor besitzen (Sub New() in
VB.NET)
sie müssen
über öffentliche Eigenschaften und Methoden verfügen
Assemblies mit COM- sichtbaren Klassen müssen:
mittels dem Tool regasm registriert
werden
optional: signiert werden (snk)
optional: Installation im GAC (Achtung:
Registrierungsschlüssel mit Angabe des Speicherortes der
Assembly nicht vergessen)
Registrierung
Die Registrierung kann in Visual Studio 2005 ab
der Professional- Version automatisch erfolgen, indem die
Projekteinstellung Kompilieren/Für Com Interop registrieren
gesetzt wird.
Alternativ kann die
Registrierung einer CCW- Klasse auch mittels dem Tool regasm.exe
erfolgen. Diese bietet sich auch an, wenn die bei der
Registrierung vorgenommenen Einstellungen schnell geprüft werden
sollen, indem diese in eine Textdatei geschrieben werden:
//Fall 1: Registrieren, wird aber später im GAC installiert
c:\..\regasm MyComLib.dll
//Fall 2: Registrieren und definieren des Speicherortes. Kann anschliessend sofort verwendet werden
c:\..\regasm MyComLib.dll /codebase
//Fall 3: Wie Fall 2, jedoch sind alle Registrierungseinträge in eine Datei geschrieben worden zwecks Analyse
// wenn Anschließend in den GAC
c:\..\regasm MyComLib.dll /regfile:MyComLib.reg
// oder ohne GAC
c:\..\regasm MyComLib.dll /codebase /regfile:MyComLib.reg
Bei der Registrierung werden die CCW- Klassen einer sog. ProgID
zugeordnet. Diese besteht aus zwei Teilen, die durch einen Punkt
getrennt sind. Der erste Teil ist der Namespace, und der zweite der
Klassenname.
ProgID :<=> Namespace.Classname
Die Klassen selber sind z.B. unter VB6 im Objektbrowser unter einem
Namespace=Name der Assembly zu finden.