ja, lang lang ist's her, aber mein pb-bock war zwischendurch auf unter null und ich hab selbst noch einiges ausprobieren müssen. da es hier nicht nur um code geht und ich da ein bischen mehr ausgeholt hab, ist das in den tuts besser aufgehoben. wahrscheinlich bin ich eh schon der siebzehnte, der hier darüber schreibt, aber was solls. ein tut über com von maestro höchstpersönlich gibts übrigens hier. was ich jetzt hier schreiben, ist das, was ich mir in der letzten zeit so angelesen habe. bitte mich nicht gleich zu steinigen, wenn nicht alles stimmt. bitte posten, damit ich die fehler korrigieren kann.ts-soft hat geschrieben:Ich freu mich schon auf das Beispiel
com ist keine einbahnstraße. trotzdem wird der haupteinsatz in der nutzung von vorgegebenen com-objekten liegen. deswegen spreche ich in nachfolgenden text immer von der seite des com-nutzers, außer wenn ich mich ausdrücklich auf den com-anbieter beziehe. viele punkte gelten natürlich für beide seiten. in codebeispielen lasse ich vieleicht einige aspekte weg. oft sind wenige codezeilen schneller verständlich, auch wenn diese in einem lauffähigen programm noch ergänzt gehören (z.b. mit sicherheitsabfragen).
Objekte allgemein
da ein com-objekt eigentlich nur ein normales objekt mit ein paar standards ist, sollte man die funktion eines objektes erst mal intus haben. wenn du dich mal ein bischen damit beschäftigst, ist es eigentlich gar nicht so kompliziert. das wichtigste dabei ist, dass deine vTable im objekt an erster stelle eingetragen ist und dass die methoden in der vTable und im interface die gleiche reihenfolge haben. ein beispiel von einem einfachen objekt von ts-soft findest du hier. hier erstellt ts-soft erst mal eine klasse, in zeile 46 ruft er die new-methode auf und erstellt aus der klasse ein neues objekt.
Com-Objekte
um evtl. missverständnissen vorzubeugen. com hat nichts mit einer bestimmten programmiersprache, nichts mit oop, nichts mit windows und nichts mit microsoft zu tun. com ist ein festgelegter standard, der auf allen maschinen funktioniert, die mit 0 und 1 umgehen können. com hat in der grundform relativ einfache regeln. in verschiedenen programmiersprachen gibt es aufsätze, die das com handling stark vereinfachen. in pb fehlt dazu leider die native unterstützung und da werden wir auch noch warten bis wir schwarz sind. trotzdem ist es bis zu einem gewissen grad relativ einfach, com auch in pb zu nutzen.
Arten des Zugriffes
grundsätzlich muss man zwischen zwei arten des zugriffes auf die com-methoden unterscheiden. auf manche com-objekte ist der zugriff nur mit einer der beiden möglichkeiten gegeben. dies ist davon abhängig, ob das interface komplett bekannt ist und ob das interface IDispatch (siehe nächster punkt) implementiert ist. im idealfall ist das com-objekt so ausgelegt, dass beide zugriffsarten zulässig sind.
- Frühe Bindung (statische Bindung, Early Binding, VTable Binding)
im prinzip geht hier nur darum, den richtigen einsprungspunkt für die methoden zu bekommen. das läuft wie in diesem beispiel und wenn das interface bekannt ist, kann auf die gleiche weise auf jedes com-objekt zugegriffen werden. wie man in diesem fall zum objekt kommt, da lasse ich mich später noch aus. da mit dem interface und dem objekt bereits zur kompilierungszeit alle einsprungspunkte bekannt sind, wird bei der kompilierung bereits alles aufgelöst und die exe braucht von einem interface gar nichts mehr zu wissen. leider ist der geschwindigkeitsvorteil nicht sooo gravierend, da der prozessübergreifende speicherzugriff das meiste an zeit wegfrisst. in einer dll sollte der zeitvorteil wieder größer sein, wenn alles in einem prozess abgehandelt wird. der zugriff auf diese art ist in pb am einfachsten zu verwirklichen und ist mit ein paar codezeile erstellt.
vorteile:
- einfach zu erstellen
- schneller zugriff
nachteile:
- bei einer neuen version des com-objektes könnte sich das interface ändern (nach meiner meinung zu vernachlässigen, da ein ordentlicher programmierer neue methoden einfach hinten anhängt)
- scriptsprachen können nicht mit früher bindung auf com-objekte zugreifen, da eben nichts kompiliert wird (ist aber nur als com-anbieter interessant)
- funktioniert nicht immer (hat zumindest bei mir manchmal probleme gemacht)
beispiel vb6:
Dim oWorksheet As Excel.Worksheet
mit der definition Worksheet kann vb das interface auslesen und damit bereits zur compilierungszeit auflösen
bemerkung:
dass der zugriff mit früher bindung nicht immer funktioniert, liegt evtl. noch an einer microsoft eigenheit. da haben die ganze arbeit geleistet und zur allgemeinen verwirrung im objektbrowser schnittstellen zu klassen upgegraded, was sie aber definitiv nicht sind. bin noch nicht dazugekommen mir das genauer anzuschauen ob es daran liegen könnte.
- Späte Bindung (dynamische Bindung, Late Binding, Automation)
in gewissen situationen muss man auf ein objekt zugreifen, ohne zu wissen was einem entgegenkommt. ein beispiel wäre die pb-ide. wenn ich das objekt jedes tabs abfragen könnte, wüßte ich noch immer nicht, was hinter diesem objekt steckt. es könnte ein code sein, es könnte aber auch ein projekt sein. in einem solchen fall gibt es immer eine gemeinsame methode, die z.b. TabTyp heißen könnte. daraus frage ich dann ab, ob es ein code oder projekt ist und weiß damit, welche weiteren methoden mir zur verfügung stehen. wie man in diesem fall auf die methoden zugreifen kann, darauf komm ich später zurück.
vorteile:
- flexibler bei einer neuen version des com-objektes
- interface muss bei programmerstellung nicht bekannt sein
Nachteile:
- aufwändiger code erforderlich
- langsamer zugriff
beispiel vb6:
Dim oWorksheet As Object
zur kompilierungszeit ist nichts anderes bekannt, als dass es sich um ein objekt handelt.
bemerkung:
selbst bei einem unbekanntem objekt sollte es mit einem rumpfobjekt möglich sein auf die gemeinsamen methoden mittels früher bindung zuzugreifen und dann mit dem ergebnis das richtige interface zu benutzen. noch nicht probiert, sollte in den meisten fällen aber funktionieren.
com bietet eine vielzahl von standardinterfaces, wovon zwei aber besonders hervorgehoben werden müssen, weil ohne die beiden in com gar nichts geht.
- IUnknown
jedes com-objekt hat als mindestvoraussetzung die methoden dieses interfaces zu implementieren und IUnknown ist quasi die mutter aller com-objekte. wenn die drei methoden von IUnknown implementiert sind, ist ein einfaches com-objekt schon fertig. es steht nirgends geschrieben, dass ein com-objekt registriert sein muss, ein interfaceId oder weiteren schnickschnack haben muss. wenn dann noch ein paar zusätzliche methoden integriert werden, kann das com-objekt schon verwendet werden. IUnknown enthält folgende drei methoden:
wenn nur die methoden von IUnknown implementiert sind, kann auf das com-objekt nur mit früher bindung zugegriffen werden.Code: Alles auswählen
Interface IUnknown QueryInterface(a, b) AddRef() Release() EndInterface
QueryInterface aus sicht des com-nutzers:
ein com-objekt kann nicht nur selbst methoden implementiert haben, die klasse kann auch noch weitere schnittstellen zur verfügung stellen. mit QueryInterface können diese abgefragt werden.
AddRef/Release aus sicht des com-nutzers:
mit diesen beiden methoden wird der referenzzähle des objektes beeinflusst. der referenzzähler ist für die lebendsdauer des com-objektes zuständig. dieser beinhaltet immer die anzahl, wie oft ein zeiger auf das objekt (oder auf eine weitere schnittstelle der klasse) gespeichert worden ist. sobald dieser einmal auf 0 geht, zerstört sich das objekt selbst. in den meisten fällen braucht durch den nutzer nichts gemacht werden. sollte jedoch ein zeiger auf dieses com-objekt kopiert oder gelöscht werden, dann weiß das com-objekt ja nichts davon und AddRef/Release ist vom benutzer auszulösen. AddRef/Release gibt den wert des referenzzählers zurück. wenn man im entwicklungsstadium diesen wert mal zur kontrolle wissen will, dann immer in der reihenfolge objekt\AddRef() : Debug objekt\Release() abfragen. umgekehrt wäre das ein fall für schrödingers katze, wenn der referenzzähler bereits auf 1 steht.
die kontrolle über den referenzzähler zu behalten ist sehr wichtig, damit das com-objekt sich auch selbst zerstören kann. sonst bleibt das com-objekt bestehen und die anwendung des com-anbieters wird nicht beendet. bei abfrage des referenzzählers ist zu beachten, dass schnittstellen oft den referenzzähler der klasse benützen, obwohl die abfrage in der schnittstelle erfolgt ist. also nicht gleich verzweifeln, wenn der referenzzähler einer schnittstelle nicht auf 0 geht. erst wenn alle referenzen auf die schnittstellen der klasse und auf die klasse selbst entfernt sind, dann wird der referenzzähler auch auf 0 gehen.
ich habe schon öfter gesehen, dass der referenzzähler z.b. nach einem QueryInterface sofort wieder released wird. dies halte ich für keine gute praxis und geht vollkommen an der idee des referenzzählers vorbei. erst wenn der verweis aus der objektvariablen gelöscht wird, dann sollte auch das objekt released werden. wenn man das konsequent durchzieht, dann lässt sich das auch gut handeln und gibt weniger probleme, als wenn ich einen teil gleich und einen teil später released. der referenzzähler soll ja wirklich darstellen, wie oft noch ein verweis auf das objekt existiert.
QueryInterface aus sicht des com-anbieters:
dazu ist jetzt eine interfaceId (IID) notwendig. das heißt noch immer nicht, dass das com-objekt registriert sein muss. die IID muss lediglich dem anbieter und dem nutzer bekannt sein. in einfachen fällen genügt auch die IID_IUnknown alleine.da passiert jetzt folgendes: der nutzer des com-objektes fragt mit der beiden bekannten IID über QueryInterface an, ob es sich um das gewünschte objekt handelt. Wenn QueryInterface nun feststellt, yeah, thats me, dann gibt es im procedurparameter einen zeiger auf das eigene objekt zurück. da davon ausgegangen werden kann, dass der nutzer diesen zeiger auch speichert, wird der referenzzähler gleich um eins erhöht. von der prozedur wird #S_OK zurückgegeben. das gleiche passiert, wenn der nutzer mit der IID_IUnknown anfragt. Wenn mit einer unbekannten IID angefragt wird, wird einfach #E_NOINTERFACE zurückgegeben.Code: Alles auswählen
Procedure.l QueryInterface (*This.cSH_Site, *iid.IID, *Object.Integer) Define *Me.cSH = *This\Me ;Standardzuweisungen auf eigenes Objekt If CompareMemory(*iid, *This\IID, 16) Or CompareMemory(*iid, ?IID_IUnknown, 16) *Object\i = *This *This\cntRef + 1 ProcedureReturn #S_OK EndIf ;Unbekanntes Interface *Object\i = 0 ProcedureReturn #E_NOINTERFACE EndProcedure
wie schon geschrieben, kann eine klasse ja nicht nur eigene methoden zur verfügung stellen, sondern auch noch eigene schnittstellen beinhalten. dann führt aber kein weg mehr daran vorbei, jede coklasse braucht eine eigene IID, die beiden nutzern bekannt sein muss. obiger code wäre dann noch z.b. wie folgt zu ergänzen:in diesem beispiel passiert folgendes: der nutzer des com-objektes fragt mit dem IID_IActiveScriptSiteWindow bei QueryInterface nach, ob es sich um das gewünschte objekt handelt. QueryInterface hat im ersten vergleich aber festgestellt, dass es sich nicht um das eigene com-objekt handelt. also: ne du, das bin nicht ich, aber ich weiß was. QueryInterface erstellt in diesem fall (falls noch nicht vorhanden) das gewünschte schnittstellenobjekt und gibt einen objektzeiger darauf über den prozedurparameter zurück.Code: Alles auswählen
;SiteWindow Objekt zuweisen (wenn nicht vorhanden, dann erstellen) If CompareMemory(*iid, ?IID_IActiveScriptSiteWindow, 16) If *Me\oActiveScriptSiteWindow = 0 *Me\oActiveScriptSiteWindow = cSH_SiteWindow_New(*Me) *Me\oActiveScriptSiteWindow\AddRef() EndIf *Object\i = *Me\oActiveScriptSiteWindow *Me\oActiveScriptSiteWindow\AddRef() ProcedureReturn #S_OK EndIf
- IDispatch
IDispatch ist eine erweiterung von IUnknown und enthält vier methoden mit denen mit später bindung auf die methoden des com-objektes zugegriffen werden kann. wenn das interface bekannt ist, sollte man mit früher und später bindung auf das objekt zugreifen können. nur nebenbei bemerkt. wenn das com-objekt IDispatchEx unterstützt, dann sollte man diese methoden verwenden, da die leichter zum handeln sind.
die ersten beiden methoden sind zum zugriff auf die methoden nicht unbedingt erforderlich. der zugriff erfolgt in zwei schritten mit den methoden GetIDsOfNames und Invoke.Code: Alles auswählen
Interface IDispatch Extends IUnknown GetTypeInfoCount(a) GetTypeInfo(a, b, c) GetIDsOfNames(a, b, c, d, e) Invoke(a, b, c, d, e, f, g, h) EndInterface
GetIDsOfNames / Invoke aus sicht des com-nutzers:
diese beiden methoden werden benötigt um auf das die einzelnen funktionen des com-objektes mit später bindung zugreifen zu können. zuerst wird mit GetIDsOfNames mittels des methodennames die DispId ermittelt. aus performancegründen sollte diese nur einmal abgefragt und für spätere zugriffe zwischengespeichert werden.
normalerweise ist es ja so, dass immer die anwendung, die einen speicher reserviert hat, diesen auch wieder frei geben muss. bei com verhält sich das ein bischen anders. grundsätzlich sind bStrings mit der api SysAllocString zu erstellen und mit der api SysFreeString wieder freizugeben. erst dadurch ist gewährleistet, dass jeder der beiden an com beteiligten partner den speicher auch wieder frei geben kann.
in beispiel 1 weiter unten wird von excel ein bstring mit dem titel übergeben. dazu sollte man sich immer überlegen, gibt es für den com-partner irgendeinen grund, irgendein ereignis wann er seinen bstring wieder frei geben sollte. in dem beispiel ist eigentlich vollkommen klar, es gibt keinen. excel sagt sich nicht, heute ist mittwoch, es ist 17:43, draußen scheint die sonne und weil ich grad lust und laune habe, gebe ich den speicher wieder frei. also kann es nur die aufgabe des empfängers sein, dass er den bstring wieder frei gibt, auch wenn er ihn nicht allociert hat.
gerade bei diesen bstrings sollte man fürchterlich aufpassen, dass diese ordentlich freigegeben werden. da die bstrings durch eine api reserviert werden, werden sie weder durch beenden des eigenen pb-programmes noch durch beenden des partnerprogrammes frei gegeben. da feiern die bstrings speicherparty bis zum abwinken, d.h. bis zum ausschalten des computers.
wenn der bstring in einer variant-struktur steht, kann auch die api VariantClear verwendet werden. bei windowseigenen methoden, die im msdn dokumentiert sind, steht meistens auch dabei, wer für die freigabe zuständig ist.
bei variants könnte das gleiche wie bei den bstrings gelten. dort ist es aber meistens so, dass der aufrufer einen varianttyp zur verfügung stellen muss und der partner schreibt dann was rein. safearrays könnten auch noch in dieses kapitel fallen, hab ich mich aber noch nicht damit beschäftigt.
Wie kommt man zu den Interfaces und IID's
ich verwende den OLE/COM Interfacegenerator von Stefan Moebius. den gibts hier zum downloaden. den interfacegenerator starten, dann z.b. die datei Excel.exe auswählen, auf create drücken und nach ein paar sekunden ist alles fertig. das ganze dann als pbi datei speichern und mit einem XIncludeFile in die anwendung einbinden.
leider hat das teil einen großen nachteil. da es fast in jeder anwendung ein interface mit dem namen 'Application' gibt, kommt es schnell mal zu problemen. es wäre hilfreich, wenn man einen string eingeben könnte, der jedem interface sozusagen als namespace vorangestellt wird. ich empfehle auf jeden fall, bei den optionen die möglichkeit Add Compiler directives auszuschalten. wenn es überschneidungen in der benamsung gibt, dann soll es gleich beim compilieren ordentlich krachen und nicht, dass dann für excel das applicationinterface aus word verwendet wird, nur weil dieses zufällig vorher erstellt wurde.
ach ja, hätte ich fast vergessen. der OLE/COM interfacegenerator leidet auch ein bischen alzheimer und vergiss schon mal einen parameter. dann kommt noch dazu, dass der interfacegenerator auch die dispinterfaces und die iid's für die dispinterfaces erzeugt, obwohl die eigentlich vollkommen nutzlos und nur verwirrend sind. wenn mal gar nichts funktioniert, dann am besten immer einen anderen viewer zum vergleichen bereit halten. ich verwende den oleview.exe von microsoft. den gibts hier zum downloaden.
wenn es sich nur um einzelne und kleinere interfaces handelt, die im msdn beschrieben sind, nicht umscheissen und lange suchen woraus die generiert werden können. es ist schneller die einfach selber zu schreiben schreiben.
Warum es erforderlich sein kann, selbst com-objekte anzubieten
einige anwendungen erfordern, dass man selbst ein com-objekt erstellt, das dann benutzt werden kann um die fremdanwendung zu steuern. so sind zum beispiel beim erstellen eines scripthostes einige com-objekte mit etlichen schnittstellen zu erstellen. über diese objekte wird dann die ganze kommunikation z.b. für fehler, skriptabbruch und debugging durchgeführt. allerdings handelt es sich hier um lauter objekte mit früher bindung, die on the fly erstellt werden können. ein weiteres beispiel ist dieser code von freak, wo er auch schnell mal das com-objekt NewSink erstellt.
Tipp zum erforschen von Klassen
wenn ihr mal eine com-anwendung selber erstellt oder gezwungen seid ein com-objekt selber zu erstellen, dann schreibt ihr ja in QueryInterface erst mal die standardzuweisungen auf euer eigenes objekt. schickt aber keinesfalls dann alles andere gleich in den nirvana. macht für euch eine message, dass ein unbekanntes interface angefragt wurde. da kommen oft sachen zutage, von denen ihr noch nie was gehört habt und momentan auch nicht wisst, was damit anzufangen ist. sucht in der registry oder im internet nach der iid und legt diese an. dann macht für jedes dieser unbekannten interfaces in QueryInterface einen eigenen vergleich und schickt es dann einzeln in den nirvana.
irgendwann kommt ihr dann wahrscheinlich bei euerem projekt auch auf den punkt, wo zwar alles ohne fehler läuft, ihr euch aber fragt 'was nun?'. mit einem blick auf die noch nicht implementierten schnittstellen kommt dann oft der große AHA effekt.
COMate
im zusammenhang von pb und com muss auf jeden fall auch noch COMate erwähnt werden. COMate arbeitet mit später bindung und hat dafür auch die gleichen vor- und nachteile. bei vielen gleichen zugriffen muss auf jeden fall mit COMate_PrepareStatement gearbeitet werden, sonst kommen unerträgliche laufzeiten zustande.
DCom
nur der vollständigkeitshalber sei hier noch DCom erwähnt. hier handelt es sich um eine com schnittstelle, die nicht nur über die prozessgrenzen sondern auch über die rechnergrenzen hinaus verwendet werden kann. ich hatte vor einiger zeit das zweifelhafte vergnügen, auf der seite eines unserer forumsmitglieder einen virus auszufassen, der mit DCom gearbeitet hat. nachdem ich den DCom dienst abwürgen konnte, konnte ich wieder normal arbeiten. das einzige was nicht funktioniert hat, waren die standard internetspiele wie reversi und backgammon. ergo: DCom ist für windowsspiele und viren zuständig
Beispiele und Laufzeiten
in den nachfolgenden beiträgen habe ich ein paar beispiele erstellt. generell sollen die beispiele eher einen rahmen zeigen. die beispiele sind sehr einfach und geradlinig programmiert. gerade bei der späten bindung kann durch ein paar prozeduren etliches an arbeit wegfallen. die meisten beispiele sind so nicht feldtauglich, da nur die allernotwendigsten sicherheitsabfragen eingebaut sind.
bitte alle beispiele mit unicodeunterstützung abspeichern
- Beispiele 1-3
beispiel zum lesen/setzen von ein paar attribute und verwenden eines enumobjektes in excel. die vergleichszeiten für 10k durchläufe sehen bei mir folgendermaßen aus:
- frühe bindung: 1.234 ms
- späte bindung: 1.796 ms
- mit comate: 3.547 ms
- Beispiele 4+5
hier habe ich ein selbstregistrierende com-dll mit dualinterface erstellt. die vergleichszeiten sehen bei mir für 1 mio durchläufe folgendermaßen aus:
- frühe bindung: 47 ms
- vba: 1.218 ms
- vbs: 2.546 ms
- comate: 2.969 ms