Henryk Budzisz ZPORR nr POKL.04.01.01-00-449/08-00 Koszalin 2009
Technologia COM Wprowadzenie COM Interfejsy Technologia COM w Visual C++ Globally Unique Identifiers (GUIDs) Biblioteka COM Tworzenie obiektu COM Inteface IUnknown Definiowanie komponentu nr POKL.04.01.01-00-449/08-00 2
COM co to jest? COM (Component Object Model) jest opracowaną przez Microsoft technologią umożliwiającą efektywną komunikację między aplikacjami Component Object Model definiuje: binarny standard wywoływania funkcji między komponentami, struktury interfejsów udostępnianych przez poszczególne obiekty, mechanizmy jednoznacznej identyfikacji komponentów i ich interfejsów. nr POKL.04.01.01-00-449/08-00 3
Diagram binarnego standard Klient Obiekt komponentowy (instancja komponentu) wskaźnik na VTBL (pole prywatne) wywoływania funkcji VTBL jest wspólna dla wszystkich klientów korzystających z danego komponentu COM Serwer wskaźnik na funkcję1 wskaźnik na funkcję2 wskaźnik na funkcję3 VTBL (tablica funkcji wirtualnych) funkcja1(pobj,arg1,arg2,...) {... } Implementacja komponentu COM zawarta zwykle w bibliotece DLL nr POKL.04.01.01-00-449/08-00 4
Komponenty COM w jednym procesie Technologia COM definiuje sposób współpracy pomiędzy komponentem a jego klientem. W pojedynczym procesie, klient łączy się z komponentem bez pośrednich komponentów. nr POKL.04.01.01-00-449/08-00 5
Komponenty COM w różnych procesach Klient, który chce się skomunikować z komponentem musi skorzystać z pośrednictwa tzw. RPC stubs oraz obiektów Proxy, istniejących po obu stronach połączenia. nr POKL.04.01.01-00-449/08-00 6
Komponenty COM na różnych komputerach Jeżeli klient i komponent rezydują na różnych komputerach, w technologii DCOM (Distributed COM) komunikację międzyprocesorową zastępuje się protokołem sieciowym. nr POKL.04.01.01-00-449/08-00 7
COM Interfejsy Obiekty COM udostępniają swoje funkcje obiektom zewnętrznym za pośrednictwem interfejsów. Interfejs jest zestawem prototypów funkcji składowych komponentu COM (metod). Nazwy interfejsów przyjęło się poprzedzać przedrostkiem I, np.: ILookup Interfejs można zdefiniować na bazie innego interfejsu, czyli zastosować dziedziczenie (ale wyłącznie jednobazowe) Interfejs nie posiada własnej implementacji. Każdy komponent może implementować wiele interfejsów - oferować wiele zestawów usług. Klienty (aplikacje lub inne komponenty) odwołują się do interfejsów za pośrednictwem wskaźników. Każdy interfejs posiada własny, unikalny identyfikator - Globally Unique Identifier (GUID). nr POKL.04.01.01-00-449/08-00 8
Interfejsy Schemat implementacji interfejsu Każdy komponent może implementować wiele interfejsów - oferować wiele zestawów usług. IA IB Komponent COM (coclass) IC Komponent lub coclass (skrót od component object class) zawiera kod wszystkich funkcji udostępnianych przez interfejsy, funkcje pomocnicze i dane. Komponent jest umieszczony w serwerze COM - pliku binarnym (DLL lub EXE). nr POKL.04.01.01-00-449/08-00 9
Implementacja interfejsu Implementacja interfejsu (definicja komponentu) może być zrealizowana w różnych językach programowania. Komponenty mogą być używane przez inne komponenty lub aplikacje realizowane w różnych językach programowania. Technologia COM definiuje standardy komunikowania się na poziomie binarnym. Na poziomie źródłowym ta sama funkcja będzie kodowana zgodnie ze składnią danego języka, np.: Visual Basic object.add( Time As Double, Name As String ) As Variant C++ HRESULT Add( double Time, BSTR Name, VARIANT* pval ); Java public com.ms.com.variant Add( double Time, String Name ); Trzeba ponadto zapewnić konwersję typów danych. Szczegóły w dokumentacji MSDN (COM Language Translation). nr POKL.04.01.01-00-449/08-00 10
Technologia COM w Visual C++ Preferowanym językiem implementacji jest C++. Interfejs jest definiowany jako klasa abstrakcyjna z czystymi funkcjami wirtualnymi. Do tworzonych obiektów automatycznie dołączana jest Vtable (tablica wywołań funkcji wirtualnych). W C trzeba taką tablicę samemu zdefiniować. Każda metoda musi mieć wskaźnik zwrotny na obiekt. W C++ automatycznie dodawany jest wskaźnik this. W C trzeba dodać do każdej funkcji dodatkowy parametr. Definicja klasy stanowi też automatycznie przestrzeń nazw dla składowych. nr POKL.04.01.01-00-449/08-00 11
Globally Unique Identifiers (GUIDs) Do identyfikacji każdego interfejsu i każdego komponentu, używany jest unikalny identyfikator (128-bitowa struktura; long + uint + uint + 8*char = 16 bajtów). Plik guiddef.h zawiera definicję struktury opisującej GUID i pomocnicze makrodefinicje, np.: DEFINE_GUID. Programista zamiast tego identyfikatora używa związanej z nim stałej symbolicznej IID_<nazwa interfejsu> lub CLSID_<nazwa komponentu>. Przykład definicji stałej IID_ILOOKUP: DEFINE_GUID(IID_ILOOKUP, 0xc4910d71, 0xba7d, 0x11cd, 0x94, 0xe8, 0x08, 0x00, 0x17, 0x01, 0xa8, 0xa3); Do generowania GUID udostępnione są programy narzędziowe (w Visual C++ Tools/Create GUID) oraz funkcja CoCreateGuid z COM API. nr POKL.04.01.01-00-449/08-00 12
Obsługa błędów Wszystkie funkcje COM w Visual C++ (z wyjątkiem AddRef i Release z interfejsu IUnknown) zwracają jako wynik wartość typu HRESULT zawierającą informacje o błędach (plik winerror.h wiersz 17 040). Typowy schemat obsługi błędów: HRESULT hr; hr = funkcja_com(parametry); if (hr == S_OK) { } // dalsze operacje... else cerr << Wywołanie funkcja_com nie powiodło się ; nr POKL.04.01.01-00-449/08-00 13
Makrodefinicje do obsługi błędów Powszechną praktyką jest wykorzystywanie do obsługi błędów makrodefinicji SUCCEEDED i FAILED, którym przekazuje się wynik wywołania funkcji typu HRESULT. Przykład: if ( FAILED( CoInitialize(NULL) )) { cerr << "Inicjacja nie powiodła się"; return 1; } Kod błędu można odczytać z HRESULT stosując makrodefinicję HRESULT_CODE. Opis tekstowy błędu (na podstawie jego kodu, np.: 0x800401F0) można uzyskać przy użyciu narzędzia Tools/Error Lookup w VC++. Opis jest widoczny również w debuggerze. nr POKL.04.01.01-00-449/08-00 14
Biblioteka COM COM library API udostępnia funkcje i makra do obsługi zadań związanych z definiowaniem i używaniem obiektów COM. Bibliotekę trzeba przed użyciem zainicjować przy użyciu funkcji HRESULT CoInitialize(LPVOID pvreserved); Schemat inicjacji: // Inicjacja biblioteki COM - załadowanie plików DLL. if ( FAILED( CoInitialize(NULL) )) { cerr << "Inicjacja biblioteki COM nie powiodła się" << endl; return 1; }... // Zwolnij zasoby przydzielone bibliotece COM. CoUninitialize(); nr POKL.04.01.01-00-449/08-00 15
Tworzenie obiektu COM Aby otworzyć obiekt COM i uzyskać wskaźnik na interfejs stosuje się funkcję CoCreateInstance() z COM API HRESULT CoCreateInstance( REFCLSID rclsid, LPUNKNOWN punkouter, DWORD dwclscontext, REFIID riid, LPVOID* ppv); Parametry: rclsid identyfikator komponentu, np.: CLSID_ShellLink. punkouter używany przy agregacji obiektu COM (NULL gdy nie jest stosowana agregacja) dwclscontext rodzaj serwera; dla serwera DLL w tym samym procesie stosuje się stałą CLSCTX_INPROC_SERVER. riid - identyfikator interfejsu, np.: IID_IShelLink. ppv - wskaźnik na interfejs; parametr zwrotny funkcji. Zwracane wartości: S_OK, REGDB_E_CLASSNOTREG, CLASS_E_NOAGGREGATION lub E_NOINTERFACE. nr POKL.04.01.01-00-449/08-00 16
Interfejs IUnknown Podstawowym interfejsem implementowanym przez każdy obiekt COM jest interfejs IUnknown, udostępniający trzy funkcje: QueryInterface, AddRef, Release. IUnknown Komponent COM QueryInterface AddRef Release implementacja implementacja implementacja nr POKL.04.01.01-00-449/08-00 17
IUnknown funkcje AddRef i Release Funkcje AddRef i Release zarządzają licznikiem referencji do interfejsów. AddRef jest wywoływana, gdy klient używa danego interfejsu i zwiększa licznik o 1. Release jest wywoływana, gdy klient nie potrzebuje już interfejsu i zmniejsza licznik. Obie zwracają nową wartość licznika referencji. Funkcja QueryInterface zostanie omówiona w dalszej części wykładu. nr POKL.04.01.01-00-449/08-00 18
Schemat tworzenia i użycia obiektu COM HRESULT hr; // zmienna przechowująca wynik wywołania IShellLink* pisl; // wskaźnik na interfejs hr = CoCreateInstance ( CLSID_ShellLink, NULL, // CLSID komponentu // nie używa się agregacji CLSCTX_INPROC_SERVER, // typ serwera IID_IShellLink, (void**) &pisl ); if ( SUCCEEDED ( hr ) ) { } // IID interfejsu // zwracany wskaźnik // Wywołania metod komponentu z użyciem pisl... else { } pisl->release(); // powiadom obiekt COM o zakończeniu // Nie utworzono obiektu COM; hr zawiera kod błędu nr POKL.04.01.01-00-449/08-00 19
Przykład użycia komponentu Active Desktop Active Desktop jest usługą Internet Explorera pozwalającą na wyświetlanie stron www, dokumentów HTML, apletów Javy i komponentów ActiveX na tapecie (bez użycia przeglądarki; Uwaga! Spowalnia system). Działania zrealizowane w przykładzie: 1. Inicjalizacja biblioteki COM (funkcja CoInitialize). 2. Utworzenie obiektu COM typu ActiveDesktop i pobranie wskaźnika do interfejsu IActiveDesktop. 3. Wywołanie metody GetWallpaper() komponentu ActiveDesktop. 4. Jeżeli wywołanie zakończyło się powodzeniem wydrukowanie nazwy pliku przechowującego tapetę widoczną na pulpicie. 5. Pobranie przy użyciu metody GetDesktopItemCount() liczby elementów (stron www dodanych do listy) wydruk informacji. 6. Pobranie (w pętli) informacji o elementach (funkcja GetDesktopItem) do struktury COMPONENT i wydruk nazw elementów (stron www). 7. Zwolnienie interfejsu (funkcja Release) 8. Zwolnienie zasobów przydzielonych bibliotece COM (funkcja CoUninitialize). Przykład: solution: Pulpit, project: Pulpit1 nr POKL.04.01.01-00-449/08-00 20
Łaocuchy w technologii COM uzupełnienie przykładu Łańcuchy zwracane przez funkcje COM kodowane są w Unikodzie (2 bajty na znak). Znak w Unikodzie definiowany jest w VC++ przez typ WCHAR, np.: char tekst1[80]; // zwykła tablica znakowa WCHAR tekst2[80]; // tablica znaków w Unikodzie Do przekształcenia tekstu Unikod na tekst ASCII służy funkcja WideCharToMultiByte(). Prostszym sposobem jest użycie klasy CString z biblioteki MFC, której konstruktor akceptuje zarówno łańcuchy ASCII jak i Unikod (konwersja polskich znaków jest poprawna), np.: CString str1(unicode_text); Strumień standardowy wcout akceptuje teksty wyłącznie w Unikodzie. Zwykły tekst musi być przekształcony w Unikod (prefiks L, np.: L Nazwa: ). Uwaga: nie radzi sobie z polskimi znakami przerywa wyprowadzanie. nr POKL.04.01.01-00-449/08-00 21
IUnknown funkcja QueryInferface Funkcja QueryInterface dostępna w każdym komponencie (ponieważ zdefiniowana jest w interfejsie IUnknown) pozwala na uzyskanie dostępu do innych interfejsów danego obiektu. Jej składnia jest następująca: HRESULT QueryInterface (REFIID iid, void** ppvobject); Parametry: iid - identyfikator GUID żądanego interfejsu ppvobject - wskaźnik do żądanego interfejsu; parametr zwrotny. Gdy interfejs jest dostępny, funkcja zwraca wartość S_OK. W przypadku, gdy interfejs nie jest zaimplementowany zwracana jest wartość E_NOINTERFACE. nr POKL.04.01.01-00-449/08-00 22
Przykład użycia komponentu z wieloma interfejsami Zadaniem programu (project: Pulpit2) jest dodanie na pulpicie skrótu do własnego pliku exe. Etapy realizacji zadania: 1. Pobranie ścieżki do własnego pliku exe (GetModuleFileName). 2. Pobranie ścieżki do foldera pulpitu (SHGetSpecialFolderPath). 3. Uzupełnienie ścieżki o nazwę pliku typu.lnk zawierającego link. 4. Sprawdzenie czy plik już istnieje (CFile::GetStatus). 5. Przekształcenie ścieżki z nazwą pliku na łańcuch w Unikodzie (MultiByteToWideChar). 6. Inicjacja biblioteki COM (CoInitialize). 7. Utworzenie obiektu COM typu Shell Link (CoCreateInstance) i uzyskanie wskaźnika na interfejs IShellLink. 8. Ustawienie ścieżki do pliku, dla którego tworzony jest skrót (IShellLink::SetPath) 9. Uzyskanie wskaźnika na kolejny interfejs IPersistFile obiektu Shell Link (IShellLink::QueryInterface) 10. Zapisanie pliku skrótu do foldera pulpitu (IPersistFile::Save) 11. Zwolnienie interfejsów IPersistFile i IShellLink (Release) oraz CoUninitialize nr POKL.04.01.01-00-449/08-00 23
Definiowanie komponentu COM Definiowanie interfejsu Makra i typy danych stosowane w definicjach Implementacja interfejsu nr POKL.04.01.01-00-449/08-00 24
Definiowanie interfejsu Każdy interfejs komponentu COM musi być bezpośrednio lub pośrednio wyprowadzony z interfejsu IUnknown. Interfejs jest definiowany w języku IDL (Microsoft Interface Definition Language). Przykład: import "unknwn.idl"; // definicja interfejsu IUnknown [ object, uuid(4411b7fe-ee28-11ce-9054-080036f12502) ] interface ISome : IUnknown { HRESULT SomeMethod(void); }; [ object, uuid(4411b7fd-ee28-11ce-9054-080036f12502) ] interface ISomeOther : ISome { HRESULT SomeOtherMethod([in]long l); }; Definicja interfejsu zapisywana jest w pliku.idl nr POKL.04.01.01-00-449/08-00 25
Definiowanie interfejsu w VC++ W SDK VC++ dostępna jest klasa IUnknown (plik nagłówkowy unknown.h) reprezentująca interfejs IUnknown. Z klasy tej można wyprowadzić własny interfejs stosując składnię C++, np.: // {15038B10-3D3E-11ce-9EE5-00AA004231BF} DEFINE_GUID(IID_IPrintInterface, 0x15038b10, 0x3d3e, 0x11ce, 0x9e, 0xe5, 0x0, 0xaa, 0x0, 0x42, 0x31, 0xbf); class IPrintInterface : public IUnknown { public: // Standard IUnknown interface functions virtual HRESULT QueryInterface(REFIID riid, LPVOID ppvobj)=0; virtual ULONG AddRef(void) = 0; virtual ULONG Release(void) = 0; // This interface virtual HRESULT PrintObject(void) = 0; }; nr POKL.04.01.01-00-449/08-00 26
Makra i typy danych stosowane w definicjach typedef struct _GUID { unsigned long Data1; // 32 bity unsigned short Data2; // 16 bitów unsigned short Data3; // 16 bitów byte Data4[ 8 ]; // 8*8 = 64 bity } GUID; // definicja Globally Unique Identifier, 128 bitów typedef GUID IID - identyfikator interfejsu; synonim GUID typedef IID* LPIID; - wskaźnik daleki na ID interfejsu #define REFIID const IID& - referencja na identyfikator interfejsu typedef GUID CLSID - identyfikator co-klasy; synonim GUID typedef CLSID* LPCLSID; - wskaźnik daleki na ID co-klasy typedef long HRESULT; - typ wyniku funkcji COM; synonim long nr POKL.04.01.01-00-449/08-00 27
Konwencja wywoływania metod interfejsu W Microsoft C++ wprowadzono kilkanaście dodatkowych słów kluczowych (rozpoczynających się od ), które służą do sterowania kompilacją. Słowo kluczowe stdcall określa sposób wywołania wymagany przez funkcje Win32 API. class IPrintInterface : public IUnknown { public: // Standard IUnknown interface functions virtual HRESULT stdcall QueryInterface(REFIID riid, LPVOID ppvobj)=0; virtual ULONG stdcall AddRef(void) = 0; virtual ULONG stdcall Release(void) = 0; // This interface virtual HRESULT stdcall PrintObject(void) = 0; }; nr POKL.04.01.01-00-449/08-00 28
Makrodefinicja STDMETHODCALLTYPE Definicja: #ifdef _WIN32 // Win32 doesn't support export #define STDMETHODCALLTYPE stdcall #else #define STDMETHODCALLTYPE export stdcall #endif class IPrintInterface : public IUnknown { public: // Standard IUnknown interface functions virtual HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, LPVOID ppvobj)=0; virtual ULONG STDMETHODCALLTYPE AddRef(void) = 0; virtual ULONG STDMETHODCALLTYPE Release(void) = 0; // This interface virtual HRESULT STDMETHODCALLTYPE PrintObject(void) = 0; }; nr POKL.04.01.01-00-449/08-00 29
Makra STDMETHODIMP_ oraz STDMETHODIMP #define STDMETHODIMP HRESULT STDMETHODCALLTYPE #define STDMETHODIMP_(type) type STDMETHODCALLTYPE Pierwsze makro definiuje konwencję i wynik wywołania HRESULT, drugie podany typ wyniku. Przykład: class IPrintInterface : public IUnknown { public: // Standard IUnknown interface functions virtual STDMETHODIMP QueryInterface(REFIID riid, LPVOID ppvobj)=0; virtual STDMETHODIMP_(ULONG) AddRef(void) = 0; virtual STDMETHODIMP_(ULONG) Release(void) = 0; // This interface virtual STDMETHODIMP PrintObject(void) = 0; }; nr POKL.04.01.01-00-449/08-00 30
Makra STDMETHOD_ i STDMETHOD #define STDMETHOD(method) virtual HRESULT STDMETHODCALLTYPE method #define STDMETHOD_(type,method) virtual type STDMETHODCALLTYPE method Pierwsza wersja definiuje funkcję wirtualną interfejsu, która zwraca wynik typu HRESULT. Druga definiuje funkcję, która zwraca wynik wskazanego typu. class IPrintInterface : public IUnknown { public: // Standard IUnknown interface functions STDMETHOD(QueryInterface)(REFIID riid, LPVOID ppvobj)=0; STDMETHOD_(ULONG, AddRef)(void) = 0; STDMETHOD_(ULONG, Release)(void) = 0; // This interface STDMETHOD(PrintObject)(void) = 0; }; nr POKL.04.01.01-00-449/08-00 31
Makrodefinicja PURE Definiuje czystą funkcję wirtualną: #define PURE = 0 class IPrintInterface : public IUnknown { public: // Standard IUnknown interface functions STDMETHOD(QueryInterface)(REFIID riid, LPVOID ppvobj) PURE; STDMETHOD_(ULONG, AddRef)(void) PURE; STDMETHOD_(ULONG, Release)(void) PURE; // This interface STDMETHOD(PrintObject)(void) PURE; }; nr POKL.04.01.01-00-449/08-00 32
Implementacja interfejsu W C++ interfejs jest implementowany przez klasę. Przykład: class CPrintObject : public IPrintInterface { public: CPrintObject() : refcount(0) {} // konstruktor virtual ~CPrintObject() {} // destruktor // Standardowe funkcje interfejsu IUnknown virtual STDMETHODIMP QueryInterface(REFIID riid, LPVOID ppvobj); virtual STDMETHODIMP_(ULONG) AddRef(void); virtual STDMETHODIMP_(ULONG) Release(void); // Funkcje interfejsu IPrintInterface virtual STDMETHODIMP PrintObject(void); protected: CPrintObject(); ULONG refcount; // licznik referencji }; Metody już nie są czystymi funkcjami wirtualnymi i muszą zostać zdefiniowane. nr POKL.04.01.01-00-449/08-00 33
Definicja funkcji AddRef i Release Funkcje AddRef i Release zarządzają licznikiem referencji. Każde użycie interfejsu powoduje zwiększenie stanu licznika (wywołanie AddRef), a kiedy już nie jest potrzebny aplikacja klienta ma obowiązek wywołać funkcję Release. STDMETHODIMP_(ULONG) CPrintObject::AddRef(void) { return ++refcount; // zwiększa licznik referencji } STDMETHODIMP_(ULONG) CPrintObject:: Release(void) { if (--refcount == 0) delete this; // usunięcie obiektu!!! return refcount; } nr POKL.04.01.01-00-449/08-00 34
Funkcja QueryInterface Zadaniem funkcji jest zwrócenie wskaźnika na interfejs o podanym identyfikatorze GUID. Przykład użycia funkcji (założenie: punk jest wskaźnikiem na obiekt IUnknown): IPrintInterface* pprint = NULL; if (punk->queryinterface(iid_iprintinterface, { } (void**)&pprint) == NOERROR) pprint->printobject(); pprint->release(); // release pointer obtained via QueryInterface! nr POKL.04.01.01-00-449/08-00 35
Definicja QueryInterface STDMETHODIMP CPrintObject::QueryInterface(REFIID iid, void FAR* FAR* ppvobj) { } if (iid == IID_IUnknown iid == IID_IPrintInterface) { } *ppvobj = this; AddRef(); // zwiększenie licznika referencji return NOERROR; return E_NOINTERFACE; // zgłoszenie błędu nr POKL.04.01.01-00-449/08-00 36