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 zró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:
1. 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
2. Wpisz Zodiac jako nazwę koklasy, wybierz Free jako model wątkowy i wpisz w ostatnie z
pól cokolwiek, co uważasz za stosowne.
3. 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
Przejdzmy teraz do definiowania metod pierwszego z interfejsów IZodiac.
1. 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.
2. Zmień nazwę nowo utworzonej metody z Method1 na GetZodiacSign.
3. Podświetl w lewym panelu pozycję reprezentującą metodę GetZodiacSign i przejdz do
karty Parameters prawego panelu.
4. 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 przejdz 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
,
public CComCoClass,
public IConnectionPointContainerImpl,
public TEvents_Zodiac,
public IDispatchImpl
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.
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 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 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
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 agr egacją (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 HRESULT
TEvents_Zodiac::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 IZodiacEventsDispT : public TAutoDriver
{
public:
IZodiacEventsDispT(){}
void Attach(LPUNKNOWN punk)
{ m_Dispatch = static_cast(punk); }
HRESULT __fastcall OnZodiacSignready(BSTR Sign/*[in]*/);
};
typedef IZodiacEventsDispT IZodiacEventsDisp;
Jak widać, IZodiacEventsDisp jest konkretyzacją szablonu IZodiacEventsDispT,
wywodzącego się z TAutoDriver. 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
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(Month),
static_cast(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:
1. Zadeklaruj interfejs ISupportErrorInfo jako jedną z klas bazowych koklasy
TZodiacImpl:
class ATL_NO_VTABLE TZodiacImpl :
public CComObjectRootEx,
public CComCoClass,
public IConnectionPointContainerImpl,
public TEvents_Zodiac,
public IDispatchImpl,
public ISupportErrorInfo
2. 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()
3. Zadeklaruj w koklasie metody interfejsu ISupportErrorInfo:
class ATL_NO_VTABLE TZodiacImpl :
&
&
public:
STDMETHOD(InterfaceSupportsErrorInfo)(REFIID riid);
&
4. 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
zró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:
1. Kliknąć w przycisk New Record (szósty od lewej) na pasku narzędziowym edytora TLE;
2. Zmienić nazwę utworzonego rekordu na TDetailedZodiacSign;
3. 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:
1. Podświetlić w lewym panelu najwyższą pozycję (ZodiacServer) i kliknąć w przycisk New
Interface (pierwszy z lewej) na pasku narzędziowym edytora TLE;
2. Zmienić nazwę nowo utworzonego interfejsu na IDetailedZodiac;
3. 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
4. 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:
1. W lewym panelu podświetlić koklasę Zodiac.
2. Przejść na kartę Implements prawego panelu; karta ta zawiera listę interfejsów
implementowanych przez koklasę.
3. Z menu kontekstowego (uruchamianego prawym kliknięciem) wybrać opcję Insert
Interface, w wyniku czego pojawi się okno dialogowe Insert Interface (rys.
16.10).
4. Wybrać z okna dialogowego interfejs IDetailedZodiac; w wyniku tego wyboru interfejs
dodany zostanie do listy implementowanych interfejsów,
5. 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,
public CComCoClass,
public IConnectionPointContainerImpl,
public TEvents_Zodiac,
public IDispatchImpl,
public TEvents_DetailedZodiac,
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,
public CComCoClass,
public IConnectionPointContainerImpl,
public TEvents_Zodiac,
public IDispatchImpl,
public TEvents_DetailedZodiac,
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? Odpowiedz 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ózny 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:
1. Podświetlić koklasę Zodiac w lewym panelu;
2. Przejść na kartę Implements i za pomocą menu kontekstowego dodać interfejs
IDetailedZodiacEvents do listy interfejsów implementowanych przez koklasę;
3. Kliknąć prawym przyciskiem w linię reprezentującą interfejs IDetailedZodiacEvents
na karcie Implements i z menu kontekstowego wybrać opcję Source (rys. 16.12);
4. 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 &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 &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 ptrEvents = *pp;
to nic innego, jak uzyskanie wskaznika do implementowanego interfejsu
IDetailedZodiacEvents na podstawie znanego wskaznika 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 zró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,
public CComCoClass,
public IConnectionPointContainerImpl,
public TEvents_Zodiac,
public IDispatchImpl,
public ISupportErrorInfo,
public IDetailedZodiac,
public TEvents_DetailedZodiac
&
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 t ypu (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:
1. Załaduj do IDE projekt serwera i wyświetl okno TLE za pomocą opcji View|Type
Library menu głównego.
2. Kliknij w przycisk Export to IDL (patrz rysunek 16.14).
3. 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.
4. Zamknij projekt serwera.
Tu proszę wkleić rysunek z pliku orig-16-14.bmp
Rysunek 16.14 Eksportowanie kodu zródłowego IDL z edytora TLE
Wydruk 16.8 przedstawia fragmenty wygenerowanego kodu:
Wydruk 16.8 Plik zró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:
5. Uruchomić edycję pliku ZodiacServer.idl przy użyciu dowolnego edytora tekstowego.
6. Przesunąć poza sekcję library definicje wszystkich interfejsów nie oznakowanych jako
zgodne z automatyzacją OLE i usunąć całą resztę kodu.
7. 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
znalezć 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
zró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:
1. Usuń z projektu plik Unit1.c produkowany automatycznie przez kreator.
2. Dodaj do projektu plik *.c wyprodukowany przez MIDL oraz stworzony przed chwilą plik
*.def.
3. 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
4. Zamknij okno opcji projektu i zachowaj projekt pod nazwą ZodiacServerPS.bpr.
5. Skompiluj projekt rezultatem bezbłędnej kompilacji powinien być plik
ZodiacServerPS.dll.
6. 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 zró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:
1. Wybrać opcję Project|Import Type Library z menu głównego IDE; spowoduje to
otwarcie okna dialogowego Import Type Library (rys. 16.17).
2. 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.
3. 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 zró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 zró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 zró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 TCOMIZodiacT : public TComInterface, public
TComInterfaceBase
{
public:
TCOMIZodiacT() {}
TCOMIZodiacT(IZodiac *intf, bool addRef = false) :
TComInterface(intf, addRef) {}
TCOMIZodiacT(const TCOMIZodiacT& src) : TComInterface(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 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 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
TComInterface& operator=(const TComInterface 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 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
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
{
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 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();
}
Aań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 IZodiacPtr;
typedef TComInterface IZodiacEventsPtr;
typedef TComInterface
IDetailedZodiacPtr;
typedef TComInterface
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 zró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 TCustomSink : public Base
{
private:
CComPtr 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 ptrUnk;
QueryInterface(IID_IUnknown, reinterpret_cast(&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:
1. Należy zdefiniować obiekt COM na bazie klas CComObjectRootEx (albo
CComObjectRoot) oraz CComCoClass.
2. Należy uczynić implementowany interfejs zdarzenia kolejną klasą bazową obiektu i
oczywiście stworzyć implementację metod tego interfejsu.
3. Zdefiniować typ własnego ujścia zdarzeń, na przykład:
typedef TCustomSink 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
#include
#include
#include
#include
#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,
public CComCoClass,
public IDetailedZodiacEvents
{
public:
TDetailedZodiacSinkImpl() :
FOnDetailedZodiacSign(NULL)
{
}
DECLARE_THREADING_MODEL(otApartment);
BEGIN_COM_MAP(TDetailedZodiacSinkImpl)
COM_INTERFACE_ENTRY(IDetailedZodiacEvents)
END_COM_MAP()
protected:
// Wskaznik 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 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.
Wyszukiwarka
Podobne podstrony:
16 00 23 6zp6f5atwimzdczjedugclrxvgbku6fz6cvvkyq
16 00 Prace tymczasowe i rozbiorkowe 1
TI 00 10 16 T B pl(1)
TI 00 08 16 B pl(2)
WSM 00 16 pl(2)
TI 00 08 16 T pl
ustawa o umowach miedzynarodowych 14 00
00 Notatki organizacyjne
Scenariusz 16 Rowerem do szkoły
r 1 nr 16 1386694464
16 narrator
więcej podobnych podstron