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.
und jetzt das ganze noch verbinden mit
LateLoad
#define LATELOAD_FUNC_0(ReturnType,FuncName) \
public: \
ReturnType FuncName() \
{\
static CDispIdHolder dispid(m_lpDispatch,#FuncName); \
return InvokeHelper(dispid, DISPATCH_METHOD, VT_EMPTY, NULL, NULL); \
}
und man kann sich die klasse via makro in einem header only bauen…
LATELOAD_BEGIN_CLASS(BlubWrapper,com_class) // noch umzusetzen
LATELOAD_FUNC_0(HRESULT,DoSomething)
LATELOAD_END_CLASS()