Programowanie COM
Technologia COM (ang. Component Object Model) stanowi propozycję Microsoftu w kierunku tworzenia aplikacji opartych na komponentach (ang. componentware). Poświęcono jej już tak wiele opracowań, iż trudno byłoby dodać do tego jeszcze coś istotnego. Z tego właśnie względu niniejszy rozdział nie stanowi wprowadzenia do technologii COM — chociaż wyjaśnia wiele ważnych koncepcji — do jego zrozumienia konieczna jest więc przynajmniej podstawowa wiedza z tej dziedziny.
Mimo rosnącej popularności Linuksa i Kylix'a zainteresowanie programistów technologią COM nie słabnie. I nic w tym dziwnego — nawet zagorzali krytycy Windows muszą przyznać, iż wciąż stanowią one podstawowy system operacyjny dla komputerów wykorzystywanych do zastosowań biurowych i tendencja ta nie wydaje się zanikać; kolejne wersje Windows coraz bardziej przesiąknięte są technologią COM, ergo — „windowsowi” programiści nie mają wielkiego wyboru i utarta ścieżka „COM—DCOM—COM+” zdaje się nie mieć realnej alternatywy.
Technologia COM umożliwia budowanie aplikacji opartych na współpracujących ze sobą, acz poza tym niezależnych, komponentach binarnych. Wśród najważniejszych jej zalet zdecydowanie wymienić trzeba następujące:
definiuje ona pewien standard na poziomie binarnym, w oderwaniu od konkretnego narzędzia projektowego czy języka programowania;
ma ona na wskroś przezroczysty („transparentny”) charakter — użytkownikowi jest obojętne, gdzie fizycznie znajduje się aktualnie wykorzystywany komponent; jeżeli nawet znajduje się on na odległym komputerze, aplikacja klienta korzysta z niego w taki sam sposób, jak z komponentu lokalnego.
C++Builder 5, jak zresztą i inne narzędzia projektowe dla platformy Windows, z konieczności oferuje programistom elementy udostępniające im mechanizmy COM. Przeznaczeniem niniejszego rozdziału jest więc przedstawienie informacji trudnych do znalezienia lub wręcz nieobecnych w innych źródłach, jak również spojrzenie na C++Builder z perspektywy programisty COM.
Serwery i klienty COM
W jednym z filmów bohater przedostaje się z ekranu do naszego świata rzeczywistego; próbując następnie rozbić szybę samochodu, dotkliwie się kaleczy, przekonując się tym samym, iż w tutaj pewne sprawy mają się nieco inaczej niż w jego wyimaginowanym świecie. Piszemy te słowa dlatego, iż w naszym przekonaniu C++Builder zdecydowanie jest narzędziem stworzonym i przeznaczonym do pracy w świecie rzeczywistym — i jeżeli nawet przychodzi nam przełamywać jakieś granice, chronią nas stalowe rękawice i żadne okaleczenie nam nie grozi.
Wraz z wprowadzeniem ATL 3.0 w wersji 5 C++Buildera, produktywność tego ostatniego pod względem tworzenia klientów i serwerów COM wzrosła jeszcze bardziej; w samej technologii COM nie brak rzecz jasna tematów słabo udokumentowanych — przykładowo problematyka tworzenia ujścia zdarzeń (ang. event sink) w oparciu o interfejsy dyspozycyjne (dispinterfaces) przewija się nieustannie przez grupę dyskusyjną Borlanda (zob. nntp://forums.inprise.com/borland.public.cppbuilder.activex). C++Builder 5 nie tylko dostarcza niezbędnych ku temu narzędzi, lecz także dokumentuje je obszernie w swych plikach pomocy.
Niestety, jeżeli chodzi o zdarzenia innej kategorii — jak te generowane przez „klasyczne” interfejsy (ang. custom interfaces) — nie mamy tak dobrych wiadomości. Pod względem budowy serwerów COM kreatory C++Buildera nie dostarczają żadnych standardowych środków ich obsługi i programista zdany jest wyłącznie na własną inwencję w tym względzie; niewiele inaczej przedstawia się sprawa po stronie klienta.
W niniejszym rozdziale opiszemy zdarzenia obydwu kategorii — przedstawimy sposoby generowania ich po stronie serwera i obsługi po stronie klienta. Zaprezentujemy też przykłady konstrukcji bibliotek DLL typu proxy i stub, który to temat udokumentowany jest przez C++Builder dość skąpo.
Interfejsy wychodzące i ujścia zdarzeń
Obiekt COM może implementować wiele interfejsów pełniących rozmaitą rolę w różnorodnych okolicznościach. Użytkownik-klient, wywołując odpowiednie metody tych interfejsów, powoduje określone działania po stronie serwera, które to działania zdeterminowane są implementacją tychże metod. Ponadto obiekty COM realizować mogą powiadamianie użytkownika o wystąpieniu pewnych zdarzeń na serwerze, jak np. zakończenie długotrwałej operacji; powiadamianie takie ma charakter asynchroniczny i uwalnia użytkownika od ciągłego „przepytywania” (ang. polling) serwera na okoliczność wystąpienia tychże zdarzeń.
Elementami serwera, umożliwiającymi takie właśnie powiadamianie, są tzw. interfejsy wychodzące (ang. outgoing interfaces) — są to interfejsy, które aplikacja klienta powinna zaimplementować i do których obiekt serwera odwoływać się będzie każdorazowo, gdy wystąpi określone zdarzenie. Elementami aplikacji-klienta odpowiedzialnymi za implementację interfejsów wychodzących są tzw. ujścia zdarzeń (ang. event sinks) — są to obiekty COM, z którym każdy implementuje pojedynczy interfejs; każdy z tych obiektów połączony jest z serwerem za pośrednictwem tzw. punktu połączeniowego (ang. connection point).
Rysunek 16.1 ilustruje schematycznie obydwie metody komunikacji pomiędzy klientem i serwerem: wywoływanie metod serwera oraz obsługę zdarzeń w ramach ujścia zdarzeń.
Tu proszę wykonać rysunek znajdujący się na stronie 664 oryginału z następującymi zmianami:
Client -> klient
Server -> serwer
COM object -> obiekt COM
Event Sink -> ujście zdarzeń
Rysunek 16.1 Komunikacja klient-serwer oparta na punktach połączeniowych i na ujściu zdarzeń
Tworzenie serwera COM
Jako że właściwe zdefiniowanie problemu stanowi połowę jego rozwiązania, rozpocznijmy więc od określenia zadań, jakie wykonywać ma nasz przykładowy serwer. Będzie to swoisty serwer „zodiakalny” przeznaczony dla tych, którzy interesują się horoskopami i tajnikami astrologii. Otrzymując datę urodzenia użytkownika, serwer ten wyprodukuje nazwę związanego z tą datą znaku zodiaku — ta usługa powinna być dostępna dla klientów każdej kategorii, również tych tworzących programy w Visual Basicu. Dla użytkowników bardziej zaawansowanych, korzystających z Delphi lub C++Buildera, dostępna będzie ponadto informacja rozszerzona, jak porada dnia, profil osobowości, przypisany żywioł (ziemia, ogień, woda, powietrze), patronująca planeta itp.
Funkcje te realizowane będą przez pojedynczy obiekt COM serwera, posiadający dwa interfejsy: pierwszy, dualny, wyprowadzony z IDispatch, dostępny dla Visual Basica oraz drugi, specjalizowany i bardziej złożony. Na wypadek przeciążenia serwera przewidzieliśmy także wyjątki i komunikaty diagnostyczne dla niecierpliwych użytkowników.
Interfejsy dualne (dual interfaces) przeznaczone są obecnie raczej na użytek klientów posługujących się językami programowania prowadzącymi ścisłą kontrolę typów (tzw. strongly-typed languages) — na przykład Visual Basic'iem —i pełnią rolę swoistych „łat” (ang. patches) umożliwiających zwiększenie efektywności przetwarzania, właśnie dzięki owej rygorystycznej kontroli typów, wykonywanej na etapie kompilacji.
Ze względu na ów prowizoryczny charakter interfejsów dualnych nie zaleca się ich stosowania — niezależnie od tego C++Builder oferuje wiele środków dla ich obsługi, najprawdopodobniej ze względów wstecznej kompatybilności z Visual Basic'iem na użytek szerokiej rzeszy przyzwyczajonych do tego ostatniego użytkowników.
Wybór typu serwera
W dalszym ciągu rozdziału pod pojęciem „serwera” rozumieć będziemy plik binarny udostępniający przynajmniej jeden obiekt COM. Zależnie od lokalizacji rozróżniamy trzy typy serwerów COM: wewnątrzprocesowy (inproc), zewnątrzprocesowy (outproc) i zdalny (remote).
Serwer wewnątrzprocesowy ma zawsze postać biblioteki DLL (pliki *.OCX, skrywające w swym wnętrzu kontrolki ActiveX, są właśnie bibliotekami DLL). Biblioteka ta rezyduje w przestrzeni adresowej procesu-klienta, co czyni ją względnie sprawną w porównaniu z serwerami pozostałych typów. Podstawową wadą takiej integracji jest jednakże nieodporność na awarie — gdy załamie się serwer, załamuje się także zazwyczaj cała aplikacja.
Z serwerem zewnątrzprocesowym sprawa ma się cokolwiek odwrotnie — jest on plikiem wykonywalnym realizowanym jako odrębny proces; czyni go to mniej efektywnym w stosunku do serwera wewnątrzprocesowego, lecz ze względu na izolację przestrzeni adresowych błędy serwera nie zagrażają na ogół aplikacji-klientowi.
Serwer zdalny rezyduje na innym komputerze niż aplikacja-klient. Może on być zaimplementowany jako biblioteka DLL (korzystająca z pośrednictwa procesu surrogate) albo plik wykonywalny (.EXE).
Dla osiągnięcia maksymalnej efektywności obiekty kompatybilne z COM+/MTS powinny być realizowane za pośrednictwem bibliotek DLL — chyba, że jakieś ważne względy przemawiają za plikiem .EXE. Nasz przykładowy serwer jest serwerem wewnątrzprocesowym, z konieczności więc ma postać biblioteki DLL.
Wybór modelu wątkowego
Mimo iż kreatory COM C++Buildera oferują kilka modeli wątkowych, wszystkie one mogą być podzielone na dwie grupy: apartamentowe (ang. apartment) i swobodne (ang. free).
Apartament COM jest rodzajem logicznego obszaru otwieranego przez funkcję CoInitialize() (lub CoInitializeEx()) i zamykanego przez funkcję CoUninitialize(); podstawowym celem tego obszaru jest zdefiniowanie granic synchronizacji. Dla każdego wątku odwołującego się do obiektu COM powinien być utworzony osobny apartament — biblioteka COM czasami wykonuje tę czynność automatycznie, czasami nie; ta niewątpliwie interesująca kwestia wykracza jednak poza ramy niniejszego rozdziału, ze względu na stopień swej szczegółowości. Z wątkami w poszczególnych apartamentach związana jest pętla komunikatów Windows, synchronizująca dostęp do poszczególnych metod komponentu. Ten rodzaj modelu wątkowego wymuszany jest w stosunku do kontrolek ActiveX.
Swobodny model wątkowy nie korzysta ze wspomnianej pętli komunikatów Windows; ponieważ potencjalnie dowolna metoda obiektu COM może być wywoływana przez dowolny wątek w dowolnej chwili, wszelkie czynności synchronizacyjne muszą być więc zapewnione przez sam obiekt.
Kreatory C++Buildera posługują się utartym żargonem, proponując następujące rodzaje modeli wątkowych:
Single — serwer w ogóle nie obsługuje wielowątkowości; wszelkie odwołania do obiektu COM szeregowane są w jego głównym wątku, z którym związany jest duży, pojedynczy apartament. Ze względu na katastrofalne potencjalnie konsekwencje dla efektywności serwera, model ten nie powinien być w ogóle używany.
Apartment (STA — Single-Threaded Apartment) — każda metoda obiektu wykonywana jest w postaci osobnego wątku, zaś poszczególne wątki synchronizowane są poprzez pętlę komunikatów Windows. Każdy wątek rezyduje w swym odrębnym apartamencie, co uwalnia od kłopotów związanych z modelem Single.
Free (MTA — Multi-Threaded Apartment) — dowolny wątek może wywoływać dowolną metodę w dowolnej chwili; wszelkie czynności synchronizacyjne muszą być zapewnione przez implementację obiektu COM.
Both — obiekt COM oznaczany jest jako zgodny z obydwoma modelami wątkowymi: STA i MTA. COM wybierze dla niego model tożsamy ze stosowanym przez klienta. To rozwiązanie jest najbardziej elastyczne, uwalnia bowiem od wielu problemów związanych z transpozycją danych (ang. marshaling).
Neutral — ten model dostępny jest tylko w COM+. Kilka wątków może jednocześnie odwoływać się do obiektu, przy czym synchronizacja zapewniona jest przez bibliotekę COM+. W „klasycznym” COM model ten równoważny jest modelowi STA.
Należy pamiętać o tym, iż to na programiście implementującym metody obiektu COM spoczywa odpowiedzialność za zdolność tego obiektu do pracy w wyspecyfikowanym modelu wątkowym. Dla naszego serwera wybraliśmy model STA, jako że jest on najbardziej uniwersalny i zalecany nawet w przypadku serwerów o dużych wymaganiach efektywnościowych — na przykład współpracujących bezpośrednio z urządzeniami. Na użytek niniejszego rozdziału model ten stanowi także okazję do omówienia kilku istotnych zagadnień związanych z transpozycją danych.
Utworzenie serwera
Tworzenie wewnątrzprocesowego serwera COM rozpoczyna się od wybrania opcji ActiveX Library z karty ActiveX okna New Items (rys. 16.2). Spowoduje to utworzenie nowego projektu — zapisz go pod nazwą ZodiacServer.
Tu proszę wkleić rysunek z pliku orig-16-2.bmp
Rysunek 16.2 Początek tworzenia wewnątrzprocesowego serwera COM
W ten oto prosty sposób stworzyliśmy pusty serwer wewnątrzprocesowy.
Jeżeli chcesz utworzyć serwer zewnątrzprocesowy, zainicjuj nową aplikację (File|New Application).
Dodanie obiektu COM
Aby dodać do pustego serwera obiekt COM, trzeba wykonać kolejno następujące czynności:
Z okna New Items wybierz opcję Automation Object; wyświetlone zostanie okno New Automation Object (rys. 16.3).
Tu proszę wkleić rysunek z pliku orig-16-3.bmp
Rysunek 16.3 Atrybuty tworzonego obiektu COM
Wpisz Zodiac jako nazwę koklasy, wybierz Free jako model wątkowy i wpisz w ostatnie z pól cokolwiek, co uważasz za stosowne.
Zaznacz opcję Generate Event support code, w celu automatycznego wygenerowania kodu ATL związanego z obsługą zdarzeń pochodzących od interfejsów wychodzących.
Kliknij w przycisk OK. Powinno to spowodować wyświetlenie okna edytora biblioteki typów (TLE — Type Library Editor— rys. 16.4); jeżeli okno to się nie ukaże, wyświetl je za pomocą opcji View|Type Library menu głównego IDE.
Tu proszę wkleić rysunek z pliku orig-16-4.bmp
Rysunek 16.3 Okno edytora TLE z kodem nowo utworzonego obiektu COM
Pozycje w lewym panelu okna reprezentują koklasę Zodiac i dwa interfejsy: IZodiac, który jest interfejsem dualnym i jednocześnie domyślnym interfejsem serwera i IZodiacEvents, będący interfejsem dyspozycyjnym (dispinterface) reprezentującym zdarzenia generowane przez obiekt COM.
Tu zaczyna się wskazówka
Podobnie jak w C++, tak i na gruncie COM obecne są klasy, zwane w tym przypadku koklasami (ang. coclass — skrót od Component Object Class). Zadaniem koklasy jest implementacja interfejsów. To co widzimy w prawym panelu okna z rysunku 16.4, to definicja koklasy i implementowanych przez nią interfejsów w języku IDL (ang. Interface Definition Language). Zarówno każdy z interfejsów, jak i sama koklasa, identyfikowane są niepowtarzalnymi, charakterystycznymi łańcuchami zwanymi UUID (ang. Unique Identifier):
[
uuid(962A5645-4FF0-11D5-9351-D4F9944FAD58),
version(1.0),
helpstring("Zodiac Object")
]
coclass Zodiac
{
[default] interface IZodiac;
[default, source] dispinterface IZodiacEvents;
};
Interfejs kwalifikowany jako source jest interfejsem wychodzącym, nie jest więc implementowany przez koklasę, lecz przez jej punkt połączeniowy.
Tu kończy się wskazówka
Tu zaczyna się następna wskazówka
Biblioteka typu (ang. Type Library) składa się z danych opisujących zawartość serwera. Stanowi ona rodzaj binarnej dokumentacji każdego składnika obiektu COM, jak np. implementowane przez niego interfejsy i ich sygnatury. Zazwyczaj biblioteka typu powstaje jako wynik kompilacji jej zapisu w języku IDL, jednakże wiele środowisk projektowych (w tym C++Builder i Delphi) posiada narzędzia do bezpośredniego manipulowania nią na poziomie binarnym.
Biblioteki typu mogą być rozprowadzane jako integralne zasoby serwera, albo w postaci oddzielnych plików *.tlb. Ta druga ewentualność nie jest jednak zalecana ze względów bezpieczeństwa; serwery automatyzacji oraz kontrolki ActiveX mogą być dystrybuowane jedynie wraz z dołączaną biblioteką typu.
Po zawarciu bliższej znajomości z narzędziami C++Buildera wspomagającymi wykorzystanie technologii COM łatwo skonstatować, iż ich funkcjonowanie koncentruje się właśnie wokół bibliotek typu oraz ich integracji z serwerami COM; podstawowym narzędziem umożliwiającym określanie funkcji spełnianych przez tworzony serwer jest nieprzypadkowo edytor TLE.
Tu kończy się wskazówka
Przejdźmy teraz do definiowania metod pierwszego z interfejsów — IZodiac.
Kliknij prawym przyciskiem w ikonę reprezentującą ów interfejs w lewym panelu okna z rys. 16.4 i z menu kontekstowego wybierz opcję New|Method.
Zmień nazwę nowo utworzonej metody z Method1 na GetZodiacSign.
Podświetl w lewym panelu pozycję reprezentującą metodę GetZodiacSign i przejdź do karty Parameters prawego panelu.
Używając przycisku Add, dodaj trzy parametry określone następująco:
Name |
Type |
Modifier |
Day |
long |
[in] |
Month |
long |
[in] |
Sign |
BSTR* |
[out, retval] |
Pierwsze dwa z nich są parametrami wejściowymi zawierającymi dzień i miesiąc urodzenia, trzeci jest parametrem wynikowym, zawierającym nazwę znaku zodiaku odpowiadającego podanej dacie. Każde z pól w kolumnach Type i Modifier posiada stowarzyszoną listę wyboru, rozwijaną za pomocą przycisku z wielokropkiem (ellipsis) wskazywanym na rysunku 16.5 przez kursor myszki.
Tu proszę wkleić rysunek z pliku orig-16-5.bmp
Rysunek 16.5 Dodawanie parametrów do metody interfejsu za pomocą edytora TLE
Podstawowymi kategoriami parametrów (określanymi w kolumnie Modifier) są parametry wejściowe (in), wyjściowe (out) i wejściowo-wyjściowe (in-out). Rozróżnienie to wynika ze specyfiki transpozycji danych (marshaling) podczas ich wymiany pomiędzy klientem i serwerem.
I tak dla parametrów wyjściowych (out) pamięć przydzielana jest przez serwer, zaś klient odpowiedzialny jest za jej zwolnienie. Dla parametrów wejściowych (in) zarówno przydział, jak i zwolnienie pamięci leży całkowicie w gestii klienta. W przypadku parametrów wejściowo wyjściowych (in-out) pamięć przydzielana jest przez klienta; serwer może (choć nie musi) zmienić obszar pamięci przydzielony dla parametru, co nie zmienia faktu, iż za jego zwolnienie odpowiedzialny jest klient.
Modyfikator retval oznacza „wynik zwrotny” (ang. return value) i ma sens jedynie w przypadku tworzenia komponentów-otoczek po stronie klienta; w dalszej części rozdziału zajmiemy się dokładniej tą kwestią.
Zajmijmy się teraz drugą z metod — GetZodiacSignAsync() — dokonującą asynchronicznego ustalenia odpowiedniej nazwy znaku zodiaku. Sygnatura tej metody jest następująca:
HRESULT _stdcall GetZodiacSignAsync([in] long Day, [in] long Month);
Zwróć uwagę, iż nie posiada ona parametru wyjściowego niosącego wynikową nazwę znaku zodiaku. Nazwa ta będzie bowiem zwracana przez zdarzenie generowane w momencie, gdy zakończą się obliczenia zainicjowane przez metodę GetZodiacSignAsync().
Dodaj więc wpierw do interfejsu IZodiac metodę GetZodiacSignAsync(), specyfikując dwa parametry wejściowe (in) typu long — Day i Month — po czym przejdź do interfejsu IZodiacEvents i dodaj metodę OnZodiacSignReady o następującej sygnaturze:
HRESULT OnZodiacSignReady ([in] BSTR Sign);
Zwróć uwagę, iż parametr Sign jest teraz parametrem wejściowym — z punktu widzenia klienta parametr ten dostarczany jest z serwera, lecz wywołanie odbywa się w ujściu zdarzeń klienta.
Aktualny stan biblioteki ZodiacServer.tlb po wykonaniu opisanych czynności przedstawia rysunek 16.6.
Tu proszę wkleić rysunek z pliku orig-16-6.bmp
Rysunek 16.6 Zawartość koklasy Zodiac w oknie edytora TLE
Może to zaskakujące, ale wykonaliśmy właśnie zasadniczą część operacji definiowania koklasy Zodiac — choć mogłoby się wydawać, że to zaledwie dobry początek. Lwią część pozostałej pracy wykona dla nas C++Builder, generując „szkieletowy” kod podzielony na kilka plików, nam pozostanie tylko uzupełnienie treści poszczególnych metod interfejsów koklasy.
Wspomniane generowanie zostanie wykonane przez C++Builder w odpowiedzi na kliknięcie w przycisk Refresh Implementation — to pierwszy od lewej przycisk w prawej sekcji przycisków na pasku narzędziowym okna TLE. Wygenerowane zostaną cztery pliki: ZodiacServer.cpp, ZodiacImpl.cpp, ZodiacServer_TLB.cpp i ZodiacServer_ATL.cpp.
Przegląd wygenerowanego kodu
Przed przystąpieniem do uzupełniania kodu wygenerowanego przez C++Builder spróbujmy wpierw zrozumieć istotę tego, co zostało wygenerowane.
W wersji 5 C++Buildera kod związany z mechanizmami COM tworzony jest w oparciu o specyficzną bibliotekę szablonów, zwaną ATL 3 (ATL jest skrótem od ActiveX Template Library).
Plik ZodiacServer_TLB.h zawiera definicję w kategoriach języka C++ każdego z typów deklarowanych z użyciem TLE — jest to więc odwzorowanie elementów języka IDL na elementy C++. Plik ZodiacServer_TLB.h dołączany jest na samym początku pliku ZodiacImpl.h będącego plikiem nagłówkowym modułu koklasy.
Do pliku ZodiacServer_TLB.h powrócimy jeszcze przy okazji tworzenia klienta COM. Pliki ZodiacServer_ATL.h i ZodiacServer_ATL.cpp zawierają deklarację oraz implementację obiektu _Module, będącego egzemplarzem klasy CComModule i wykorzystywanym przez ATL do reprezentowania serwera i jego globalnych danych.
Plik ZodiacServer.cpp zawiera funkcję-punkt wejścia do biblioteki DLL serwera oraz definicje globalnych funkcji dokonujących rejestracji i wyrejestrowania serwera. Zawiera on także tzw. mapę obiektów umożliwiającą skojarzenie poszczególnych koklas z ich klasami-producentami (class factories).
Deklaracja klasy implementującej serwer COM znajduje się w pliku ZodiacImpl.h:
class ATL_NO_VTABLE TZodiacImpl :
public CComObjectRootEx<CComMultiThreadModel>,
public CComCoClass<TZodiacImpl, &CLSID_Zodiac>,
public IConnectionPointContainerImpl<TZodiacImpl>,
public TEvents_Zodiac<TZodiacImpl>,
public IDispatchImpl<IZodiac, &IID_IZodiac, &LIBID_ZodiacServer>
Klasa TZodiacImpl jest wiernym odpowiednikiem koklasy Zodiac. Wywodzi się ona z klasy-szablonu CComObjectRootEx, realizującej organizację dostępu do poszczególnych interfejsów i zarządzanie ich licznikami odwołań, a także ewentualne czynności synchronizacyjne wynikające z przyjętego modelu wątkowego obiektu COM — jako że użyliśmy modelu MTA, szablonem wyjściowym dla naszej klasy jest CComObjectRootEx<CComMultiThreadModel>.
Inny szablon biblioteki ATL — CComCoClass — zapewnia domyślną klasę-producenta dla obiektu COM, definiuje model agregacji, jak również zajmuje się niektórymi aspektami obsługi błędów, czego przykład przedstawimy w dalszej części rozdziału. Zauważ, iż wyprowadzenie z szablonu CComCoClass<TZodiacImpl, &CLSID_Zodiac> sugeruje jednoznacznie, iż identyfikator CLSID obiektu mapowany jest w jego klasę; mapowanie to zrealizowane jest ostatecznie w module ZodiacServer.cpp:
BEGIN_OBJECT_MAP(ObjectMap)
OBJECT_ENTRY(CLSID_Zodiac, TZodiacImpl)
END_OBJECT_MAP()
Tak więc związek pomiędzy deklarowaną koklasą i jej implementacją (w serwerze) ustanowiony jest na poziomie kodu aplikacji.
Kolejny z szablonów — IDispatchImpl — wykorzystywany jest na potrzeby implementacji interfejsów dualnych wywodzących się z IDispatch. Szablon ten zajmuje się skomplikowanymi szczegółami implementacji metody Invoke() odpowiedzialnej za przekazywanie odwołań do konkretnego interfejsu. Deklaracja klasy-szablonu IDispatchImpl znajduje się w pliku atlcom.h i wygląda mniej więcej tak:
template <class T, const IID* piid,
const GUID* plibid = &CComModule::m_libid,
WORD wMajor = 1, WORD wMinor = 0,
class tihclass = CComTypeInfoHolder>
class ATL_NO_VTABLE IDispatchImpl : public T
{
...
Zauważ, iż klasa bazowa dla IDispatchImpl reprezentowana jest przez parametr T, który w naszym przypadku zastępowany jest nazwą IZodiac; wynikiem rozwinięcia szablonu jest więc deklaracja
IDispatchImpl<IZodiac, &IID_IZOdiac, &LIBID_ZodiacServer>
oznaczająca, iż nasza klasa nie tylko implementuje interfejs IZodiac, ale jest jego bezpośrednią klasą pochodną. Mamy tu do czynienia z wykorzystaniem dziedziczenia jako jednego ze sposobów implementacji interfejsu.
Innym sposobem implementacji interfejsu COM jest mechanizm zwany agregacją (ang. aggregation), nie będziemy się nim jednak zajmować w niniejszej książce.
Klasa IConnectionPointContainerImpl — także szablon z biblioteki ATL — wyposaża obiekt COM w możliwość pełnienia roli pojemnika (container) dla punktu połączeniowego, tym samym przygotowując go do generowania zdarzeń. Klasa ta zarządza kolekcją obiektów IConnectionPointImpl, z których każdy odpowiedzialny jest za obsługę jednego z interfejsów wychodzących naszego obiektu.
Tak właśnie wygląda zarys implementacji serwera COM z użyciem biblioteki ATL.
Czytelnikom zainteresowanym szczegółami implementacji biblioteki ATL autorzy oryginału polecają książkę ATL Internals, aut. Chris Sells i in., wyd Addison-Wesley Publ., ISBN 0201695898. (przyp. tłum.)
Kontynuujmy naszą analizę. Deklaracja klasy-szablonu TEvents_Zodiac w pliku ZodiacServer_TLB.h jest generowana przez TLE. Zasługuje ona na szczególną uwagę, ponieważ jej implementacja zawiera kod odpowiedzialny za generowanie interesujących nas zdarzeń. Przyjrzyjmy się mianowicie metodzie Fire_OnZodiacSignReady():
template <class T> HRESULT
TEvents_Zodiac<T>::Fire_OnZodiacSignready(BSTR Sign)
{
T * pT = (T*)this;
pT->Lock();
IUnknown ** pp = m_vec.begin();
while (pp < m_vec.end())
{
if (*pp != NULL)
{
m_EventIntfObj.Attach(*pp);
m_EventIntfObj.OnZodiacSignReady(Sign);
m_EventIntfObj.Attach(0);
}
pp++;
}
pT->Unlock();
}
Metoda ta dokonuje iteracji po wszystkich klientach dołączonych do serwera; każdy z klientów jest informowany o tym, iż zakończyło się wykonywanie pewnej operacji — fizycznie polega to na wywołaniu metody OnZodiacSignReady() na rzecz obiektu m_EventIntfObj.
Obiekt m_EventIntfObj jest egzemplarzem klasy IZodiacEventsDisp deklarowanej w pliku ZodiacServer_TLB.h w sposób przedstawiony na wydruku 16.1.
Wydruk 16.1 Deklaracja klasy IZodiacEventsDisp
template <class T>
class IZodiacEventsDispT : public TAutoDriver<IZodiacEvents>
{
public:
IZodiacEventsDispT(){}
void Attach(LPUNKNOWN punk)
{ m_Dispatch = static_cast<T*>(punk); }
HRESULT __fastcall OnZodiacSignready(BSTR Sign/*[in]*/);
};
typedef IZodiacEventsDispT<IZodiacEvents> IZodiacEventsDisp;
Jak widać, IZodiacEventsDisp jest konkretyzacją szablonu IZodiacEventsDispT, wywodzącego się z TAutoDriver<IZodiacEvents>. TAutoDriver odpowiedzialny jest za wywołanie (w ramach swej metody OleFunction()) metody Invoke() interfejsu IDispatch swego wewnętrznego obiektu automatyzacji. TAutoDriver nie jest jednak definiowany w ramach biblioteki ATL, lecz jest klasą C++Buildera deklarowaną w pliku utilcls.h.
Ponieważ jednak TZodiacImpl wywodzi się także z klasy TEvents_Zodiac, w celu wygenerowania zdarzenia wystarczy wywołać jej metodę Fire_OnZodiacSignReady().
Zwieńczeniem deklaracji klasy TZodiacImpl jest kilka makr i jedna statyczna funkcja — odpowiedzialne są one za rejestrację serwera COM:
Wydruk 16.2 Kod odpowiedzialny za rejestrację klasy TZodiacImpl
// Dane używane do rejestracji obiektu
//
DECLARE_THREADING_MODEL(otFree);
DECLARE_PROGID("ZodiacServer.Zodiac");
DECLARE_DESCRIPTION("Astrology human fate wizard");
// Funkcje wywoływane w celu rejestracji/wyrejestrowania obiektu
//
static HRESULT WINAPI UpdateRegistry(BOOL bRegister)
{
TTypedComServerRegistrarT<TZodiacImpl>
regObj(GetObjectCLSID(), GetProgID(), GetDescription());
return regObj.UpdateRegistry(bRegister);
}
Prezentowane makra dostarczają elementów informacji, które wpisane zostaną do Rejestru systemowego: wykorzystywanego modelu wątkowego, identyfikatora rejestrowanego serwera oraz werbalnego opisu jego przeznaczenia. Funkcja UpdateRegistry() dokonuje faktycznego wpisania tych informacji do Rejestru, wraz z informacjami o samym obiekcie COM.
Proces rejestracji obiektu COM w C++Builderze różni się od analogicznego procesu w Visual C++, opartego na plikach skryptowych *.rgs. Tłumaczy to zasadniczą różnicę pomiędzy bibliotekami ATL w obydwu tych środowiskach.
Kolejne makra definiują mapę odpowiadającą za zdolność obiektu do realizacji podstawowych mechanizmów związanych z interfejsami, w szczególności udostępnianie ich egzemplarzy w odpowiedzi na wywołanie metody QueryInterface():
BEGIN_COM_MAP(TZodiacImpl)
COM_INTERFACE_ENTRY(IZodiac)
COM_INTERFACE_ENTRY2(IDispatch, IZodiac)
COM_INTERFACE_ENTRY_IMPL(IConnectionPointContainer)
END_COM_MAP()
Ostatnie z mapowań realizuje tablicę punktów połączeniowych:
BEGIN_CONNECTION_POINT_MAP(TZodiacImpl)
CONNECTION_POINT_ENTRY(DIID_IZodiacEvents)
END_CONNECTION_POINT_MAP()
Mapowanie to tworzy kolekcję identyfikatorów dla wszystkich interfejsów wychodzących — do tej klasy należy interfejs IZodiacEvents, którego identyfikator stanowi argument makra CONNECTION_POINT_ENTRY(). Wspomniana kolekcja wykorzystywana jest w implementacji klasy IConnectionPointContainerImpl, gdzie następuje jej przeszukiwanie w celu stwierdzenia, czy określony punkt połączeniowy jest zaimplementowany. Nie zapominajmy, że interfejs wychodzący nie jest implementowany jako taki — implementowany jest jego punkt połączeniowy.
Uzupełnianie treści metod
Po pobieżnym zapoznaniu się z ważniejszymi fragmentami kodu (właściwie: szkieletu tego kodu) przystąpmy zatem do implementacji pustych jeszcze metod. Po otwarciu pliku ZodiacImpl.cpp ujrzymy następującą konstrukcję:
/////////////////////////////////////////////////////////////////////////////
// TZodiacImpl
STDMETHODIMP TZodiacImpl::GetZodiacSigAsync(long Day, long Month)
{
}
STDMETHODIMP TZodiacImpl::GetZodiacSign(long Day, long Month, BSTR* Sign)
{
}
To nic innego jak szkielety metod zadeklarowanych uprzednio w oknie edytora TLE.
Rozpoczniemy od metody GetZodiacSign(). Jej zadaniem jest, jak wiadomo, ustalenie nazwy znaku zodiaku odpowiadającego określonej kombinacji „miesiąc-dzień”. Zrealizowaliśmy to za pomocą przeszukiwania specyficznej bazy danych (kompletny kod znajduje się na załączonym CD-ROMie) — nic oczywiście nie stoi na przeszkodzie, by zainteresowany Czytelnik wybrał inny sposób implementacji. Pierwotna postać naszej metody będzie więc następująca:
STDMETHODIMP TZodiacImpl::GetZodiacSign(long Day, long Month, BSTR* Sign)
{
TCOMCriticalSection::Lock Lock(CS);
::GetZodiacSign(Day, Month, *Sign);
return S_OK;
}
Ponieważ przypisaliśmy naszemu obiektowi COM model wątkowy MTA, musimy zapewnić synchronizację dostępu poszczególnych wątków do krytycznych zasobów i operacji, będących udziałem funkcji GetZodiacSign() (takim zasobem jest np. przeszukiwana baza danych) — jak pamiętamy, biblioteka COM niekoniecznie musi takową synchronizację zapewnić. W roli mechanizmu synchronizacyjnego wykorzystaliśmy sekcję krytyczną. Właścicielem tej sekcji jest obiekt zagnieżdżonej klasy TCOMCriticalSection::Lock tworzony na początku metody i automatycznie zwalniany po jej zakończeniu.
Szczegóły implementacyjne algorytmu ustalenia znaku zodiaku ukryte są w treści funkcji GetZodiacSign(), która w naszym wariancie prezentuje się następująco:
Wydruk 16.3 Ustalenie znaku zodiaku na podstawie daty
static void GetZodiacSign(long Day, long Month, BSTR& Sign)
{
unsigned short year = ((Month == 1) && (Day <= 19)) ? 2001 : 2000;
// Zapytanie SQL
static const LPCTSTR SQL =
_T("SELECT Name FROM ZODIAC WHERE (CAST(\"%s\" AS DATE) BETWEEN StartingDate AND EndingDate)");
TDateTime BirthDay(year,
static_cast<unsigned short>(Month),
static_cast<unsigned short>(Day));
ZodiacDataModule->QZodiac->Active = FALSE;
ZodiacDataModule->QZodiac->SQL->Clear();
ZodiacDataModule->QZodiac->SQL->Add(
Format(SQL, ARRAYOFCONST(( BirthDay.DateString().c_str() ))) );
ZodiacDataModule->QZodiac->Active = TRUE;
if (ZodiacDataModule->QZodiac->RecordCount != 1)
throw Exception(_T("Invalid arguments"));
ZodiacDataModule->QZodiac->First();
WideString wstrSign =
ZodiacDataModule->QZodiac->FieldByName(_T("Name"))->AsString.c_str();
Sign = wstrSign.Detach();
}
Obiekt WideString jest otoczką łańcucha typu BSTR. Obiekt ten jest właścicielem przedmiotowego łańcucha, jednakże ponieważ parametr Sign jest parametrem wyjściowym i za jego zwolnienie odpowiedzialna jest aplikacja-klient (a nie sam obiekt WideString) jest on odłączany (Detach()) od swego macierzystego obiektu.
Ulepszenie obsługi błędów
Wielu programistów zwykle nie docenia znaczenia prawidłowej obsługi błędów w aplikacji. Na gruncie COM zagadnienie to staje się o tyle istotniejsze, iż ze względu na odizolowanie od siebie poszczególnych komponentów aplikacji (rezydujących często na różnych komputerach) ich błędne działanie może przez dłuższy czas pozostawać niezauważone. Ponadto oryginalna specyfikacja COM nie definiuje żadnych klas wyjątków (co notabene czyni np. CORBA), tak więc implementacje wszystkich metod każdego interfejsu powinny być zamknięte w ramy bloków try…catch z prostego powodu — ewentualne wyjątki nie mogą się „wydostać” poza granice tychże metod. Poszczególne komponenty aplikacji COM mogą być przecież stworzone w różnych językach i nietrudno sobie wyobrazić sytuację, gdy nieobsłużony wyjątek C++ przedostaje się na grunt np. Visual Basica.
Wyjątkiem od tej zasady mogą być jedynie proste funkcje, w których wyjątki nie mają szans wystąpienia. C++Builder umieszcza wspomniane bloki try…catch w niektórych fragmentach generowanego przez siebie kodu — nawet jednak tam, gdzie tego nie czyni, powinniśmy sami takową obsługę zorganizować.
Jak tu jednak mówić o prawdziwej obsłudze błędów, skoro jedyną (wydawałoby się) możliwością ich sygnalizacji jest ustawienie wyniku funkcji (HRESULT)?
Aby zniwelować tę niedogodność COM, zaprojektowano interfejs o nazwie ISupportErrorInfo:
interface ISupportErrorInfo : IUnknown
{
HRESULT InterfaceSupportErrorInfo([in] REFIID riid);
}
W przypadku wystąpienia błędu należy wywołać funkcję COM o nazwie CreateErrorInfo(), w wyniku czego utworzony zostanie obiekt ErrorInfo związany z obsługą tego błędu; wynikiem funkcji jest wskazanie na interfejs ICreateErrorInfo(), którego metody umożliwiają wpisanie do wspomnianego obiektu dokładniejszych informacji o błędzie, jak np. identyfikatora odnośnego interfejsu czy też werbalnego opisu błędnej sytuacji.
Dostęp do tychże informacji możliwy jest dzięki metodom interfejsu IErrorInfo wspomnianego obiektu; integracji tego ostatniego z bieżącym wątkiem dokonuje funkcja COM o nazwie SetErrorInfo().
Aplikacja klienta może łatwo uzyskać rozszerzoną informację o błędzie na podstawie wartości HRESULT wywołując funkcję GetErrorInfo(). Funkcja ta zwraca wskazanie na interfejs IErrorInfo obiektu ErrorInfo ostatnio skojarzonego z bieżącym wątkiem.
Wykorzystanie powyższych mechanizmów staje się wielce ułatwione dzięki bibliotece ATL — jedyne, co należy uczynić w celu sygnalizacji błędu serwera, to wywołanie metody Error() zdefiniowanej w CComCoClass. Uczyniliśmy tak w poprawionej implementacji metody GetZodiacSign():
Wydruk 16.4 Implementacja metody GetZodiacSign() obejmująca obsługę błędów serwera
STDMETHODIMP TZodiacImpl::GetZodiacSign(long Day, long Month, BSTR* Sign)
{
try
{
TCOMCriticalSection::Lock Lock(CS);
::GetZodiacSign(Day, Month, *Sign);
}
catch(Exception &e)
{
hResult = Error(e.Message.c_str(), IID_IZodiac, E_FAIL);
}
return hResult;
}
Parametrami wywołania metody Error() są: werbalny opis błędu (zawarty w zmiennej reprezentującej wyjątek), identyfikator interfejsu i kod zwracanej wartości, przekazywanej następnie jako wynik wywołania całej metody.
Pozostaje nam jeszcze tylko sedno sprawy, czyli zaimplementowanie metod interfejsu ISupportErrorInfo:
Zadeklaruj interfejs ISupportErrorInfo jako jedną z klas bazowych koklasy TZodiacImpl:
class ATL_NO_VTABLE TZodiacImpl :
public CComObjectRootEx<CComMultiThreadModel>,
public CComCoClass<TZodiacImpl, &CLSID_Zodiac>,
public IConnectionPointContainerImpl<TZodiacImpl>,
public TEvents_Zodiac<TZodiacImpl>,
public IDispatchImpl<IZodiac, &IID_IZodiac, &LIBID_ZodiacServer>,
public ISupportErrorInfo
Dodaj pozycję reprezentującą interfejs ISupportErrorInfo do mapy COM:
BEGIN_COM_MAP(TZodiacImpl)
COM_INTERFACE_ENTRY(IZodiac)
COM_INTERFACE_ENTRY2(IDispatch, IZodiac)
COM_INTERFACE_ENTRY_IMPL(IConnectionPointContainer)
COM_INTERFACE_ENTRY(ISupportErrorInfo)
END_COM_MAP()
Zadeklaruj w koklasie metody interfejsu ISupportErrorInfo:
class ATL_NO_VTABLE TZodiacImpl :
…
…
public:
STDMETHOD(InterfaceSupportsErrorInfo)(REFIID riid);
…
Zaimplementuj metody interfejsu ISupportErrorInfo w koklasie w następujący sposób:
STDMETHODIMP TZodiacImpl::InterfaceSupportsErrorInfo(REFIID riid)
{
static const IID* arr[] =
{
&IID_IZodiac
};
for (int i=0; i < (sizeof(arr) / sizeof(arr[0])); i++)
{
if (InlineIsEqualGUID(*arr[i],riid))
return S_OK;
}
return S_FALSE;
}
Implementacja metody generującej zdarzenie
Implementacja metody GetZodiacSignAsync() podlega tym samym regułom, co prezentowana przed chwilą implementacja metody GetZodiacSign(). Jej treść przedstawia wydruk 16.5.
Wydruk 16.5 Implementacja metody GetZodiacSignAsync()
STDMETHODIMP TZodiacImpl::GetZodiacSignAsync(long Day, long Month)
{
try
{
TCOMCriticalSection::Lock Lock(CS);
BSTR Sign = NULL;
::GetZodiacSign(Day, Month, Sign);
Fire_OnZodiacSignReady(Sign);
SysFreeString(Sign);
}
catch(Exception &e)
{
return Error(e.Message.c_str(), IID_IZodiac, E_FAIL);
}
return S_OK;
};
Metoda uzyskuje żądaną nazwę znaku zodiaku za pośrednictwem swej „synchronicznej” koleżanki i przekazuje tę nazwę jako parametr do metody generującej zdarzenie. Należy przy tym zwrócić uwagę na pewien subtelny aspekt zarządzania pamięcią na potrzeby tegoż parametru — serwer dokonuje zarówno przydziału, jak i zwolnienia tej pamięci, bowiem z punktu widzenia klienta parametr ten jest parametrem wejściowym (generowanie zdarzenia polega bowiem tak naprawdę na wywołaniu odpowiedniej metody ujścia zdarzeń po stronie klienta).
Na obecnym etapie konstrukcji nasz obiekt COM jest zdolny do przyjmowania wywołań ze strony klientów i generowania zdarzeń opartych na interfejsie dyspozycyjnym. Pokażemy teraz, jak wyposażyć go w możliwości generowania zdarzeń opartych na interfejsie wychodzącym — posłuży nam to do uzyskania dodatkowej informacji związanej z określonym znakiem zodiaku.
Implementacja „klasycznego” interfejsu
Edytor TLE, mimo niewątpliwego „zadomowienia” na gruncie Delphi i C++Buildera wciąż postrzegany jest jako narzędzie dalekie od dojrzałości i zawierające błędy, szczególnie pod względem obsługi „nie-dualnych” interfejsów. Przystępując do definiowania interfejsów wychodzących należy więc liczyć się z koniecznością bezpośredniego operowania kodem źródłowym C++ i IDL.
Rozpocznijmy od pewnej struktury, która używana będzie w roli parametru kilku metod interfejsu. Definicja tej struktury w języku IDL wygląda następująco:
typedef struct tagTDetailedZodiacSign
{
BSTR Sign;
long House;
BSTR Element;
BSTR Planet;
BSTR Details;
BSTR Advice;
} TDetailedZodiacSign;
Aby dodać ów nowy typ do biblioteki typu, należy wykonać kolejno następujące czynności:
Kliknąć w przycisk New Record (szósty od lewej) na pasku narzędziowym edytora TLE;
Zmienić nazwę utworzonego rekordu na TDetailedZodiacSign;
Dodać do rekordu kolejne pola struktury. Należy w tym celu każdorazowo podświetlić nazwę rekordu w lewym panelu i kliknąć w przycisk New Field na pasku narzędziowym (wskazany przez kursor myszki na rysunku 16.7).
Tu proszę wkleić rysunek z pliku orig-16-7.bmp
Rysunek 16.7 Dodawanie do biblioteki typu nowego rekordu i jego pól
Dodamy także nowy interfejs realizujący dostarczanie informacji szczegółowej. W tym celu należy:
Podświetlić w lewym panelu najwyższą pozycję (ZodiacServer) i kliknąć w przycisk New Interface (pierwszy z lewej) na pasku narzędziowym edytora TLE;
Zmienić nazwę nowo utworzonego interfejsu na IDetailedZodiac;
Podświetlić interfejs IDetailedZodiac na lewym panelu, następnie przejść do karty Attributes prawego panelu i upewnić się, że interfejsem bazowym jest IUnknown, nie IDispatch (rys. 16.8).
Tu proszę wkleić rysunek z pliku orig-16-8.bmp
Rysunek 16.8 Dodawanie do biblioteki typu interfejsu IDetailedZodiac
Przejść na kartę Flags prawego panelu i zlikwidować zaznaczenie opcji Dual i Ole Automation; jeżeli opcje te okażą się niedostępne, należy przejść na kartę Text i zlikwidować wymienione flagi w definicji interfejsu.
Kolejną czynnością będzie dodanie do biblioteki dwóch następujących metod:
HRESULT _stdcall GetDetailedZodiacSign([in] long Day, [in] long Month, [out] TDetailedZodiacSign * DetailedSign);
HRESULT _stdcall GetDetailedZodiacSignAsync ([in] long Day, [in] long Month);
Zauważ, iż typ trzeciego parametru pierwszej metody jest wskazaniem na zdefiniowaną uprzednio strukturę; sposób wyboru typu parametru w oknie TLE przedstawia rysunek 16.9.
Tu proszę wkleić rysunek z pliku orig-16-9.bmp
Rysunek 16.9 Wybór typu parametru metody
Interfejs został już zdefiniowany, należy jeszcze tylko sprawić, by był on implementowany przez koklasę Zodiac. W tym celu należy kolejno wykonać poniższe czynności:
W lewym panelu podświetlić koklasę Zodiac.
Przejść na kartę Implements prawego panelu; karta ta zawiera listę interfejsów implementowanych przez koklasę.
Z menu kontekstowego (uruchamianego prawym kliknięciem) wybrać opcję Insert Interface, w wyniku czego pojawi się okno dialogowe Insert Interface (rys. 16.10).
Wybrać z okna dialogowego interfejs IDetailedZodiac; w wyniku tego wyboru interfejs dodany zostanie do listy implementowanych interfejsów,
Zapisać poczynione zmiany (File|Save All).
Tu proszę wkleić rysunek z pliku orig-16-10.bmp
Rysunek 16.10 Wybór interfejsu do implementacji przez koklasę
Po zapisaniu zmian otwórzmy plik ZodiacImpl.h i zobaczmy, cóż w nim przybyło nowego. W szczególności zwróćmy uwagę na makro określające związek koklasy z nowym interfejsem:
class ATL_NO_VTABLE TZodiacImpl :
public CComObjectRootEx<CComMultiThreadModel>,
public CComCoClass<TZodiacImpl, &CLSID_Zodiac>,
public IConnectionPointContainerImpl<TZodiacImpl>,
public TEvents_Zodiac<TZodiacImpl>,
public IDispatchImpl<IZodiac, &IID_IZodiac, &LIBID_ZodiacServer>,
public TEvents_DetailedZodiac<TZodiacImpl>,
public ISupportErrorInfo,
DUALINTERFACE_IMPL(Zodiac, IDetailedZodiac)
…
Makro to niedwuznacznie sugeruje, iż koklasa Zodiac implementuje dualny interfejs IDetailedZodiac — ten ostatni nie jest jednak interfejsem dualnym! To właśnie jeden z przejawów błędnego funkcjonowania TLE, wymagający ręcznej ingerencji w generowany kod:
class ATL_NO_VTABLE TZodiacImpl :
public CComObjectRootEx<CComMultiThreadModel>,
public CComCoClass<TZodiacImpl, &CLSID_Zodiac>,
public IConnectionPointContainerImpl<TZodiacImpl>,
public TEvents_Zodiac<TZodiacImpl>,
public IDispatchImpl<IZodiac, &IID_IZodiac, &LIBID_ZodiacServer>,
public TEvents_DetailedZodiac<TZodiacImpl>,
public ISupportErrorInfo,
public IDetailedZodiac
…
Ten sam błąd występuje również w mapie COM:
BEGIN_COM_MAP(TZodiacImpl)
COM_INTERFACE_ENTRY(IZodiac)
COM_INTERFACE_ENTRY2(IDispatch, IZodiac)
DUALINTERFACE_ENTRY(IDetailedZodiac)
COM_INTERFACE_ENTRY_IMPL(IConnectionPointContainer)
COM_INTERFACE_ENTRY(ISupportErrorInfo)
END_COM_MAP()
Poprawna jej postać powinna wyglądać następująco:
BEGIN_COM_MAP(TZodiacImpl)
COM_INTERFACE_ENTRY(IZodiac)
COM_INTERFACE_ENTRY2(IDispatch, IZodiac)
COM_INTERFACE_ENTRY(IDetailedZodiac)
COM_INTERFACE_ENTRY_IMPL(IConnectionPointContainer)
COM_INTERFACE_ENTRY(ISupportErrorInfo)
END_COM_MAP()
Teraz należy przystąpić do implementacji metod interfejsu IDetailedZodiac. Jako że implementacja metody GetDetailedZodiacSign() nie wnosi do naszej dyskusji żadnych nowych elementów, nie będziemy jej tutaj omawiać; zainteresowani Czytelnicy znajdą tę implementację na załączonym CD-ROMie. Metoda GetDetailedZodiacSignAsync(), mimo iż podobna do GetZodiacSignAsync(), powoduje generowanie zdarzeń i z tego względu warta jest odrębnego omówienia.
Generowanie zdarzeń serwera
Dlaczego w ogóle zajmować się zdarzeniami generowanymi przez serwer i wykorzystywać je w swych aplikacjach? Odpowiedź jest prosta: ze względów efektywności. Wywołanie metody koklasy za pośrednictwem interfejsu dyspozycyjnego obejmuje zawsze dodatkowy poziom sterowania: należy mianowicie uzyskać numer dyspozycyjny metody (dispid) i wywołać metodę Invoke() interfejsu IDispatch. Wywołanie metody Invoke() związane jest z przekazaniem dużej liczby parametrów określających m.in. numer dyspozycyjny metody, typ wywołania i zestaw parametrów samej metody — te ostatnie mają w dodatku typ wariantowy, co wymaga dodatkowych konwersji. Programista używający narzędzi w rodzaju C++Buildera zazwyczaj nie jest świadom tej złożoności, bowiem cały scenariusz wywołania metody jest przed nim po prostu ukryty, przynajmniej częściowo. Wspomniane narzuty czasowe (i pamięciowe) istnieją jednak niezależnie od świadomości programisty, a chcąc ich uniknąć, musimy uciec się właśnie do mechanizmu zdarzeń.
Zaletą interfejsów dyspozycyjnych jest natomiast późny charakter wiązania (ang. late binding) wywoływanych metod. Wywołania takie, jakkolwiek powolne, używane są przez języki skryptowe, jak JavaScript czy VBScript.
Znając już przesłankę generowania zdarzeń opartych na „klasycznych” interfejsach, przystąpmy do implementacji związanych z nimi metod. Deklaracja IDL implementowanego interfejsu powinna wyglądać mniej więcej tak:
[
uuid(DAD75927-5174-11D5-9351-A808C0525A5B),
version(1.0)
]
interface IDetailedZodiacEvents: IUnknown
{
[
id(0x00000001)
]
HRESULT _stdcall OnDetailedZodiacSignReady([in] TDetailedZodiacSign * DetailedSign );
};
Używając edytora TLE, należy w znany już sposób wprowadzić definicję zarówno samego interfejsu, jak i jego metody. Stan biblioteki po wykonaniu tych czynności przedstawia rysunek 16.11.
Tu proszę wkleić rysunek z pliku orig-16-11.bmp
Rysunek 16.11 Stan biblioteki typu po dodaniu interfejsu IDetailedZodiacEvents
Każdy interfejs może mieć dwojakiego rodzaju odniesienie do implementującej go koklasy: może być mianowicie wychodzący (ang. outgoing) lub przychodzący (ang. ingoing); spośród dotychczas definiowanych interfejsów żaden nie został jeszcze sklasyfikowany w tej kategorii. Aby zaznaczyć, iż koklasa implementuje interfejs IDetailedZodiacEvents jako wychodzący, należy wykonać kolejno następujące czynności:
Podświetlić koklasę Zodiac w lewym panelu;
Przejść na kartę Implements i za pomocą menu kontekstowego dodać interfejs IDetailedZodiacEvents do listy interfejsów implementowanych przez koklasę;
Kliknąć prawym przyciskiem w linię reprezentującą interfejs IDetailedZodiacEvents na karcie Implements i z menu kontekstowego wybrać opcję Source (rys. 16.12);
Zapisać poczynione zmiany.
Tu proszę wkleić rysunek z pliku orig-16-12.bmp
Rysunek 16.12 Kwalifikowanie interfejsu jako „wychodzący”
Jeżeli teraz spojrzymy do pliku ZodiacImpl.h, nie zobaczymy tam niczego nowego! C++Builder nie tworzy bowiem żadnego kodu związanego z obsługą zdarzeń generowanych przez klasyczny interfejs. Całość implementacji tego rodzaju zdarzeń musi być wykonana przez programistę.
Jeżeli uważnie przeczytasz implementację klasy TEvents_Zodiac w pliku ZodiacServer_TLB.h, stworzenie podobnej implementacji dla klasy TEvents_DetailedZodiac nie będzie takie trudne. Jedno z możliwych rozwiązań przedstawia wydruk 16.6.
Wydruk 16.6 Implementacja klasy-szablonu TEvents_DetailedZodiac
class TEvents_DetailedZodiac :
public IConnectionPointImpl<T,
&IID_IDetailedZodiacEvents,
CComDynamicUnkArray>
{
public:
HRESULT Fire_OnDetailedZodiacSignReady(TDetailedZodiacSign* DetailedSign)
{
T * pT = (T*)this;
pT->Lock();
IUnknown ** pp = m_vec.begin();
while (pp < m_vec.end())
{
if (*pp != NULL)
{
CComQIPtr<IDetailedZodiacEvents,
&IID_IDetailedZodiacEvents> ptrEvents = *pp;
if (ptrEvents != NULL)
ptrEvents->OnDetailedZodiacSignReady(DetailedSign);
}
pp++;
}
pT->Unlock();
return S_OK;
}
};
Jedyną istotną różnicą w stosunku do klasy TEvents_Zodiac jest brak obiektu pośredniczącego m_EventIntfObj. Wywołanie metody OnDetailedZodiacSignReady() odnoszone jest bezpośrednio do interfejsu — konstrukcja
CComQIPtr<IDetailedZodiacEvents, &IID_IDetailedZodiacEvents> ptrEvents = *pp;
to nic innego, jak uzyskanie wskaźnika do implementowanego interfejsu IDetailedZodiacEvents na podstawie znanego wskaźnika do interfejsu IUnknown. Żądanie nowego interfejsu (QueryInterface) dokonywane jest automatycznie przez konstruktor klasy CComQIPtr; w analogiczny sposób destruktor tej klasy sygnalizuje chęć zwolnienia interfejsu (Release).
Klasa-szablon TEvents_DetailedZodiac może być wykorzystywana w charakterze punktu wyjściowego do definiowania obsługi własnych zdarzeń opartych na interfejsie— za cenę niewielkich modyfikacji w kodzie źródłowym.
Dotarliśmy wreszcie do ostatniego elementu naszej układanki: należy mianowicie zmodyfikować deklarację klasy TZodiacImpl (w pliku ZodiacImpl.h) tak, by uwidocznić jej związek z klasą TEvents_DetailedZodiac:
class ATL_NO_VTABLE TZodiacImpl :
public CComObjectRootEx<CComMultiThreadModel>,
public CComCoClass<TZodiacImpl, &CLSID_Zodiac>,
public IConnectionPointContainerImpl<TZodiacImpl>,
public TEvents_Zodiac<TZodiacImpl>,
public IDispatchImpl<IZodiac, &IID_IZodiac, &LIBID_ZodiacServer>,
public ISupportErrorInfo,
public IDetailedZodiac,
public TEvents_DetailedZodiac<TZodiacImpl>
…
oraz dodać do mapy COM pozycję odpowiadającą interfejsowi IDetailedZodiacEvents:
BEGIN_CONNECTION_POINT_MAP(TZodiacImpl)
CONNECTION_POINT_ENTRY(DIID_IZodiacEvents)
CONNECTION_POINT_ENTRY(IID_IDetailedZodiacEvents)
END_CONNECTION_POINT_MAP()
…
Nasz obiekt COM stał się tym samym zdolnym do generowania zdarzeń za pośrednictwem interfejsu IDetailedZodiacEvents z dowolnego miejsca kodu. Należy jeszcze tylko dodać do implementacji metody GetDetailedZodiacAsync()wywołanie funkcji Fire_OnDetailedZodiacSignReady()(w pliku ZodiacImpl.cpp) — zdarzenie informujące klienta o tym, iż zakończyła się asynchroniczna operacja, generowane jest tym razem za pomocą interfejsu „klasycznego” nie interfejsu dyspozycyjnego:
Wydruk 16.7 Implementacja metody TZodiacImpl::GetDetailedZodiacSignAsync()
struct TDetailedZodiacSignImpl : public TDetailedZodiacSign
{
TDetailedZodiacSignImpl()
{
Sign = NULL;
Element = NULL;
Element = NULL;
Planet = NULL;
Details = NULL;
Details = NULL;
Advice = NULL;
}
~TDetailedZodiacSignImpl()
{
if (Sign != NULL)
SysFreeString(Sign);
if (Element != NULL)
SysFreeString(Element);
if (Planet != NULL)
SysFreeString(Planet);
if (Details != NULL)
SysFreeString(Details);
if (Advice != NULL)
SysFreeString(Advice);
}
};
…
…
STDMETHODIMP TZodiacImpl::GetDetailedZodiacSignAsync(long Day, long Month)
{
try
{
TCOMCriticalSection::Lock Lock(CS);
TDetailedZodiacSignImpl DetailedSign;
::GetDetailedZodiacSign(Day, Month, DetailedSign);
Fire_OnDetailedZodiacSignReady(&DetailedSign);
}
catch(Exception &e)
{
return Error(e.Message.c_str(), IID_IDetailedZodiac, E_FAIL);
}
return S_OK;
};
Zewnętrzna funkcja ::GetDetailedZodiacSign() dokonuje wypełnienia struktury TDetailedZodiacSignImpl() stosownymi wartościami. Zwróć uwagę, iż metoda GetDetailedZodiacSignAsync() wywoływana jest tak naprawdę przez klienta — klient i serwer zamieniają się więc rolami: wspomniana struktura, jako parametr wejściowy, musi być zarówno zaalokowana, jak i zwolniona przez serwer, co tłumaczy obecność destruktora ~TDetailedZodiacSignImpl().
Na tym zakończyliśmy konstruowanie serwera COM. Należy teraz skompilować jego projekt oraz zarejestrować go za pomocą opcji Run|Register ActiveX Server z menu głównego IDE. Jeżeli kompilacja i rejestracja przebiegną pomyślnie, C++Builder wyświetli informujący tym komunikat (Successfully registered ActiveX Server …).
Tworzenie DLL dla proxy i stub'a
W sytuacji, gdy klient i serwer rezydują w oddzielnych apartamentach, oddzielnych procesach albo różnych komputerach, przesyłane pomiędzy nimi dane podlegają procesowi tzw. transpozycji (ang. marshaling). Dwoma najistotniejszymi elementami tego procesu są: pełnomocnik, określany częściej jako proxy oraz tzw. pień, zwany z angielska stub.
Proxy istnieje zawsze w przestrzeni adresowej klienta i odpowiedzialny jest za właściwe pakowanie parametrów przekazywanych do wywoływanych metod w określony sposób (poprzez sieć, poprzez komunikację międzyprocesową lub poprzez granice pomiędzy apartamentami).
Dla odmiany stub rezyduje w przestrzeni adresowej serwera i wykonuje czynność dokładnie odwrotną, czyli rozpakowanie otrzymanych parametrów, zwane detranspozycją (ang. unmarshaling) i przekazanie ich do bezpośrednio wywoływanych metod serwera.
Opisany mechanizm przedstawia schematycznie rysunek 16.13.
Tu proszę wykonać rysunek 16.13 ze strony 693 oryginału z następującymi zmianami:
Client -> klient
Server -> serwer
Parameter (powinno być: Parameters) -> parametry
Proxy (Packs) -> proxy (pakowanie)
Stub (unpacks) -> stub (rozpakowanie)
Packed Parameter (powinno być: Packed Parameters) -> spakowane parametry
Wyrazy „proxy” i „stub” powinny być złożone pochyłą czcionką.
Rysunek 16.13 Idea transpozycji i detranspozycji danych
Obecnie istnieją trzy odmiany transpozycji danych. Transpozycja bibliotek typu (Type Library Marshaling) dostępna jest automatycznie za pośrednictwem interfejsów dyspozycyjnych i specyficznych interfejsów automatyzacji OLE; związany z nią kod znajduje się w bibliotece oleaut32.dll. Implementacja transpozycji standardowej (Standard Marshaling) jest elementem konstrukcji własnych proxy i stub'ów posługujących się specyficznymi interfejsami nie związanymi z automatyzacją OLE — tym właśnie zajmiemy się w niniejszej sekcji. Trzeci rodzaj transpozycji — transpozycja specyficzna (Custom Marshaling) — dostępna jest dla obiektów COM poprzez interfejs IMarshall.
Bywa, iż proxy i stub postrzegane są w kontekście serwerów i koklas — tymczasem tak naprawdę są one raczej związane z interfejsami, zajmują się bowiem fizyczną postacią parametrów metod interfejsów (i ewentualnych wyników tych metod). Rejestracja biblioteki DLL zawierającej implementację proxy i stub'a stanowi w istocie dostarczenie implementacji procesu transpozycji danych dla interfejsów używanych przez tę bibliotekę. Gdy określony interfejs po raz pierwszy przystępuje do wykonania czynności wymagających transpozycji danych, COM dokonuje przeszukania rejestru systemowego w celu odnalezienia proxy i stub'a odpowiedzialnych za tę transpozycję; informacja ta jest oczywiście wykorzystywana przy następnych transpozycjach i detranspozycjach.
Zalecane jest zatem tworzenie pary „proxy-stub” dla każdego interfejsu „klasycznego” nie związanego z automatyzacją OLE — nawet wówczas, jeżeli wydaje się, iż aplikacja-klient nie wymaga transpozycji danych.
Scenariusz utworzenia proxy i stub'a dla interfejsów serwera ZodiacServer przedstawia się następująco:
Załaduj do IDE projekt serwera i wyświetl okno TLE za pomocą opcji View|Type Library menu głównego.
Kliknij w przycisk „Export to IDL” (patrz rysunek 16.14).
Zapisz zmiany poczynione w projekcie; C++Builder zapyta Cię o nazwę, pod która ma zapisać plik zawierający wygenerowany kod IDL — wpisz ZodiacServer.idl.
Zamknij projekt serwera.
Tu proszę wkleić rysunek z pliku orig-16-14.bmp
Rysunek 16.14 Eksportowanie kodu źródłowego IDL z edytora TLE
Wydruk 16.8 przedstawia fragmenty wygenerowanego kodu:
Wydruk 16.8 Plik źródłowy IDL serwera ZodiacServer
[
uuid(962A5640-4FF0-11D5-9351-D4F9944FAD58),
version(1.0),
helpstring("ZodiacServer Library")
]
library ZodiacServer
{
importlib("stdole2.tlb");
…
…
interface IZodiac: IDispatch
{
[
id(0x00000001)
]
HRESULT _stdcall GetZodiacSign([in] long Day,
[in] long Month,
[out, retval] BSTR * Sign );
[
id(0x00000002)
]
HRESULT _stdcall GetZodiacSigAsync([in] long Day, [in] long Month );
};
…
…
dispinterface IZodiacEvents
{
properties:
methods:
[
id(0x00000001)
]
HRESULT OnZodiacSignready([in] BSTR Sign );
};
…
…
typedef struct tagTDetailedZodiacSign
{
BSTR Sign;
long House;
BSTR Element;
BSTR Planet;
BSTR Details;
BSTR Advice;
} TDetailedZodiacSign;
…
…
coclass Zodiac
{
[default] interface IZodiac;
[default, source] dispinterface IZodiacEvents;
interface IDetailedZodiac;
[source] interface IDetailedZodiacEvents;
};
…
…
interface IDetailedZodiac: IUnknown
{
[
id(0x00000001)
]
HRESULT _stdcall GetDetailedZodiacSign(
[in] long Day,
[in] long Month,
[in] TDetailedZodiacSign * DetailedSign );
[
id(0x00000002)
]
HRESULT _stdcall GetDetailedZodiacSignAsync(
[in] long Day,
[in] long Month );
};
…
…
interface IDetailedZodiacEvents: IUnknown
{
[
id(0x00000001)
]
HRESULT _stdcall OnDetailedZodiacSignReady(
[in] TDetailedZodiacSign * DetailedSign );
};
};
To jeszcze nie koniec; wygenerowany kod IDL trzeba poddać jeszcze pewne obróbce wynikającej z faktu, iż kompilator IDL Microsoftu (MIDL) generuje kod proxy i stub'a tylko dla tych interfejsów, które definiowane są poza sekcją library pliku *.IDL. Nie stanowi to problemu w przypadku interfejsów dualnych zgodnych z automatyzacją OLE, ponieważ transpozycja danych na ich potrzeby wykonywana jest automatycznie przez COM (Type Library Marshaling); interfejsy „klasyczne” muszą być natomiast przesunięte poza wspomnianą sekcję. W tym celu należy jeszcze wykonać trzy poniższe czynności:
Uruchomić edycję pliku ZodiacServer.idl przy użyciu dowolnego edytora tekstowego.
Przesunąć poza sekcję library definicje wszystkich interfejsów nie oznakowanych jako zgodne z automatyzacją OLE i usunąć całą resztę kodu.
Umieścić na początku pliku dyrektywę importującą plik objidl.idl (plik ten znajduje się w katalogu Include\idl lokalnej instalacji C++Buildera).
To nie pomyłka w numeracji — powyższe trzy punkty stanowią dalszy ciąg poprzedniej części scenariusza
Ostateczną postać pliku ZodiacServer.idl po wykonaniu edycji przedstawia wydruk 16.9
Wydruk 16.9 Zmodyfikowany plik ZodiacServer.idl przystosowany dla kompilatora MIDL
import "objidl.idl"; // Standardowe definicje COM
[
uuid(962A5640-4FF0-11D5-9351-D4F9944FAD58),
version(1.0),
]
typedef struct tagTDetailedZodiacSign
{
BSTR Sign;
long House;
BSTR Element;
BSTR Planet;
BSTR Details;
BSTR Advice;
} TDetailedZodiacSign;
[
uuid(DAD75923-5174-11D5-9351-A808C0525A5B),
version(1.0)
]
interface IDetailedZodiac: IUnknown
{
[
id(0x00000001)
]
HRESULT _stdcall GetDetailedZodiacSign(
[in] long Day,
[in] long Month,
[out] TDetailedZodiacSign * DetailedSign );
[
id(0x00000002)
]
HRESULT _stdcall GetDetailedZodiacSignAsync(
[in] long Day, [in] long Month );
};
[
uuid(DAD75927-5174-11D5-9351-A808C0525A5B),
version(1.0)
]
interface IDetailedZodiacEvents: IUnknown
{
[
id(0x00000002)
]
HRESULT _stdcall OnDetailedZodiacSignReady(
[in] TDetailedZodiacSign * DetailedSign );
};
Kompilator MIDL (midl.exe) znajduje się w podkatalogu Bin lokalnej instalacji C++Buildera. W celu usprawnienia jego uruchamiania zaleca się stworzenie pliku wsadowego (.bat) zawierającego m.in. ścieżkę do programu i definicje związanych z nim zmiennych środowiskowych. Propozycję takiego pliku z ustawieniami specyficznymi dla pewnego komputera przedstawia wydruk 16.10 — przed użyciem należy oczywiście dostosować specyfikowane w nim ścieżki do konkretnej instalacji.
Wydruk 16.10 Plik wsadowy związany z kompilatorem midl.exe
rem All paths here defined are my own paths, in my PC. You must use yours:
rem Wszystkie specyfikowane ścieżki dotyczą mojego komputera. Zmień je
rem stosownie do swej własnej instalacji
@echo off
rem ścieżka dostępu do plików IDL
setINCPATH=c:\progra~1\borland\cbuild~1\include\;
* c:\progra~1\borland\cbuild~1\include\idl
rem ścieżka dostępu do preprocesora C++Buildera
set CPPPATH=c:\progra~1\borland\cbuild~1\bin
rem ścieżka dostępu do kompilatora midl.exe
set MIDLPATH=c:\progra~1\borland\cbuild~1\bin
rem wywołanie MIDL
@echo on
%MIDLPATH%\midl -ms_ext -I%INCPATH% -cpp_cmd%CPPPATH%\CPP32
* -cpp_opt "-P- -oCON -DREGISTER_PROXY_DLL -I%INCPATH%" %1
Opis przełączników i opcji kompilatora MIDL użytych w prezentowanym pliku wsadowym znaleźć można w systemie pomocy C++Buildera. Czytelników zainteresowanych pełniejszym opisem kompilatora odsyłamy do internetowych stron MSDN (http://msdn.microsoft.com).
Jedynym argumentem wywołania prezentowanego pliku wsadowego jest nazwa wejściowego pliku IDL, natomiast wynikiem pracy kompilatora MIDL są cztery poniższe pliki (zakładamy, że plik wejściowy ma nazwę ZodiacServer.idl):
ZodiacServer.h
ZodiacServer_i.c
ZodiacServer_p.c
dlldata.c
Dla zarejestrowania (lub wyrejestrowania) biblioteki DLL implementującej proxy i stub konieczne jest ponadto utworzenie pliku *.def odzwierciedlającego funkcje eksportowane z biblioteki. Nasz przykładowy plik ZodiacServerPS.def mógłby wyglądać następująco:
LIBRARY ZODIACSERVERPS
DESCRIPTION 'Zodiac Proxy/Stub DLL'
EXPORTS
DllGetClassObject PRIVATE
DllCanUnloadNow PRIVATE
DllRegisterServer PRIVATE
DllUnregisterServer PRIVATE
GetProxyDllInfo PRIVATE
Dokładna znajomość zawartości plików produkowanych przez kompilator MIDL nie jest bezwzględnie konieczna, ważne jest natomiast to, iż pliki te muszą zostać włączone do projektu tworzącego bibliotekę, podobnie zresztą jak plik *.def. Projekt ten inicjujemy oczywiście za pomocą kreatora DLL Wizard (dostępnego w oknie New Items), przy czym jako typ kodu źródłowego należy tym razem wybrać C (nie C++), zgodnie z rysunkiem 16.15.
Tu proszę wkleić rysunek z pliku orig-16-15.bmp
Rysunek 16.15 Opcje startowe projektu tworzącego bibliotekę proxy/stub DLL
Dalszy ciąg scenariusza tworzenia projektu przedstawia się następująco:
Usuń z projektu plik Unit1.c produkowany automatycznie przez kreator.
Dodaj do projektu plik *.c wyprodukowany przez MIDL oraz stworzony przed chwilą plik *.def.
W pole Conditional defines na karcie Directories/Conditionals opcji projektu wpisz następującą zawartość:
WIN32;_WINDOWS;_MBCS;_USRDLL;REGISTER_PROXY_DLL;_WIN32_WINNT=0x0400
lub następującą, jeżeli chcesz, by utworzony proxy był zgodny z Windows 95/98:
WIN32;_WINDOWS;_MBCS;_USRDLL;REGISTER_PROXY_DLL;_WIN32_DCOM
Zamknij okno opcji projektu i zachowaj projekt pod nazwą ZodiacServerPS.bpr.
Skompiluj projekt — rezultatem bezbłędnej kompilacji powinien być plik ZodiacServerPS.dll.
Zarejestruj utworzoną bibliotekę DLL jako serwer COM za pomocą programu regsvr32, podając pełną ścieżkę dostępu do biblioteki, na przykład
regsvr32 e:\BCPPB5\Zodiac\Server\ZodiacServerPS.dll
Nasz serwer jest już całkowicie gotowy na przyjmowanie żądań — zobaczmy więc, co dzieje się po stronie klienta.
Tworzenie klienta COM
Aplikacja klienta musi posiadać interfejs użytkownika umożliwiający wprowadzenie daty urodzenia oraz wyświetlenie nazwy znaku zodiaku i innych informacji związanych z tą datą, w sposób synchroniczny lub asynchroniczny. Formularz główny aplikacji przedstawia rysunek 16.16.
Tu proszę wkleić rysunek z pliku orig-16-16.bmp
Rysunek 16.16 Formularz główny aplikacji-klienta COM
Formularz ten można co prawda skonstruować samodzielnie bez większego trudu, jednak w tym miejscu skoncentrujemy się raczej na tych aspektach aplikacji, które charakterystyczne są dla technologii COM. Zainteresowani Czytelnicy znajdą kompletny kod źródłowy projektu na załączonym CD-ROMie.
Importowanie biblioteki typu
Aby aplikacja klienta posiadała informację na temat typów danych eksponowanych przez serwer ( w tym klas i interfejsów) konieczne jest zaimportowanie do niej biblioteki typów serwera. W tym celu należy kolejno:
Wybrać opcję Project|Import Type Library z menu głównego IDE; spowoduje to otwarcie okna dialogowego „Import Type Library” (rys. 16.17).
W wyświetlonym oknie usunąć zaznaczenie opcji Generate Component Wrapper, chcemy bowiem uzyskać jedynie kod w C++ odzwierciedlający zawartość biblioteki, nie zaś kod komponentu VCL instalowanego w Palecie.
Z listy bibliotek wybrać interesującą nas bibliotekę serwera — „ZodiacServer Library (Version 1.0)” i kliknąć w przycisk Create Unit. Spowoduje to wygenerowanie pliku o nazwie ZodiacServer_TLB.cpp i dodanie go do projektu.
Tu proszę wkleić rysunek z pliku orig-16-17.bmp
Rysunek 16.17 Importowanie biblioteki typu do aplikacji-klienta COM
Jeżeli porównamy dwie wersje kodu źródłowego biblioteki — tę oryginalną na serwerze i tę wygenerowaną przez kreator „Import Type Library” stwierdzimy (raczej bez specjalnego zdziwienia), iż są one identyczne; można by więc odnieść wrażenie, iż zamiast importować bibliotekę typu do aplikacji klienta można by po prostu …skopiować jej kod źródłowy. Rozumowanie takie nie uwzględnia jednak co najmniej dwóch istotnych faktów. Po pierwsze, specyfikacja COM jest standardem binarnym, a więc kod źródłowy poszczególnych komponentów aplikacji może być zwyczajnie niedostępny. Po drugie — specyfikacja COM niezależna jest od konkretnego języka programowania, a więc język, w którym bibliotekę oryginalnie zaprogramowano (np. C++) może być różny od języka, w którym chcemy uzyskać jej importowaną wersję (np. Object Pascala).
Podstawowym powodem, dla którego udostępnia się biblioteki typów, jest możliwość uzyskania specyficznych dla danego języka deklaracji używanych typów i komponentów — komponenty stają się wówczas samodokumentujące, i to na dowolnej platformie projektowej (o ile, rzecz jasna, posiada ona funkcje importowania bibliotek typów).
Przegląd wygenerowanych konstrukcji C++
Przyjrzyjmy się bliżej plikowi ZodiacServer_TLB.h, wygenerowanemu przez kreator importowy i zawierającemu deklarację używanych przez bibliotekę koklas i interfejsów. Wydruk 16.11 przedstawia fragment tego pliku dotyczący interfejsów IZodiac i IDetailedZodiac.
Wydruk 16.11 Deklaracje interfejsów IZodiac i IDetailedZodiac w pliku ZodiacServer_TLB.h
…
…
interface IZodiac : public IDispatch
{
public:
virtual HRESULT STDMETHODCALLTYPE GetZodiacSign(
long Day/*[in]*/,
long Month/*[in]*/,
BSTR* Sign/*[out,retval]*/) = 0; // [1]
virtual HRESULT STDMETHODCALLTYPE GetZodiacSignAsync(
long Day/*[in]*/,
long Month/*[in]*/) = 0; // [2]
#if !defined(__TLB_NO_INTERFACE_WRAPPERS)
BSTR __fastcall GetZodiacSign(long Day/*[in]*/, long Month/*[in]*/)
{
BSTR Sign = 0;
OLECHECK(this->GetZodiacSign(Day, Month, (BSTR*)&Sign));
return Sign;
}
#endif // __TLB_NO_INTERFACE_WRAPPERS
};
…
…
interface IDetailedZodiac : public IUnknown
{
public:
virtual HRESULT STDMETHODCALLTYPE GetDetailedZodiacSign(
long Day/*[in]*/,
long Month/*[in]*/,
Zodiacserver_tlb::TDetailedZodiacSign*DetailedSign/*[out]*/) = 0; // [1]
virtual HRESULT STDMETHODCALLTYPE GetDetailedZodiacSignAsync(
long Day/*[in]*/, long Month/*[in]*/) = 0; // [2]
};
…
…
Nietrudno zauważyć informację o pochodzeniu obydwu interfejsów: IZodiac, jako interfejs dualny, wywodzi się z IDispatch, natomiast „klasyczny” interfejs IDetailedZodiac wywodzi się z IUnknown. W ramach interfejsu IZodiac widzimy również drugą, przeciążoną postać metody GetZodiacSign(). Wynikowy łańcuch jest tutaj wprost wynikiem funkcji, w odróżnieniu od pierwotnego aspektu przekazującego ów łańcuch przez trzeci parametr, rezerwując dla wyniku (typu HRESULT) informację o powodzeniu lub niepowodzeniu operacji. Drugi, przeciążony aspekt funkcji i tak korzysta z owego pierwotnego aspektu, kontrolując poprawność jego wywołania (tj. zwrócenie wartości 0) za pomocą makra OLECHECK.
Niestety, makro OLECHECK implementowane jest jako połączenie obsługi błędów z generowaniem asercji, nie wykorzystując przy tym interfejsu IErrorInfo, którego metody serwer mógłby implementować (jak czyni to serwer ZodiacServer). Produkowany przez makro komunikat, zawierający werbalny opis zaistniałego wyjątku jest więc raczej użyteczny jedynie dla celów śledzenia aplikacji, nie zaś dla potrzeb projektanta aplikacji COM.
Jako alternatywę dla makra OLECHECK umieściliśmy na załączonym CD-ROMie plik ComThrow.h zawierający kilka map przydatnych w obsłudze błędów zaistniałych w aplikacjach COM. Oprócz implementacji wspomnianych makr plik ten zawiera również opis ich użycia, wraz z prostymi przykładami.
Aby uprościć projektowanie aplikacji klienta, zamiast bezpośrednich odwołań do interfejsów zastosowaliśmy specjalne klasy-szablony, stanowiące dla tych interfejsów swoiste otoczki i z tego względu zwane „eleganckimi” interfejsami (ang. smart interfaces) — zamiast bowiem interfejsów mamy do czynienia z wygodniejszymi klasami C++. Przykład takiego „eleganckiego interfejsu” — TCOMIZodiac — przedstawia wydruk 16.12.
Wydruk 16.12 Deklaracja „eleganckiego” interfejsu TCOMIZodiac
template <class T /* IZodiac */ >
class TCOMIZodiacT : public TComInterface<IZodiac>, public TComInterfaceBase<IUnknown>
{
public:
TCOMIZodiacT() {}
TCOMIZodiacT(IZodiac *intf, bool addRef = false) : TComInterface<IZodiac>(intf, addRef) {}
TCOMIZodiacT(const TCOMIZodiacT& src) : TComInterface<IZodiac>(src) {}
TCOMIZodiacT& operator=(const TCOMIZodiacT& src) { Bind(src, true); return *this;}
HRESULT __fastcall GetZodiacSign(
long Day/*[in]*/,
long Month/*[in]*/,
BSTR* Sign/*[out,retval]*/);
BSTR __fastcall GetZodiacSign(
long Day/*[in]*/,
long Month/*[in]*/);
HRESULT __fastcall GetZodiacSignAsync(
long Day/*[in]*/,
long Month/*[in]*/);
};
typedef TCOMIZodiacT<IZodiac> TCOMIZodiac;
TCOMIZodiac jest konkretyzacją szablonu TCOMIZodiacT; cenę płaconą za wygodę użytkowania stanowi tu kilka dodatkowych metod i operatorów. Najważniejsze z operatorów dziedziczone są z szablonu TComInterface, którego konkretyzacja TComInterface<IZodiac> jest jednym z przodków klasy TCOMIZodiacT. TComInterface implementuje mianowicie operatory „*” oraz „->” zwracające wskazanie na odnośny interfejs;
class TComInterface
{
public:
TComInterface() : intf(0)
{}
…
…
operator T* () const
{
return intf;
}
T& operator*()
{
_ASSERTE_(intf!=0 /* Don't allow *() of smart interface with NULL pointer
interface */);
return *intf;
}
…
…
T* operator->() const
{
_ASSERTE_(intf != 0 /* Don't allow ->() of smart interface with NULL pointer
interface */);
return intf;
}
T* operator->()
{
_ASSERTE_(intf != 0 /*Don't allow ->() of smart interface with NULL pointer
interface */);
return intf;
}
Dodatkowo definiuje on operator przypisania, który przed przypisaniem zmiennej intf nowego wskazania zwalnia interfejs aktualnie wskazywany:
template <class ANOTHERINTF, const IID* ANOTHERIID>
TComInterface<T, piid>& operator=(const TComInterface<ANOTHERINTF,
ANOTHERIID>& src)
{
_ASSERTE_(/* Need have valid IID to invoke this */GetIID() != GUID_NULL);
Reset();
if (src)
{
OLECHECK(src->QueryInterface(GetIID(), (LPVOID*)(&intf)));
}
return *this;
}
Do inicjowania „eleganckich” interfejsów służy specjalizowany szablon-kreator TCoClassCreatorT umożliwiający łatwe tworzenie obiektów COM. Jest on zdefiniowany w pliku utilcls.h w sposób pokazany na wydruku 16.13
Wydruk 16.13 Deklaracja szablonu TCoClassCreatorT
template <class TOBJ, class INTF, const CLSID* clsid, const IID* iid>
class TCoClassCreatorT : public CoClassCreator
{
public:
static TOBJ Create();
static HRESULT Create(TOBJ& intfObj);
static HRESULT Create(INTF** ppintf);
static TOBJ CreateRemote(LPCWSTR machineName);
static HRESULT CreateRemote(LPCWSTR machineName, TOBJ& intfObj);
static HRESULT CreateRemote(LPCWSTR machineName, INTF** ppIntf);
};
Co prawda zgodnie z opisem w pliku pomocy C++Buildera TCoClassCreator zaprojektowany został na potrzeby tworzenia obiektów implementujących interfejsy dualne, jednakże równie dobrze spisuje się on w odniesieniu do obiektów eksponujących jedynie „klasyczne” interfejsy.
„Eleganckie” interfejsy i kreatory, mimo iż oparte na szablonach, nie są częścią biblioteki ATL, lecz rodzimymi elementami C++Buildera.
Najważniejszą metodą szablonu TCoClassCreatorT jest statyczna metoda Create(), wewnętrznie wywołująca funkcję ::CoCreateInstance() dla zbudowania obiektu klasy, której otoczkę szablon stanowi.
Inna metoda kreatora TCoClassCreatorT — CreateRemote() — związana jest z technologią DCOM, nie zaleca się jednak jej stosowania, bowiem nazwy komputerów nie poddają się w łatwy sposób skalowaniu; przeczy to niejako „przezroczystemu” charakterowi technologii COM. W zamian proponuje się wykorzystanie narzędzi administratora DCOM (DCOMCnfg).
Klasa CoZodiac jest konkretyzacją szablonu kreatora, deklarowaną w pliku ZodiacServer_TLB.h:
class TCoClassCreatorT
<TCOMIZOdiac, IZOdiac, &CLSID_Zodiac, &IID_Zodiac> CoZodiac;
Tworzenie i wykorzystywanie obiektu COM serwera
Dla celów utrzymywania i kontrolowania obiektu serwera z poziomu aplikacji-klienta należy utworzyć w głównym formularzu pole typu TCOMIZodiac:
class TMainForm : public TForm
{
__published: // IDE-managed Components
TMonthCalendar *FCalendar;
TButton *btnZodiac;
TButton *btnDetailedZodiac;
TCheckBox *chkAsync;
TMemo *memLog;
TLabel *Label1;
TButton *btnClear;
TLabel *Label2;
void __fastcall btnZodiacClick(TObject *Sender);
void __fastcall btnDetailedZodiacClick(TObject *Sender);
void __fastcall FormCreate(TObject *Sender);
void __fastcall btnClearClick(TObject *Sender);
private: // User declarations
TCOMIZodiac FZodiac;
TZodiacSink FZodiacSink;
TZodiacCustomSink FZodiacCustomSink;
protected:
void __fastcall OnZodiacSignReady(BSTR Sign);
void __fastcall OnDetailedZodiacSignReady(TDetailedZodiacSign& DetailedSign);
public: // User declarations
__fastcall TMainForm(TComponent* Owner);
};
Zwróć uwagę, iż do deklaracji formularza włączone zostały komponenty interfejsu użytkownika, co umożliwi swobodny dostęp do nich z pozostałej części kodu.
Dodajmy teraz następującą instrukcję do funkcji zdarzeniowej FormCreate():
FZodiac = CoZodiac::Create();
Dzięki temu obiekt COM serwera zostanie utworzony samoczynnie, bez potrzeby wywoływania funkcji CoInitialize() — całą sprawę załatwia bowiem kreator! Podobnie w momencie kończenia aplikacji klienta destruktor obiektu TCOMIZodiac zwolni automatycznie nadzorowany przez siebie interfejs.
Za pomocą pola FZodiac uzyskujemy dostęp do wszystkich metod interfejsu IZodiac — oto przykład uzyskania nazwy znaku zodiaku w reakcji na kliknięcie w przycisk „Zodiac Sign”:
Wydruk 16.14 Obsługa kliknięcia w przycisk „Zodiac Sign”
void __fastcall TMainForm::btnZodiacClick(TObject *Sender)
{
TDateTime TheDate(FCalendar->Date);
unsigned short year = 0;
unsigned short month = 0;
unsigned short day = 0;
TheDate.DecodeDate(&year, &month, &day);
if (!chkAsync->Checked)
{
BSTR bstrSign = FZodiac.GetZodiacSign(day, month);
// użyto przeciążonej funkcji-otoczki
WideString wstrSign = bstrSign;
memLog->Lines->Add(_T("Zodiac sign:"));
memLog->Lines->Add(_T(" ") + wstrSign);
memLog->Lines->Add(_T(""));
}
else
{
OLECHECK(
FZodiac.GetZodiacSignAsync(day, month));
}
}
Zastosowaliśmy tu wszędzie wczesne wiązanie (early binding), co jest normalną praktyką w przypadku klientów tworzonych w C++.
Gdy pole opcji chkAsync nie jest zaznaczone, żądana informacja uzyskiwana jest w sposób synchroniczny, a uzyskany łańcuch łamany jest pomiędzy poszczególne linie komponentu memLog. Zaznaczenie wspomnianego pola spowoduje, iż informacja przekazywana będzie w sposób asynchroniczny, za pomocą zdarzeń generowanych na serwerze i wykrywanych przez klienta — na to jednak nasz projekt nie jest jeszcze przygotowany.
Przechwytywanie zdarzeń opartych na interfejsach dyspozycyjnych
Wszystko, czego potrzebujemy po stronie klienta do obsługi zdarzeń generowanych asynchronicznie przez serwer, to obiekt COM implementujący interfejs dyspozycyjny deklarowany w koklasie serwera jako wychodzący. Począwszy od C++Buildera w wersji 4 z pierwszą poprawką (C++Builder 4, patch 1) mamy w tym celu do dyspozycji klasę-szablon TEventDispatcher ułatwiającą tworzenie ujść zdarzeń opartych na interfejsach dyspozycyjnych; jej deklaracja znajduje się w pliku utilcls.h. Klasa ta implementuje (na potrzeby serwerów) interfejs IDispatch, którego metoda InvokeEvents() kieruje wywołania metod serwera do odpowiadających im funkcji obsługi zdarzeń:
…
protected:
// To be overriden in derived class to dispatch events
virtual HRESULT InvokeEvent(DISPID id, TVariant* params = 0) = 0;
…
Ponadto metody ConnectEvents() i DisconnectEvents() klasy TEventDispatcher dokonują (odpowiednio) połączenia ujścia zdarzeń z serwerem i odłączenia go od serwera.
W związku z naszym projektem wykorzystamy szablon TEventDispatcher do stworzenia klasy TZodiacSink delegującej zdarzenia serwera do przetwarzania w zakresie funkcji zdarzeniowych VCL C++Buildera; deklaracja tej klasy, prezentowana na wydruku 16.15, znajduje się także w pliku ZodiacSink.h na załączonym CD-ROMie.
Wydruk 16.15 Implementacja ujścia zdarzeń dla interfejsu IZodiacEvents
// deklaracja typu funkcji zdarzeniowej VCL
typedef void __fastcall (__closure * TZodiacSignReadyEvent)(BSTR Sign);
//---------------------------------------------------------------------------
// Klasa implementująca interfejs IZodiacEvents
class TZodiacSink :
public TEventDispatcher<TZodiacSink, &DIID_IZodiacEvents>
{
protected:
// Pole zawierające adres funkcji zdarzeniowej
TZodiacSignReadyEvent FOnZodiacSignReady;
// dyspozytor zdarzenia
HRESULT InvokeEvent(DISPID id, TVariant* params)
{
if ((id == 1) && (FOnZodiacSignReady != NULL)) // OnZodiacSignReady
FOnZodiacSignReady(params[0]);
return S_OK;
}
// odwołanie do oryginalnej obsługi zdarzenia
CComPtr<IUnknown> m_pSender;
public:
__property TZodiacSignReadyEvent OnZodiacSignReady =
{ read = FOnZodiacSignReady, write = FOnZodiacSignReady };
public:
TZodiacSink() :
m_pSender(NULL),
FOnZodiacSignReady(NULL)
{
}
virtual ~TZodiacSink()
{
Disconnect();
}
// Connect to Server
void Connect(IUnknown* pSender)
{
if (pSender != m_pSender)
m_pSender = pSender;
if (NULL != m_pSender)
ConnectEvents(m_pSender);
}
// Disconnect from Server
void Disconnect()
{
if (NULL != m_pSender)
{
DisconnectEvents(m_pSender);
m_pSender = NULL;
}
}
};
Zwróć uwagę, iż w implementacji metody InvokeEvent() sprawdzamy numer dyspozycyjny metody interfejsu i jeżeli numer ten równy jest żądanej wartości (tutaj 1) przekazujemy sterowanie do funkcji zdarzeniowej wskazywanej przez pole FOnZodiacSignReady. Ilustruje to ogólną metodę implementacji interfejsów dyspozycyjnych — sprawdzanie numeru metody i kierowanie sterowania do odpowiedniej funkcji zdarzeniowej.
Aby wykorzystać ten mechanizm w naszej aplikacji klienckiej, dodajmy do jej formularza głównego pole wskazujące funkcję zdarzeniową
private:
…
TZodiacSink FZodiacSink;
oraz zaimplementujmy obsługę zdarzenia TZodiacSignReadyEvent:
void __fastcall TMainForm::OnZodiacSignReady(BSTR Sign)
{
WideString wstrSign = Sign;
memLog->Lines->Add(_T("Zodiac sign (ASYNCHRONOUS):"));
memLog->Lines->Add(_T(" ") + wstrSign);
memLog->Lines->Add(_T(""));
wstrSign.Detach();
}
Łańcuch zawierający szczegółową informację na temat określonego znaku zodiaku łamany jest tutaj pomiędzy poszczególne linie komponentu memLog (podobnie jak przy obsłudze synchronicznej). Zwróć uwagę, iż łańcuch ten odłączany jest od swego obiektu-otoczki, by ten nie zwolnił go przy swej destrukcji — za zwolnienie łańcucha odpowiedzialny jest bowiem sam serwer (pisaliśmy już o tym nieco wcześniej). Jedynym powodem użycia otoczki WideString jest łatwość konkatenacji podłańcuchów za pomocą przeciążonego operatora „+”.
Pozostaje nam jeszcze podłączenie ujścia zdarzeń klienta do serwera. Dokonuje tego poniższa sekwencja instrukcji znajdująca się w funkcji zdarzeniowej TMainForm::FormCreate():
FZodiacSink.OnZodiacSignReady = OnZodiacSignReady;
FZodiacSink.Connect(FZodiac);
I jeszcze jedno: informacja związana z poszczególnymi znakami zodiaku przechowywana jest w bazie danych w formacie PARADOX zlokalizowanej w podkatalogu Data projektu serwera. Do uruchomienia projektu konieczne jest zarejestrowanie tej bazy w konfiguracji BDE pod aliasem ZODIAC, co wykonać można za pomocą programu BDEADMIN.EXE (BDE Administrator). Po uruchomieniu programu należy wybrać opcję New z menu kontekstowego lewego panelu, wybrać STANDARD jako typ sterownika (Driver Name), w pole PATH prawego panelu wpisać katalog, w którym znajduje się baza (rys. 16.18) i zamknąć program.
Tu proszę wkleić rysunek z pliku AG-16-A.BMP. Rysunku tego nie ma w oryginale.
Rysunek 16.18 Rejestracja zodiakalnej bazy danych
Operowanie „klasycznymi” interfejsami
Zajmiemy się teraz wykorzystaniem interfejsu IDetailedZodiac. Jak pamiętamy, w pliku ZodiacServer_TLB.h znajdują się definicje kilku „eleganckich” interfejsów, stanowiących konkretyzację szablonu TComInterface:
typedef TComInterface<IZodiac, &IID_IZodiac> IZodiacPtr;
typedef TComInterface<IZodiacEvents, &DIID_IZodiacEvents> IZodiacEventsPtr;
typedef TComInterface<IDetailedZodiac, &IID_IDetailedZodiac>
IDetailedZodiacPtr;
typedef TComInterface<IDetailedZodiacEvents, &IID_IDetailedZodiacEvents>
IDetailedZodiacEventsPtr;
„Elegancja” szablonu TComInterface przejawia się głównie w tym, iż przypisanie wskazania na interfejs zmiennej wskazującej już na interfejs innego typu powoduje m.in. automatyczne wywołanie metody QueryInterface() wskazywanego aktualnie interfejsu, uwalniając użytkownika od czynienia tego w sposób jawny, na przykład:
// ukryte wywołanie QueryInterface():
IDetailedZodiacPtr DetailedZodiac = FZodiac;
Po wykonaniu powyższego przypisania zmienna DetailedZodiac wskazuje na egzemplarz interfejsu IZodiac, umożliwiając wywoływanie jego metod. Klikając w przycisk „Detailed Info” (btnDetailedZodiac) spowodujemy wykonanie następującej funkcji zdarzeniowej:
Wydruk 16.16 Uzyskiwanie informacji o znaku zodiaku za pomocą klasycznego interfejsu
void __fastcall TMainForm::btnDetailedZodiacClick(TObject *Sender)
{
TDateTime TheDate(FCalendar->Date);
unsigned short year = 0;
unsigned short month = 0;
unsigned short day = 0;
TheDate.DecodeDate(&year, &month, &day);
// ukryte wywołanie QueryInterface():
IDetailedZodiacPtr DetailedZodiac = FZodiac;
if (!chkAsync->Checked)
{
TDetailedZodiacSignImpl DetailedSign;
OLECHECK(
DetailedZodiac->GetDetailedZodiacSign(day, month, &DetailedSign));
memLog->Lines->Add(_T("Zodiac detailed information:"));
memLog->Lines->Add(_T(" Sign = ") + AnsiString(DetailedSign.Sign));
memLog->Lines->Add(_T(" House = ") + IntToStr(DetailedSign.House));
memLog->Lines->Add(_T(" Type = ") + AnsiString(DetailedSign.Element));
memLog->Lines->Add(_T(" Planet = ") + AnsiString(DetailedSign.Planet));
memLog->Lines->Add(_T(" Details = ") + AnsiString(DetailedSign.Details));
memLog->Lines->Add(_T(" Tip = ") + AnsiString(DetailedSign.Advice));
memLog->Lines->Add(_T(""));
memLog->Lines->Add(_T(""));
}
else
{
OLECHECK(
DetailedZodiac->GetDetailedZodiacSignAsync(day, month));
}
}
Funkcja btnDetailedZodiacClick() jest bardzo podobna do prezentowanej wcześniej funkcji btnZodiacClick; najistotniejsze różnice zaznaczyliśmy czcionką wytłuszczoną.
Tworzenie ujścia zdarzeń opartego na klasycznym interfejsie
Ponownie potrzebny jest obiekt COM po stronie klienta, tym razem w celu implementacji interfejsu IDetailedZodiacEvents. Jego konstrukcja staje się zadaniem względnie nieskomplikowanym dzięki bibliotece ATL — skonstruujemy mianowicie szablon, na bazie którego w prosty sposób definiować można będzie klasy implementujące różne rodzaje ujść zdarzeń. Kod źródłowy tego szablonu, prezentowany na wydruku 16.17, znajduje się także w pliku CustomSinks.h na załączonym CD-ROMie.
Wydruk 16.17 TCustomSink — szablon bazowy do definiowania własnych ujść zdarzeń
template <class Base, class Interface, const IID* piid = &__uuidof(Interface)>
class TCustomSink : public Base
{
private:
CComPtr<IUnknown> m_ptrSender; // generator zdarzenia
DWORD m_dwCookie; // cookie połączeniowe
public:
TCustomSink() :
m_dwCookie(0) { }
virtual ~TCustomSink() { Disconnect(); }
// iplementacja interfejsu IUnknown:
STDMETHOD_(ULONG, AddRef)() { return 1; }
STDMETHOD_(ULONG, Release)() { return 1; }
STDMETHOD(QueryInterface)(REFIID iid, void ** ppvObject)
{ return _InternalQueryInterface(iid, ppvObject); }
public:
// metody realizujące połączenie/rozłaczenie z serwerem
HRESULT __fastcall Connect(IUnknown* pSender)
{
HRESULT hr = S_FALSE;
if (pSender != m_ptrSender)
{
m_ptrSender = pSender;
if (m_ptrSender != NULL)
{
CComPtr<IUnknown> ptrUnk;
QueryInterface(IID_IUnknown, reinterpret_cast<LPVOID*>(&ptrUnk));
hr = AtlAdvise(m_ptrSender, ptrUnk, *piid, &m_dwCookie);
}
}
return hr;
}
HRESULT __fastcall Disconnect()
{
HRESULT hr = S_FALSE;
if ( (m_ptrSender != NULL) &&
(0 != m_dwCookie) )
{
hr = AtlUnadvise(m_ptrSender, *piid, m_dwCookie);
m_dwCookie = 0;
m_ptrSender = NULL;
}
return hr;
}
};
Jak widać, prezentowany szablon implementuje interfejs IUnknown, przy czym jego najbardziej skomplikowana (zazwyczaj) metoda implementowana jest za pomocą metody InternalQueryInterface() klasy CComObjectRoot, dlatego też pierwszy z argumentów szablonu (Base) musi być klasą CComObjectRoot lub jej klasą pochodną.
Innym interesującym fragmentem powyższego listingu są metody Connect() i Disconnect() odpowiedzialne za nawiązanie i zakończenie połączenia ujścia zdarzeń z serwerem. Do ich implementacji użyliśmy mianowicie funkcji AtlAdvise() i AtlUnadvise() pochodzących z biblioteki ATL.
Przepis na definiowanie własnej klasy reprezentującej ujście zdarzeń na bazie szablonu TCustomSink jest następujący:
Należy zdefiniować obiekt COM na bazie klas CComObjectRootEx (albo CComObjectRoot) oraz CComCoClass.
Należy uczynić implementowany interfejs zdarzenia kolejną klasą bazową obiektu i oczywiście stworzyć implementację metod tego interfejsu.
Zdefiniować typ własnego ujścia zdarzeń, na przykład:
typedef TCustomSink<CMySink, &IID_IMyEvents> TMyCreatableSink;
Ilustracją powyższego przepisu jest wydruk 16.18, na którym przedstawiono klasę TDetailedZodiacSinkImpl implementującą ujście zdarzeń ZodiacCustomSink dla interfejsu IDetailedZodiacEvents. Zawartość tego wydruku znajduje się w pliku ZodiacCustomSink.h na załączonym CD-ROMie.
Wydruk 16.18 Deklaracja klasy implementującej ujście zdarzeń dla interfejsu IDetailedZodiacEvents
#if !defined(ZODIACCUSTOMSINK_H__)
#define ZODIACCUSTOMSINK_H__
#include <atlvcl.h>
#include <atlbase.h>
#include <atlcom.h>
#include <ComObj.HPP>
#include <utilcls.h>
#include "CustomSinks.h"
#include "ZodiacServer_TLB.h"
typedef void __fastcall (__closure * TDetailedZodiacSignReadyEvent)
(TDetailedZodiacSign& DetailedSign);
//---------------------------------------------------------------------------
// Klasa implementującą interfejs IDetailedZodiacEvents
class ATL_NO_VTABLE TDetailedZodiacSinkImpl :
public CComObjectRootEx<CComSingleThreadModel>,
public CComCoClass<TDetailedZodiacSinkImpl, &CLSID_NULL>,
public IDetailedZodiacEvents
{
public:
TDetailedZodiacSinkImpl() :
FOnDetailedZodiacSign(NULL)
{
}
DECLARE_THREADING_MODEL(otApartment);
BEGIN_COM_MAP(TDetailedZodiacSinkImpl)
COM_INTERFACE_ENTRY(IDetailedZodiacEvents)
END_COM_MAP()
protected:
// Wskaźnik funkcji zdarzeniowej
TDetailedZodiacSignReadyEvent FOnDetailedZodiacSign;
public:
__property TDetailedZodiacSignReadyEvent OnDetailedZodiacSign =
{ read = FOnDetailedZodiacSign, write = FOnDetailedZodiacSign };
// IDetailedZodiacEvents
public:
STDMETHOD(OnDetailedZodiacSignReady(TDetailedZodiacSign* DetailedSign))
{
if (FOnDetailedZodiacSign != NULL)
FOnDetailedZodiacSign(*DetailedSign);
return S_OK;
}
};
typedef TCustomSink<TDetailedZodiacSinkImpl,
IDetailedZodiacEvents, &IID_IDetailedZodiacEvents>
TZodiacCustomSink;
#endif //ZODIACCUSTOMSINK_H__
Czytelnicy spostrzegli zapewne, iż do budowy aplikacji klienta użyliśmy tych samych klas i makr biblioteki ATL, z których korzystaliśmy już przy budowie serwera. Jest tu jednak pewna różnica — nie jest konieczne tworzenie mapy COM, bowiem aplikacja klienta nie będzie tworzyć egzemplarzy obiektu COM; z tego samego powodu nieistotny jest CLSID tegoż obiektu, dlatego też zamiast niego użyliśmy zerowego identyfikatora CLSID_NULL.
Mając już zdefiniowaną klasę TZodiacCustomSink należy teraz zrobić z niej użytek w projekcie aplikacji-klienta. Podobnie jak w przypadku klasy TZodiacSink należy najpierw dodać odpowiednie pole do formularza głównego:
private:
…
TZodiacCustomSink FZodiacCustomSink;
Następnie należy zdefiniować metodę formularza obsługującą zdarzenie OnDetailedZodiacSignReady:
void __fastcall TMainForm::OnDetailedZodiacSignReady(
TDetailedZodiacSign& DetailedSign)
{
memLog->Lines->Add(_T("Zodiac detailed information (ASYNCHRONOUS):"));
memLog->Lines->Add(_T(" Sign = ") + AnsiString(DetailedSign.Sign));
memLog->Lines->Add(_T(" House = ") + IntToStr(DetailedSign.House));
memLog->Lines->Add(_T(" Type = ") + AnsiString(DetailedSign.Element));
memLog->Lines->Add(_T(" Planet = ") + AnsiString(DetailedSign.Planet));
memLog->Lines->Add(_T(" Details = ") + AnsiString(DetailedSign.Details));
memLog->Lines->Add(_T(" Tip = ") + AnsiString(DetailedSign.Advice));
memLog->Lines->Add(_T(""));
}
po czym związać tę metodę z odpowiednim zdarzeniem klasy wskazywanej przez pole FZodiacCustomSink oraz zapewnić automatyczne połączenie z serwerem, dopisując do funkcji zdarzeniowej TMainForm::FormCreate następującą sekwencję instrukcji:
FZodiacCustomSink.OnDetailedZodiacSign = OnDetailedZodiacSignReady;
FZodiacCustomSink.Connect(FZodiac);
Na tym ostatecznie zakończyliśmy budowę aplikacji klienta — i jednocześnie prezentację projektów ilustrujących podstawowe elementy technologii COM.
Literatura zalecana
Czytelnikom zainteresowanym szczegółami technologii COM i jej związkami z C++Builderem autorzy wydania oryginalnego polecają następujące pozycje:
Na temat programowania w C++Builderze:
Kent Reisdorph i in. „C++Builder 4 Unleashed”, wyd. Sams Publishing 1999,
ISBN 0-672-31510-6
Na temat technologii COM/COM+:
Dale Rogerson „Inside COM”, wyd. Microsoft Press 1997,
ISBN 1-57231-349-8
Guy Eddon, Henry Eddon „Inside Distributed COM”, wyd. Microsoft Press 1998,
ISBN 1-57231-849-X
David S. Platt „Understanding COM+”, wyd. Microsoft Press 1999,
ISBN 0-7356-0666-8
Ash Rofail, Yasser Shohoud „Mastering COM and COM+”, wyd. Sybex Inc.,
ISBN 0-7821-2384-8
Na temat szczegółów biblioteki ATL:
Brent Rector, Chris Sells „ATL Internals”, wyd. Addison Wesley 1999,
ISBN 0-201-69589-8
O ciekawych zagadnieniach pokrewnych:
Eric Harmon „Delphi COM Programming”, wyd. Macmillan Technical Publishing 2000,
ISBN 1-57870-221-6
Zasoby internetowe:
nntp://forums.inprise.com/borland.public.cppbuilder.activex
http://community.borland.com/cpp
http://www.cetus.links.org/oo_ole.html
http://www.techvanguards.com
http://msdn.microsoft.com
Być może HELION będzie chciał zweryfikować tę listę lub coś do niej dodać.
Podsumowanie
Technologia COM nie stanowi zamkniętej całości. Tworzenie komponentów COM na potrzeby np. przetwarzania dużych baz danych, sterowania urządzeniami itp. staje się czynnością w miarę nieskomplikowaną dopiero wówczas, gdy potraktuje się narzędzie projektowe (C++Builder) jako całość, nie zaś tylko pod kątem jego współpracy z mechanizmami COM.
W niniejszym rozdziale zaprezentowaliśmy wykorzystanie elementów COM na gruncie C++Buildera, tworząc dwie współpracujące ze sobą aplikacje — serwer i klient. Śledząc proces budowy obydwu projektów przedstawiliśmy jego najważniejsze elementy — interfejsy, bibliotekę typu oraz bibliotekę szablonów ATL; kilka słów poświęciliśmy także procesowi transpozycji danych (ang. marshaling) i jego kluczowym elementom — proxy i stub'owi . Omówiliśmy także obsługę zdarzeń serwera w ramach ujścia zdarzeń klienta oraz transformację tych zdarzeń na zdarzenia VCL.
W następnym rozdziale, poświęconym rozproszonym aplikacjom COM, zajmiemy się problemem skalowania serwerów i klientów COM w kontekście architektury sieciowej.
2 Część I ♦ Podstawy obsługi systemu WhizBang (Nagłówek strony)
2 E:\HELION\CPP Builder 5\r16\r-16-00.doc