1
(Rozdział 12)
Programowanie COM
Technologia COM (ang. Component Object Model) stanowi propozycję Microsoftu w tworzeniu
aplikacji na podstawie komponentów (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 rozdział ten 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 Kyliksa zainteresowanie programistów technologią COM nie
słabnie. I nic w tym dziwnego – nawet zagorzali krytycy Windows muszą przyznać, iż wciąż stanowi
on 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 – programiści Windows nie mają wielkiego wyboru i utarta ścieżka „COM –
DCOM – COM+” zdaje się nie mieć realnej alternatywy.
Technologia COM umożliwia budowanie aplikacji na podstawie współpracujących ze sobą, acz poza
tym niezależnych, komponentów binarnych. Wśród najważniejszych jej zalet wymienić trzeba
następujące:
• definiuje pewien standard na poziomie binarnym, w oderwaniu od konkretnego narzędzia
projektowego czy języka programowania;
• ma 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 mechanizmy COM. Przeznaczeniem tego 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 klienci 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ż 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) na podstawie interfejsów dyspozycyjnych (dispinterfaces) przewija się
nieustannie przez grupę dyskusyjną Borlanda (zob.
2
nntp://forums.inprise.com/borland.public.cppbuilder.activex). C++Builder
5 nie tylko dostarcza niezbędne ku temu narzędzia, 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 rozdziale tym 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++Buildera dość skąpo.
Interfejsy wychodzące i ujścia zdarzeń
Obiekt COM może implementować wiele interfejsów, pełniących rozmaite role 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 powiadamiać mogą 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. interfej sy
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órych 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 12.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 12.1 Komunikacja klient–serwer oparta na punktach połączeniowych i na ujściu zdarzeń
3
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 Basikiem – 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 Basikiem 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ątrzpr oce sowy 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 ze wnątr zpr o cesowy m 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 zda lny 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 zgodne z COM+/MTS powinny być realizowane za
pośrednictwem bibliotek DLL – chyba że jakieś ważne względy przemawiają za plikiem .EXE. Nasz
4
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: apar tame ntowe (ang. apartment) i swobod ne (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
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ą przez 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.
5
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 tego 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. 12.2). Spowoduje to utworzenie nowego projektu
– zapisz go pod nazwą ZodiacServer.
Tu proszę wkleić rysunek z pliku orig-16-2.bmp
Rysunek 12.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 z e wn ąt r zpr oc es o wy, 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. 12.3).
Tu proszę wkleić rysunek z pliku orig-16-3.bmp
Rysunek 12.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.
6
Kliknij przycisk OK. Powinno to spowodować wyświetlenie okna edytora biblioteki typów (TLE –
Type Library Editor— rys. 12.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 12.4. 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 k ok l a s am i
(ang. coclass – skrót od Component Object Class). Zadaniem koklasy jest implementacja interfejsów.
To, co widzimy w prawym panelu okna z rysunku 12.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;
7
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.
1. Kliknij prawym przyciskiem ikonę reprezentującą ów interfejs w lewym panelu okna z rys. 13.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 przejdź 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) wskazywanego na rysunku 12.5 przez kursor myszy.
Tu proszę wkleić rysunek z pliku orig-16-5.bmp
Rysunek 12.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żą 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.
8
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 najpierw 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 12.6.
Tu proszę wkleić rysunek z pliku orig-16-6.bmp
Rysunek 12.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++Buildera w odpowiedzi na kliknięcie
przycisku 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.
9
Przegląd wygenerowanego kodu
Przed przystąpieniem do uzupełniania kodu wygenerowanego przez C++Buildera spróbujmy zrozumieć
istotę tego, co zostało wygenerowane.
W wersji 5. C++Buildera kod związany z mechanizmami COM tworzony jest na podstawie
specyficznej biblioteki szablonów, zwanej 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 wykorzystywanego 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)
10
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ą poch odną. Mamy tu do czynienia z wykorzystaniem dziedziczenia jako jednego ze sposobów
implementacji interfejsu.
Innym sposobem implementacji interfejsu COM jest mechanizm zwany a g r eg a cją (ang.
aggregation), nie będziemy się nim jednak zajmować w tej 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
autorstwa Chrisa Sellsa 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ż
11
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 12.1.
Wydruk 12.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:
12
Wydruk 12.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ą elementy 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++ na
podstawie plików skryptowych
*.rgs
. Tłumaczy to zasadniczą różnicę pomiędzy bibliotekami ATL w
obydwu ś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.
Komentarz: Proszę o
wyjaśnienie!
13
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łączonej do książki
płycie CD-ROM) – 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 12.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)");
14
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);
}
15
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 do interfejsu
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 12.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<CComMultiThreadModel>,
public CComCoClass<TZodiacImpl, &CLSID_Zodiac>,
public IConnectionPointContainerImpl<TZodiacImpl>,
public TEvents_Zodiac<TZodiacImpl>,
public IDispatchImpl<IZodiac, &IID_IZodiac, &LIBID_ZodiacServer>,
public ISupportErrorInfo
2.
Dodaj pozycję reprezentującą interfejs ISupportErrorInfo do mapy COM:
16
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 12.5.
Wydruk 12.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);
17
}
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ń na podstawie interfejsu dyspozycyjnego. Pokażemy teraz, jak
wyposażyć go w możliwości generowania zdarzeń na podstawie interfejsu wychodzącego – 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 „niedualnych” 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:
1. Kliknąć 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ąć przycisk New Field na pasku narzędziowym (wskazany przez
kursor myszy na rysunku 12.7).
18
Tu proszę wkleić rysunek z pliku orig-16-7.bmp
Rysunek 12.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ąć 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. 12.8).
Tu proszę wkleić rysunek z pliku orig-16-8.bmp
Rysunek 12.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 znaczniki 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 12.9.
Tu proszę wkleić rysunek z pliku orig-16-9.bmp
Rysunek 12.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. 12.10).
4. Wybrać z okna dialogowego interfejs IDetailedZodiac, w wyniku tego wyboru interfejs
dodany zostanie do listy implementowanych interfejsów.
19
5. Zapisać poczynione zmiany (File|Save All).
Tu proszę wkleić rysunek z pliku orig-16-10.bmp
Rysunek 12.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()
20
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łączonej płycie CD-ROM. 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ń na podstawie „klasycznych” interfejsów, 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 12.11.
Tu proszę wkleić rysunek z pliku orig-16-11.bmp
Rysunek 12.11. Stan biblioteki typu po dodaniu interfejsu IDetailedZodiacEvents
21
Każdy interfejs może mieć dwojakiego rodzaju odniesienie do implementującej go koklasy: może być
wychodzący (ang. outgoing) lub przy chodzą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 wiersz reprezentujący interfejs IDetailedZodiacEvents na
karcie Implements i z menu kontekstowego wybrać opcję Source (rys. 12.12).
4. Zapisać poczynione zmiany.
Tu proszę wkleić rysunek z pliku orig-16-12.bmp
Rysunek 12.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 12.6.
Wydruk 12.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++;
}
22
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ń zbudowanych na interfejsie – za cenę
niewielkich modyfikacji w kodzie źródłowym.
Dotarliśmy wreszcie do ostatniego elementu naszej układanki: należy 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()
…
23
Nasz obiekt COM stał się tym samym zdolny 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 12.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;
};
24
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
zarej estr ować 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 stuba
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. tr anspozy cji (ang.
marshaling). Dwoma najistotniejszymi elementami tego procesu są: pełno mo cnik, 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 detr anspozy cją (ang. unmarshaling) i
przekazanie ich do bezpośrednio wywoływanych metod serwera.
Opisany mechanizm przedstawia schematycznie rysunek 12.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 12.13. Idea transpozycji i detranspozycji danych
25
Obecnie istnieją trzy odmiany transpozycji danych. Transpozycja b i b l i o t e k t yp u (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 t r a ns p oz yc j i s t a nd a r d o we j (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 niniejszym punkcie. Trzeci rodzaj
transpozycji – t r a ns p o z yc j a s p e c yf i c zn a (Custom Marshaling) – dostępny 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
stuba, 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 stuba 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 stuba 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 przycisk „Export to IDL” (patrz rysunek 12.14).
3. Zapisz zmiany poczynione w projekcie; C++Builder zapyta Cię o nazwę, pod którą 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 12.14. Eksportowanie kodu źródłowego IDL z edytora TLE
Wydruk 12.8. przedstawia fragmenty wygenerowanego kodu:
Wydruk 12.8. Plik źródłowy IDL serwera ZodiacServer
[
uuid(962A5640-4FF0-11D5-9351-D4F9944FAD58),
version(1.0),
helpstring("ZodiacServer Library")
]
26
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)
]
27
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 pewnej obróbce, wynikającej z
faktu, iż kompilator IDL Microsoftu (MIDL) generuje kod proxy i stuba 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 12.9.
Wydruk 12.9. Zmodyfikowany plik ZodiacServer.idl przystosowany dla kompilatora MIDL
import "objidl.idl"; // Standardowe definicje COM
[
uuid(962A5640-4FF0-11D5-9351-D4F9944FAD58),
28
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 12.10 – przed
użyciem należy oczywiście dostosować specyfikowane w nim ścieżki do konkretnej instalacji.
Wydruk 12.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
29
@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
30
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 12.15.
Tu proszę wkleić rysunek z pliku orig-16-15.bmp
Rysunek 12.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 12.16.
Tu proszę wkleić rysunek z pliku orig-16-16.bmp
31
Rysunek 12.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 płycie CD-
ROM załączonej do książki.
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. 12.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ąć 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 12.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 b in ar n ym , 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).
32
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 12.11 przedstawia
fragment tego pliku dotyczący interfejsów IZodiac i IDetailedZodiac.
Wydruk 12.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]
};
…
33
…
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łączonej płycie CD-ROM 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 12.12.
Wydruk 12.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(
34
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();
35
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 12.13.
Wydruk 12.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ę 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;
36
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 przycisku „Zodiac Sign”:
Wydruk 12.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);
37
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 wiersze 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ń zbudowanych na
podstawie interfejsów 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ń zbudowanych 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.
38
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 12.15, znajduje się także w pliku
ZodiacSink.h na załączonej płycie CD-ROM.
Wydruk 12.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);
39
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 klienta, 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 wiersze 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. 12.18) i zamknąć program.
40
Tu proszę wkleić rysunek z pliku AG-16-A.BMP. Rysunku tego nie ma w oryginale.
Rysunek 12.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 przycisk „Detailed Info”
(btnDetailedZodiac), spowodujemy wykonanie następującej funkcji zdarzeniowej:
Wydruk 12.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(
41
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ń zbudowanego 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 12.17, znajduje się także w pliku CustomSinks.h na załączonej
płycie CD-ROM.
Wydruk 12.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:
42
// metody realizujące połączenie i 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 wydruku 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<CMySink, &IID_IMyEvents> TMyCreatableSink;
43
Ilustracją powyższego przepisu jest wydruk 12.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łączonej płycie CD-ROM.
Wydruk 12.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;
}
};
44
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 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.
45
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ć.
Komentarz: Czy to jest dobrze?
46
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 ich współpracy z mechanizmami COM.
W tym 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 stubowi. 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.