C++ProgrammierenWindows APIMartin Richter - So 03 Aug 2014 21:07

Ich habe ein Programm um COM-Automation erweitert. Der Test verlief super. Das Programm lief stand alone oder wurde über die Automation (CoCreateInstance) gestartet. Das Programm wurde im Installer integriert und ab da ging erst mal nichts mehr.

Wurde der externe COM Server über die Automation gestartet wurden auf einmal Satelite-DLLs nicht mehr gefunden. Das Programm ermittelte mit GetModuleFileName den Programmnamen. Ergänzte DEU zum Beispiel für die Deutsche Programmversion und suchte eine entsprechende DLL. Eigentlich ganz einfach. Das funktionierte aber nicht, weil GetModuleFileName den kurzen Dateinamen zurück gab. Also: aus PROGRAMM.EXE wurde PROGRA~1.EXE und Die Datei PROGRA~1DEU.DLL wurde gesucht, anstatt PROGRAMMDEU.DLL. Aber dann nicht gefunden…

Der Installer erzeugt scheinbar bei mir kurze Dateinamen in der Registrierung und auch MsiGetComponentPath, dass beim Start über CoCreateInstance konsultiert wurde, lieferte den kurzen Dateinamen.

Wirklich erstaunlich ist aber, dass der Name der in CreateProcess verwendet wurde auch später durch GetModuleFileName zurückgegeben wird. Wird also der kurze Dateiname beim Start verwendet, dann liefert GetModuleFileName einen kurzen Dateinamen.

Grundsätzlich kann man also nicht davon ausgehen, dass GetModuleFileName den langen Dateinamen zurück liefert.

C++MFCProgrammierenWindows APIMartin Richter - Mo 02 Apr 2012 21:46

Wenn man ein COM Objekt erzeugt und dieses im System über die ROT (Running Object Table) sichtbar, dann sollte man normalerweise Weak-Locks benutzen. Das kann man auch in der Doku zu RegisterActiveObject  nachlesen. Ansonsten wird es schwierig zu entscheinden, wann man seine Objekte zerstören kann.

Wenn aber nun eine Anwendung sichtbar gemacht wird, also das Objekt vom Benutzer übernommen wird, dann darf es ja nicht beendet werden, wenn der externe Erzeugende Prozess beendet wird und die letzte Referenz zu dem Objekt beendet wird.

Wie verhindert man das?

Die Lösung ist relativ simpel. Solange die Anwendung sichtbar ist, oder besser, wenn sie sichtbar wird ruft man einmalig CoLockObjectExternal auf! Dadurch wird ein weitere Lock auf das Objekt ausgeführt.
Aber Achtung ❗ Hier wird keine Referenzzählung verwendet. Egal wie oft man CoLockObjectExternal aufruft, der Referenzzähler wird nur einmal erhöht.

Beendet der User das Programm entsperrt man das Objekt wieder. Sollten keine weiteren Objekte in der Anwendung benutzt werden, dann terminiert die Anwendung wenn man alles richtig gemacht hat 😉
Man ruft CoLockObjectExternal am Besten entweder auf, wenn die Anwendung sichtbar wird (WM_SHOWWINDOW) und erneut wenn WM_CLOSE aufgerufen wird. Die MFC macht alles fast automatisch richtig, bis eben auf die Aufrufe von CoLockObjectExternal, die man selbst im Code unterbringen muss, wie auch die Registrierung der Objekte in der ROT.
Ist noch eine externe Referenz vorhanden wird die Anwendung nicht terminiert, weil der interne Objektzähler der MFC dies verhindert (Code in CFrameWnd::OnClose). Ist kein externer Lock mehr vorhanden sperrt CoLockObjectExternal die Anwendung vom terminieren weil damit exakt eine Referenz aufrecht erhalten wird. Wird durch den Benutzer das Schließen der Anwednung angefordert wird dann im WM_CLOSE diese letzte Refrenz aufgelöst und die Anwednung kann terminieren auch wnen noch ein Eintrag in der ROT vorhanden ist. Dieser wird dann beim Beenden der Applikation auch entfernt.

ATLMFCProgrammierenMartin Richter - Do 30 Apr 2009 20:50

Immer wieder mal, taucht MFC Code auf und es wird CoInitializeEx verwendet.
Nicht unbedingt ein Fehler, aber ich denke man sollte auch hier die MFC-Art-und-Weise verwenden: AfxOleInit. Und man sollte sich klar sein, welche Apartment Modes man verwenden darf!

AfxOleInit selbst ist ein per Thread Wrapper für ::OleInitialize(NULL);.
Zudem sorgt AfxOleInit dafür, dass bei Beenden des Threads automatisch AfxOleTerm aufgerufen wird.

::OleInitialize(NULL) wiederum ist ein Wrapper für den Aufruf von CoInitializeEx im STA Kontext:
Siehe MSDN http://msdn.microsoft.com/en-us/library/ms690134(VS.85).aspx 

OleInitialize calls CoInitializeEx internally to initialize the COM library on the current apartment. Because OLE operations are not thread-safe, OleInitialize specifies the concurrency model as single-thread apartment.

Die MFC benutzt und verlangt genau diesen STA Modus. Sofern man also nicht wirklich weiß was man mit CoInitializeEx macht, sollte man es in der MFC vermeiden. Zudem man sich eben auch nicht mehr um den Cleanup kümmern muss, da die MFC dies selber macht.

Siehe auch MSDN Doku zu AfxOleInit
http://msdn.microsoft.com/de-de/library/e91aseaz(VS.80).aspx

MFC applications must be initialized as single threaded apartment (STA). If you call CoInitializeEx in your InitInstance override, specify COINIT_APARTMENTTHREADED (rather than COINIT_MULTITHREADED). For more information, see PRB: MFC Application Stops Responding When You Initialize the Application as a Multithreaded Apartment (828643) at http://support.microsoft.com/default.aspx?scid=kb;en-us;828643.

Aber auch hier sei erwähnt, dass man sicherlich auch andere Apartment Modes verwenden kann. Allerdings muss man dann wirklich wissen was man macht und darf sich nicht auf die MFC Funktionen stützen. Für solche Fälle verwende ich hier komplett ATL in meinen MFC Programmen.
Man sollte aber in diesen Fällen wirklich wissen was man macht 😉

BTW: Automatisch aufgeräumt bei einem gestarteten Thread wird nur dann, wenn auch AfxBeginThread verwendet wird. Siehe auch: AfxBeginThread versus _beginthreadex

ATLProgrammierenSonstigesWindows APIMartin Richter - Mi 10 Dez 2008 19:54

Ich benutze als interne Makrosprache gerne VBScript. D.h. ich hoste VBScript.
Ich biete dann bestimmte COM-Objekte (IDisptach) an, die es dem Nutzer erlauben mein  Programm anzupassen oder intern zu steuern.

Nun macht es aber unter Umständen einen gravierenden Unterschied, ob ein COM-Objekt von einem programminternen Nutzer, oder von extern angesprochen wird. Intern möchte ich zum Beispiel mehr  oder andere Funktionen erlauben als durch einen externen Zugriff von einem anderen Programm oder Skript.

Wie kann man aber unterscheiden von wo ein COM Zugriff erfolgt ❓

Netterweise hat COM die Funktion CoGetCallContext, die mir genau diese Info liefert. Liefert CoGetCallContext S_OK und damit einen Interfacezeiger auf IServerSecurity, dann wurde der COM Zugriff von einem externen Prozess abgesetzt. Wenn CoGetCallObject E_NOINTERFACE zurück gibt haben wir einen internen Aufruf aus dem eigenen Prozess heraus.

bool IsExternalComCall()
{
 // get context
 CComPtr<IServerSecurity> spSecurity;
 HRESULT hr = ::CoGetCallContext(IID_IServerSecurity,
                      reinterpret_cast<void**>(&spSecurity));
 return SUCCEEDED(hr);
}

Herzlichen Dank an René König für diesen Ansatz, den er mir in microsoft.public.de.vc gab.

C++ProgrammierenSonstigesWindows APIMartin Richter - So 03 Aug 2008 20:04

Nicht wenige verwenden, wie ich auch COM Komponenten, aus dem eigenen Haus oder von Fremdherstellern. Eingebunden werden diese COM-Komponenten oft genug über das #import Statement, das ja eine wirklich simple Integration erlaubt.

Lästig ist nur, dass diese Komponenten nicht auf allen Rechnern in den selben Verzeichnissen liegen. Das macht es nicht leicht Projekte und Entwicklungmaschinen so auszustatten, dass alle Projekte gleich zu kompilieren sind. Da fängt es schon an, dass ein Entwickler ein englisches OS (C:\Program Files), ein andere ein deutsches (C:\Programme) und der dritte Entwickler benutzt das Installationverzeichnis der zu entwickelnden Komponente (C:\Dev\Project\Bin).

Aus diesem Grund bin ich dazu übergangen das #import Statement nur einmal auszuführen, und die entstehenden .tlh und .tli Dateien direkt in das Projekt aufzunehmen.
Zu schnell? OK, also schrittweise:

  • Ich binde die Komponente also wie gewohnt per #import ein.
  • Die entstehenden .tlh und evtl. auch die .tli Dateien werden in das Projektverzeichnis kopiert und in das Projekt aufgenommen.
  • Das das #import Statement wird nun auskommentiert und statt dessen entsprechende #include Statements eingesetzt.
  • Nachdem man das auch korrekt mit entsprechenden Versionsangaben der Komponente dokumentiert hat und auch dieses Verfahren in die Projektbeschreibung aufgenommen hat ist man fertig!

Vor- und Nachteile:

  • Nicht nur, dass dieses Projekt unabhängig kompiliert werden kann. Das Kompilieren ist auch noch schneller, denn die (an sich) statischen .tlh und .tli Dateien werden nicht immer neu erzeugt.
  • Vorrausetzung ist hier sicherlich, dass es sich hier um Komponenten handelt, deren Interface sich nicht mehr verändert, und man muss bei einem Update der Komponente natürlich auch manuell die neuen .tli und .tlh Dateien einbauen.
  • Ein weiterer Vorteil ist auch, dass das Interface fest eingebunden wird, das auch für die Auslieferung festgelegt wird und keine alte/oder neuere Version zu Überraschungen führt.
  • Was auch zu dem genialen Verhalten führt, dass man aus dem Sourcecontrol System eine Software Version erzeugen kann, die noch mit einer älteren/abweichenden COM Komponente erzeugt wurde ohne diese auch noch mal installieren zu müssen!
ATLC++MFCProgrammierenWindows APIMartin Richter - Sa 16 Feb 2008 14:45

Immer wieder sehe ich Entwickler, die über Late Binding COM Komponenten ansprechen. Das ist an sich nur zu unterstützen, denn DISPIDs können sich schnell mal ändern, wenn sich ein Interface ändert. Gerade bei den rasant an Verbreitung zunehmenden .NET Komponenten, die man mal schnell in die eigene Anwendung per COM einbindet, kann das Binding über die Interfaces und DISPIDs schnell zum Frust werden. Gleiches gilt für Office Komponenten, bei denen man sich nicht zwingend an ein Interface binden will. Zudem ist es der von Microsoft empfohlene Weg die Office-Automation zu benutzen (siehe auch hier).

OK! Also man nimmt Late Binding und verwendet z.B. die aktuellen Wrapper, die netterweise von der ATL zur Verfügung gestellt werden,  z.B. die netten Funktionen aus der CComDispatchDriver Klasse. GetPropertyByName, PutPropertyByName und auch die Funktionen Invoke0, Invoke1 und InvokeN haben entsprechende Überladungen, die die Verwendung von Funktions-/Eigenschaftsnamen direkt erlauben.

Der Nachteil liegt nicht gleich auf der Hand. Immer wenn solch eine Funktion aufgerufen wird, wird nicht durch der IDispatch::Involke ausgeführt, sondern auch ein Aufruf von IDispatch::GetIDsOfNames. Bei einem Out-Of-Process-Server kann dieser Roundtrip einiges an Performance kosten. Dabei ist es so einfach es besser zu machen.

Die Doku von IDispatch::GetIDsOfNames sagt folgendes:  

The member and parameter DISPIDs must remain constant for the lifetime of the object. This allows a client to obtain the DISPIDs once, and cache them for later use.

Und wer den oben genannten KB-Artikel aufmerksam gelesen hat, der hat auch was von DISPID-Caching mitbekommen.

Ich benutze gerne die die Wrapper, die in der MFC automatisch erzeugt werden, wenn man einen Type-Library über den Class View importiert. Hier werden die Funktionen auch in eine nette kleine COleDispatchDriver-Klasse verpackt und man bekommt netterweise ein gutes Exception Handling den Fehlerfall geliefert. Leider werden hier aber auch wieder nur DISPIDs verwendet.
Aber mit einem kleinen Trick, kann man diese Klassen genial einfach für Late Binding nutzen. Ich gehe wie folgt vor:

  • Ich importiere die Type-Library (tlb) mit dem Visual Studio Class View.
  • D.h. ich habe jetzt alle Wrapper mit DISPIDs, Was ich eigentlich für Late Binding vermeiden will.
  • Jetzt passe ich einfach die einzelnen Wrapper Funktionen in der folgenden Art und Weise an, ich ersetze die DISPID durch eine statische Variable:
void CMyWrapper::DoSomething(){
  static CDispIdHolder dispid(m_lpDispatch,L"DoSomething");
  InvokeHelper(dispid, DISPATCH_METHOD, VT_EMPTY, NULL, NULL);
}
  • Die kleine Klasse die ich hier verwende macht nun den Rest und besorgt die DISPID und cached sie damit weil die Variable statisch definiert ist:
class CDispIdHolder
{
public:
 CDispIdHolder(IDispatch *pDispatch,LPCOLESTR pName)
  : m_dispid(DISPID_UNKNOWN)
  {
   HRESULT hr = pDispatch->GetIDsOfNames(
         IID_NULL,
         &const_cast<LPOLESTR>(pName),
         1,
         LOCALE_SYSTEM_DEFAULT,
         &m_dispid);
   if (FAILED(hr))
    AfxThrowOleException(hr);
  }
  operator DISPID() { return m_dispid; }
private:
 DISPID m_dispid;
};

Die Exception, die man verwendet ist natürlich Implementierungsfrage.

❗ Der Performance Gain ist zum Teil beträchtlich, besonders wnen bestimmte Funktionen sehr oft aufgerufen werden müssen!

Anmerkung (für alle die es ganz genau nehmen):
Wenn man die Doku genau liest, heißt es natürlich hier, dass die Implementierung nur für die Lebenszeit des Objektes konstante DISPIDs garantiert. Wenn man allerdings bei Early Binding schon DISPIDs als konstant annimmt, ist meine Methode für Late Binding sicherlich vertretbar.

ATLC++ProgrammierenMartin Richter - Sa 02 Feb 2008 16:09

Eigentlich müsste dieser Artikel eine weitere Überschrift bekommen:
Wie fatal es ist, dass es keine vollständige ATL Dokumentation gibt!

Einige werden CComDispatchDriver kennen. Seit den VS-200x Versionen ist diese Klasse nichts anderes als sie Spezialisierung CComPtr<IDispatch>

Ich hatte vor, eine bestimmte .NET Komponente per late binding in ein C++ Programm zu integrieren.
Kein Problem. CreateInstance, hier ein Invoke0, dort ein Invoke2 und da noch ein InvokeN 😮 … und hier geht auf einmal nichts mehr. Dokumentation in der MSDN: Fehlanzeige!

Bestandsaufnahme: Ich habe einen CComVariant Array aufgebaut und die entsprechenden Daten übergeben. Sieht alles richtig aus. Aber ich erhalte immer nur Fehler (immer E_NOINTERFACE).

  1. OK, nächster Schritt. Das ganze mal direkt mit dem entsprechenden Interface versuchen. Also CComQIPtr eingebaut und direkter Aufruf über die existierende duale Schnittstelle. Komisch, nun geht es.
  2. Nun denn. Das ganze nun mal als Klasse mit dem Classview importiert und einen MFC Wrapper gebaut. Der verwendet ja auch IDispatch. Jetzt wird es mysteriös: Es geht auch!

Dann debuggen wir mal den MFC Wrapper. Schrittweise gehe ich durch den Code im COleDispatchDriver::InvokeHelperV bis Zeile 223 und dort lese ich den folgenden Code und Kommentar:

pArg += dispparams.cArgs - 1; // params go in opposite order

❗ Aha! Hier liegt der Hase im Pfeffer. Ich hatte den CComVariant Array natürlich so aufgebaut, dass das erste Argument auch das erste Element im Array war. InvokeN will aber die selbe Reihenfolge wie IDispatch::Invoke, d.h. in umgekehrter Folge. Also beginnt der Array nun mit dem letzten Argument und endet mit dem ersten und siehe da: Es geht ❗

Schade, dass hier die ATL-Doku so lückenhaft ist. Das hätte mir hier, 2 Stunden Arbeit gespart.
Ich hätte es auch schneller haben können, wenn ich mir aufmerksam die Implementierung von CComPtr<IDispatch>::Invoke2 angesehen hätte (Man achte auf den Array varArgs):

inline HRESULT CComPtr<IDispatch>::Invoke2(__in DISPID dispid, 
       __in VARIANT* pvarParam1, 
       __in VARIANT* pvarParam2, 
       __out_opt VARIANT* pvarRet) throw() 
{ 
 if(pvarParam1 == NULL || pvarParam2 == NULL) 
   return E_INVALIDARG; 
 CComVariant varArgs[2] = { *pvarParam2, *pvarParam1 }; 
 DISPPARAMS dispparams = { &varArgs[0], NULL, 2, 0}; 
 return p->Invoke(dispid, IID_NULL, LOCALE_USER_DEFAULT, 
     DISPATCH_METHOD, &dispparams, pvarRet, NULL, NULL); 
}