Projektowanie i wytwarzanie aplikacji internetowych

część 4

Sebastian Wyrwał

Artykuł ten pokazuje, jak wykorzystać JDBC do łączenia
z bazą danych z poziomu języka Java. Stanowi to pewną rozbudowę
szkieletu aplikacji, który został stworzony w oparciu o architekturę CORBA. W
dalszej części przedstawiony został model COM będący alternatywą
dla prezentowanych dotąd rozwiązań. Jest on również podstawą dla
technologii DCOM i ActiveX. Przy użyciu tego modelu
zaimplementowany został szkielet komponentu realizującego rejestrację umów o
świadczenia usług telekomunikacyjnych. Przykład prezentuje komunikację z
komponentem COM i wewnętrzną budowę samego komponentu.

W poprzednim artykule przedstawiono zastosowanie architektury
CORBA i języka JAVA do wytworzenia szkieletu prostej aplikacji. Ograniczono się
do zaimplementowania komunikacji z pominięciem właściwej funkcjonalności.
Przypomnijmy, że funkcjonalność po stronie serwera realizuje klasa rejestracjaImp:1 

public class rejestracjaImp extends _i_rejestracjaImplBase{

public boolean autoryzacja(String kod, String haslo){

return true;
}

public boolean czy_klient_istnieje(String PESEL){

return true;
}

public boolean dodaj_nowego(String PESEL, String nazwisko, String imie,
String adres){

return true;
}

public boolean rejestruj(String PESEL, String numer_tel, String SIM,
org.omg.CORBA.StringHolder informacje){

return true;
}

public boolean czy_aktywna(String numer_umowy){

return true;
}

public boolean zakoncz(){

return true;
}

}

Dostęp do tej funkcji serwera odbywa się po stronie klienta
przy pomocy następującego interfejsu2 :

public interface i_rejestracjaOperations {

boolean autoryzacja (String kod, String haslo);
boolean czy_klient_istnieje (String PESEL);
boolean dodaj_nowego (String PESEL, String nazwisko, String imie,
String aderes);
boolean rejestruj (String PESEL, String numer_tel, String SIM,
org.omg.CORBA.StringHolder informacje);
boolean czy_aktywna (String numer_umowy);
boolean zakoncz ();

} // interface i_rejestracjaOperations

a mówiąc ściśle przy pomocy interfejsu i_rejestracja,
który dziedziczy z powyższego interfejsu.3 

Jeśli rejkl jest referencją do obiektu
zdalnego (typu i_rejestracja) po stronie klienta, to wywołanie: rejkl.autoryzacja(„Seb,”abc111”)
po stronie klienta spowoduje wywołanie metody autoryzacja(„Seb”,”abc111″)
obiektu klasy rejestracjaImp (po stronie serwera) tak, jakby referencji
do interfejsu i_rejestracja przypisano lokalną referencję do
klasy rejestracjaImp tzn.:

i_rejestracja rejkl=new rejestracjaImp;

rejkl.autoryzacja(„Seb”,”abc111″)

Jak już powiedziano, przy dalszej implementacji można
zapomnieć o użyciu architektury CORBA. Implementacja po stronie serwera będzie
się odbywała poprzez rozbudowę funkcji klasy rejestracjaImp o
mechanizmy pozwalające na współpracę z bazą danych. Język JAVA dostarcza
wygodnego mechanizmu służącego do komunikacji z bazą danych – JDBC.

JDBC – czyli o łączeniu z bazą danych

Większość aplikacji, zwłaszcza związanych z działalnością
gospodarczą, gromadzi i udostępnia jakieś dane. Najczęściej wykorzystuje się
do tego celu gotowe systemy baz danych np. Oracle, MySQL, Access, Informix. Język
Java posiada wbudowany, wygodny mechanizm, pozwalający na współpracę z
bazami danych – JDBC (ang. Java DataBase Conectivity). JDBC jest niezależny
od platformy i jest zgodny ze standardem SQL-92. (Można również wykonywać
operacje specyficzne dla danego motoru bazy danych).

Zarządca sterowników (ang.driver manager) zajmuje się obsługą
sterowników do bazy danych, które są ładowane dynamicznie podczas wykonania
programu. JDBC może łączyć się z bazą danych bezpośrednio (cały kod
sterownika napisany w języku Java) lub pośrednio za pośrednictwem np. ODBC,
co jednak spowalnia dostęp. Aby połączyć się z bazą danych należy:

  • Załadować sterownik,

  • Utworzyć połączenie z bazą danych,

  • W ramach tego połączenia można tworzyć zapytania i
    przetwarzać zbiory wyników.

Poniższy program łączy się z bazą danych identyfikowaną
przez ODBC jako br i wysyła do niej zapytanie:

Select numer_tel From umowa a następnie wypisuje numery
telefonów. (Tabela umowa ma następujące kolumny: nr, pesel, sim,
numer_tel, aktywowana, numer_umowy
).

import java.awt.*;
import java.sql.*;
public class test1{

public static void main(String args[])throws ClassNotFoundException{

try{

Class.forName("sun.jdbc.odbc.JdbcOdbcDriver");
//załadowanie sterownika
Connection c = DriverManager.getConnection("jdbc:odbc:br");
//Połączenie z bazą identyfikowaną jako br w ODBC
Statement stmt = c.createStatement();
//Stworzenie zapytania
String sqlString="Select numer_tel From umowa";
//treść SQL’a
ResultSet res = stmt.executeQuery(sqlString);
//Wykonanie zapytania
while (res.next())// pobranie następnego rekordu
{
String str=res.getString("numer_tel");//pobranie kolumny
numer_tel
System.out.println(str);
}
res.close();//zwolnienie zbioru rezultatów
stmt.close();//zwolnienie zapytania
c.close(); //zamknięcie połączenia
}
catch (ClassNotFoundException e){
System.out.println("Błąd");
e.printStackTrace(System.out);
}
catch (SQLException e){
System.out.println("Błąd");
e.printStackTrace(System.out);
}

}

}

Dla encji Klient, reprezentującej osobę chcącą
zawrzeć umowę, można zdefiniować klasę klient.

import java.awt.*;
import java.sql.*;
class klient{

private String _PESEL,_nazwisko,_imie,_adres;
public klient(String PESEL, String nazwisko, String imie, String aders){
_PESEL=PESEL;
_nazwisko=nazwisko;
_imie=imie;
_adres=aders;
}
public void insert(Connection c) throws SQLException{
String sqlString="insert into klient
values(’"+_PESEL+"’,’"+_nazwisko+"’,’"+_imie+"’,’"+_adres+"’)";
System.out.println(sqlString);
Statement stmt = c.createStatement();
stmt.executeUpdate(sqlString);
stmt.close();
}

}

Klasa ta posiada metodę insert, której wywołanie
realizuje wstawienie nowego klienta do tablicy klient. Implementacja
metody dodaj_nowego klasy rejestracjaImp wyglądałaby następująco:

public boolean dodaj_nowego(String PESEL, String nazwisko, String imie,
String adres){

klient k =

new klient(PESEL, nazwisko, imie, adres);
k.insert(c);// zakładamy, że klasa dysponuje konekcją,
//obsługę wyjątków tu pomijamy

return true;
}

Model COM

Dotychczas przedstawiono pewne koncepcje teoretyczne związane
z samymi aplikacjami internetowymi, koncepcje dotyczące podziału aplikacji,
ich projektowania oraz komunikacji między procesami. Zaprezentowano prosty
przykład komunikacji przy pomocy CGI oraz szkielet prostej aplikacji
internetowej, która wykorzystuje architekturę CORBA. Zaprezentowano również
ogólne właściwości języków obiektowych oraz przykład języka JAVA. Języki
obiektowe, jak już powiedziano, dostarczają mechanizmów zapewniających możliwość
ponownego użycia kodu. Możliwość ta występuje na poziomie kodu źródłowego
zapisanego w języku programowania. CORBA jest architekturą z której
korzystamy na poziomie języka programowania, w tym sensie, że byty programowe
(stopka, interfejs) związane z dostępem do zdalnych obiektów, występują
zazwyczaj w kodzie źródłowym. Oznacza to, że z góry założono z jakiego
obiektu zdalnego będzie się korzystać. Stopka (namiastka) wygenerowana jest
dla obiektu o określonym interfejsie. Obok przedstawionych, istnieją również
skrajnie inne koncepcje, opierające się o możliwość ponownego użycia i współdziałanie
kodu na poziomie binarnym. Poniżej przedstawiono technologię COM (ang.
Componet Object Model) opisującą współdziałanie obiektów na poziomie języka
maszynowego. Prezentowany opis wymusza pewne odejście od dotychczasowych rozważań,
czynionych na poziomie języka programowania.

Programowanie zorientowane obiektowo, pomimo swej siły
posiada pewne wady. Nie zdefiniowano ram, w obrębie których obiekty
dostarczane przez różnych dostawców mogłyby ze sobą współpracować.4 
Zwłaszcza jeśli chodzi o współpracę przez sieć, lub współpracę maszyn z
różnymi architekturami. Rozwiązaniem tej niedogodności są komponenty (ang.
software componets)
, dostarczane w postaci binarnej (czyli skompilowanej).
Komponenty takie mogą być łatwo „podłączane” do aplikacji i są
natychmiast gotowe do użycia. Można je porównać do układów scalonych, które
zrewolucjonizowały przemysł elektroniczny. Gdyby iść z tą analogią dalej,
to tradycyjne programowanie obiektowe należałoby porównać do prac na
poziomie schematu urządzenia elektronicznego. Działanie tego typu komponentów
opisuje COM (ang. component object model) stworzony przez firmy Microsoft
i Digital Equipment Corporation. Model COM ma przenosić silne mechanizmy
występujące w językach obiektowych na poziom binarny. Oczywiście komponenty COM
są tworzone przy użyciu konkretnego języka programowania, chociaż model
ten nie narzuca ani języka implementacji ani wewnętrznej struktury czy
realizacji obiektów.

Idea modelu COM

Model COM bazuje na rozdzieleniu interfejsu od implementacji.
Można jednak postawić pytanie, jak wygląda ono w praktyce. Rozważmy zatem
prosty program napisany w języku C:

#include <stdio.h>
int suma(int a, int b){

printf("n a+b=%d",a+b);
return a+b;

}
int roznica(int a, int b){

printf("n a-b=%d", a-b);
return a-b;

}
void main(void){
int (*pf) (int a, int b);

pf = suma;
pf(1,3);

 

pf = roznica;
pf(1,3);

}

W programie tym zdefiniowano dwie funkcje suma oraz roznica.
Funkcje te mają po dwa argumenty wywołania typu integer, każda zwraca
wartość typu integer. Funkcje te mają więc takie same sygnatury.
Funkcja suma wyświetla sumę dwóch liczb, funkcja roznica wyświetla
różnicę dwóch liczb. W obrębie funkcji main zdefiniowano wskaźnik
do funkcji
. Jego definicja jest następująca:

int (*pf) (int a, int b);

pf jest wskaźnikiem do funkcji zwracającej wartość
typu integer i mającą dwa argumenty wywołania typu integer. Łatwo
można zauważyć, że sygnatury funkcji suma oraz roznica
zgodne z sygnaturą wskaźnika do funkcji pf. Wskaźnikowi pf można
zatem przypisać funkcję suma lub roznica jak też i każdą inną
funkcję o sygnaturze int x int -> int.5 W przykładowym programie,
wskaźnikowi pf przypisano najpierw funkcję suma i wywołano
funkcję za pośrednictwem wskaźnika pf, co dało efekt taki, jak bezpośrednie
wywołanie funkcji suma, a następnie wskaźnikowi pf przypisano
funkcję roznica i wywołano funkcję za pośrednictwem wskaźnika, co z
kolei dało taki efekt, jak wywołanie funkcji roznica. Jak widać, wskaźniki
do funkcji pozwalają na sparametryzowanie wywołań funkcji o takich samych
sygnaturach (lista argumentów wywołania i typ wartości zwracanej). Kontynuując
powyższe rozważania można zdefiniować strukturę, w skład której wchodzą
wskaźniki do funkcji tzn.:

typedef struct {

int (*pf1) (int a);
int (*pf2) (int a, int b);
void (*pf3) (char* s);

}tablica_funkcji;

Struktura tablica_funkcji ma trzy wskaźniki do
funkcji pf1, pf2, pf3. Wskaźnik pf1 jest wskaźnikiem do
funkcji, której argument oraz wartość zwracana są typu integer. Wskaźnik
pf2 jest wskaźnikiem do funkcji, która ma dwa argumenty typu integer
i zwraca wartość typu integer. Wskaźnik pf3 jest wskaźnikiem
do funkcji, która nie zwraca żadnej wartości a jej jedynym argumentem jest łańcuch
znaków.6 

Dodatkowo zdefiniowano następujące funkcje:

int f1(int a){

printf(" f1 %d ",a+1);
return a+1;

}

int g1(int a){

printf(" g1 %d ",a+2);
return a+2;

}

int f2(int a,int b){

printf(" f2 %d ", a+b);
return a+b;

}

int g2(int a, int b){

printf(" g2 %d ", a-b);
return a-b;

}

void f3(char* s){

printf(" f3 %s",s);

}

void g3(char* s){

printf(" g3 %s",s);

}

Funkcje f1, g1 oraz wskaźnik pf1 ze struktury tablica_funkcji
mają taką samą sygnaturę. Podobnie f2, g2, pf2 oraz f3, g3, pf3.
Wskaźnikowi pf1 można zatem przypisać funkcję f1 lub g1
ale nie g2. W poniższym programie przypisano najpierw polom pf1, pf2,
pf3
zmiennej tb, funkcje f1,f2,f37 i wywołano funkcje używając
wskaźników funkcji, co powoduje wypisanie na ekranie f1 4 f2 3 f3 ala;
następnie polom pf1, pf2, pf3 przypisano funkcje g1, g2, g3 i tak
jak poprzednio, wywołano funkcje z użyciem wskaźników – program
wypisze g1 5 g2 -1 g3 ala.

void main(void){

}

Należy zauważyć, iż pomimo, że wywołania odbywają się
identycznie w obu sekcjach (oznaczonych prostokątami z linii ciągłej i
przerywanej), wywoływane są inne funkcje – f1, f2, f3 w pierwszej
sekcji oraz g1, g2, g3 w sekcji drugiej. Tak więc strukturę tablica_funkcji
można traktować jak mechanizm pozwalający na parametryzację wywołań.
Struktura tablica_funkcji jest interfejsem, za pośrednictwem którego
wywołuje się funkcje. W prostym programie wskaźniki do funkcji i same funkcje
istnieją w ramach jednego procesu w tej samej przestrzeni adresowej, co
pokazuje rysunek 1:


Rys. 1. Wskaźniki do funkcji i funkcje

Powyższy przykład pokazuje idee pośrednich wywołań
funkcji, na której opiera się model COM8 . W modelu tym:

  • wywołanie i realizacja funkcji nie muszą odbywać się
    w obrębie jednego procesu,
  • dostawca funkcji może mieć kilka interfejsów,
  • w obrębie każdego interfejsu muszą istnieć pewne
    funkcje pomocnicze, związane z cyklem życia obiektu „dostawcy”
    oraz umożliwiające odpytanie obiektu „dostawcy” o inne
    interfejsy i uzyskanie do nich dostępu9 ,
  • funkcje pomocnicze tworzą standardowy interfejs IUnknown,
    a każdy obiekt COM musi implementować ten interfejs.

Z powyższych założeń wynika, że komponentami COM
manipuluje się przy pomocy interfejsów. Mając dostęp do jednego z interfejsów
obiektu, można uzyskać dostęp do innych. W szczególności, mając dostęp do
interfejsu IUnknown można uzyskać dostęp do innych interfejsów danego
komponentu.

Właściwości modelu COM i zasady użycia komponentów

W modelu COM:

  • Współpraca pomiędzy obiektami odbywa się na poziomie
    binarnym.
  • Interfejs jest oddzielony od implementacji.
  • Interfejsy składają się ze spokrewnionych ze sobą
    abstrakcyjnych funkcji.
  • Interfejs jest identyfikowany przez 128 bitowy, globalnie
    unikatowy, identyfikator zwany ID.
  • Komponent implementuje jeden lub więcej interfejsów10 .
  • Komponenty są identyfikowane przez 128 bitowy, globalnie
    unikatowy, identyfikator zwany CLSID11 .
  • Model COM nie narzuca języka implementacji.
  • Możliwe jest ponowne użycie kodu na poziomie binarnym w
    czasie wykonania.
  • Zarządzanie wersjami jest proste.

Aby korzystać z komponentu COM, nie trzeba wiedzieć
nic o jego budowie wewnętrznej ani dysponować kodem źródłowym. Aby stworzyć
komponent, wystarczy znać jego identyfikator CLSID.12  Po
utworzeniu obiektu otrzymuje się wskaźnik do interfejsu IUnknown tego
obiektu. Współpraca na poziomie binarnym oznacza także to, że zmiana obiektu
z którego ma korzystać aplikacja, nie pociąga za sobą konieczności jej powtórnej
kompilacji. Wystarczy, że nowy obiekt implementuje te same interfejsy co stary.
Dzieje się tak dlatego, że identyfikator CLSID tworzonego komponentu może
być ustawiony przy konfiguracji programu. W modelu COM nie ma
dziedziczenia, ale komponent COM może delegować wywołania do innych
komponentów; zapewnia to możliwość ponownego użycia kodu na poziomie
binarnym. Problem zarządzania wersjami jest rozwiązany bardzo prosto –
nowa wersja interfejsu, to po prostu nowy interfejs o nowym IID a więc
konflikty wersji nie występują.13 Implementacja komponentu znajduje się
w obrębie serwera komponentu.


Rys. 2. Zewnątrz-procesowy serwer komponentu

Serwer komponentu może działać w obrębie tego samego
procesu co klient, lub w ramach innego procesu, na tej samej co klient maszynie,
lub na maszynie zdalnej. Serwerem działającym w obrębie tego samego procesu (ang.
In process server
), co klient, jest dynamicznie łączona biblioteka (dll),
jako że w systemie WIN32 biblioteki takie są ładowane do przestrzeni
adresowej procesu, który z nich korzysta. Komunikacja pomiędzy klientem a
komponentem COM realizowana jest przez wywołania funkcji, tak jak
opisano w punkcie „idea modelu COM„. Serwer działający na
tej samej maszynie co klient, lecz w innym procesie (ang. out process serwer)
jest programem wykonywalnym (exe); rzeczywista komunikacja odbywa się z
użyciem RPC, przy czym ani klient ani komponent nie są tego świadome. W
komunikacji uczestniczy proxy komponentu, znajdujące się w obrębie procesu
klienta, co jest realizowane przy pomocy dynamicznie łączonej biblioteki.

Komunikacja z komponentem znajdującym się w systemie
zdalnym14  (ang. remote serwer) przebiega również przy użyciu
RPC i proxy komponentu; mechanizm ten nosi nazwę DCOM. Należy jednak
podkreślić, że program klienta, który chce stworzyć komponent COM,
nie komunikuje się bezpośrednio z serwerem komponentu. Klient tworzy
komponenty za pośrednictwem biblioteki komponentów (ang. Component Object
Library
). Biblioteka ta jest odpowiedzialna za zidentyfikowanie (na
podstawie przekazanego jej identyfikatora klasy) serwera komponentu i
uruchomienie go. Budowa serwera zależy do jego typu. Oprócz implementacji
samego obiektu (niezależnej od typu serwera) w jego skład wchodzą pewne składniki
pomocnicze, od których abstrahujemy na tym poziomie rozważań. Ważnym składnikiem
serwera jest implementacja fabryki komponentów danego typu, która też jest
komponentem COM. Z fabryką tą współpracuje biblioteka COM, a
informacje o serwerach komponentów znajdują się w rejestrze systemu Windows.
Przykładowy zewnątrz-procesowy serwer komponentu pokazano na rysunku 2. Linią
ciągłą oznaczono wywołania służące uzyskaniu dostępu (lub utworzeniu)
komponentu. Linią przerywaną oznaczono komunikację z komponentem. Koła
oznaczają interfejsy.

Poniżej pokazano, jak jest tworzony i wykorzystywany po
stronie klienta komponent COM w praktyce. Jest to komponent klasy MyClass, który
implementuje interfejs i_rejestracja. Dla tego komponentu wywołano
metody zakoncz i rejestruj:

void __cdecl main(int argc, char *argv[]){

HRESULT hr;
i_rejestracja* pMyInterface = 0;
int w;
char s[100];
hr = CoInitialize(NULL);
if(hr>0){
hr = CoCreateInstance(CLSID_MyClass, 0, CLSCTX_LOCAL_SERVER,
IID_i_rejestracja, (void **) &pMyInterface);
if(hr>0){
hr = pMyInterface->zakoncz(&w);
if(hr>0)
printf(" w = %d",w);
else
printf("IID_i_rejestracja::zakoncz nie powiodło się .n");
hr = pMyInterface->rejestruj("7511abc","913",s,&w);
if(hr>0)
printf(" w=%d, s=%s",w,s);
else
printf("IID_i_rejestracja::rejestruj nie powiodło się.n");
pMyInterface->Release();

}else
printf("Wywołanie CoCreateInstance nie powiodło się .n");
CoUninitialize();

}else
printf("Wywołanie CoInitialize nie powiodło się .n");

}

Funkcja CoInitialize służy do zainicjowania
biblioteki COM. Komponent COM tworzy się przy pomocy funkcji CoCreateInstance.
Pierwszy argument wywołania tej funkcji określa CLSID klasy, której
komponent ma być stworzony – (CLSID_MyClass ma wartość równą
liczbie szesnastkowej 9C4DAFA1-CF83-11d5-BCE4-00C0DFAA8D1D), drugi
argument (mający w przykładzie wartość NULL) określa czy obiekt jest
częścią zbiorowości15 , trzeci argument wywołania określa kontekst
wykonania obiektu CLSCTX_SERWER16  i oznacza, że serwer znajduje się
albo w procesie wywołującym, albo w innym procesie lokalnym, albo w procesie
zdalnym. Czwarty parametr wywołania (IID_Rejestracja) określa
identyfikator interfejsu, który chce się otrzymać. Ostatni parametr określa
zmienną, której zostanie przypisany wskaźnik do tego interfejsu; w powyższym
przykładzie będzie to wskaźnik do interfejsu i_rejestracja. Po
utworzeniu komponentu wywołano na rzecz jego interfejsu i_rejestracja
dwie funkcje: zakoncz i rejestruj. Po wywołaniu tych funkcji
„zwolniono” interfejs używając funkcji Release, a następnie
„zamknięto” bibliotekę COM przy pomocy funkcji CoUnitialize().
Po każdym wywołaniu funkcji związanej z samym komponentem COM lub
biblioteką COM, sprawdzana jest wartość zwrócona przez wywoływaną
funkcję; wartość większa od zera oznacza, że wywołanie zakończyło się
sukcesem.

Należy zauważyć, że nie jest konieczne generowanie żadnych
stopek po stronie aplikacji korzystającej z obiektu COM.

Interfejsy w modelu COM

W modelu COM interakcja z komponentami odbywa się
przy pomocy funkcji składających się na interfejsy. Wskaźnik do interfejsu
można rozumieć tak, jak tablicę wskaźników do funkcji wirtualnych w języku
C++. Interfejs jest zresztą, na poziomie binarnym, zgodny z klasą
abstrakcyjną w języku C++. Chodzi tu oczywiście o wskaźnik na poziomie kodu,
a nie GUID identyfikatora. Ponadto każdy interfejs musi dziedziczyć z
interfejsu IUnknown. Interfejs ten ma następujące funkcje:

  • QueryInterface() – służącą do
    „odpytywania” obiektu o inne interfejsy,
  • AddRef() – służąca do zwiększania o
    jeden licznika odwołań,
  • Release() – służąca do zmniejszania o
    jeden licznika odwołań.

IUnknown

QueryInterface()

AddRef()

Release()


Tabela 1.
Interfejs IUnknown

Wartość licznika odwołań odpowiada ilości odwołań
innych obiektów do danego obiektu; gdy ma on wartość równą zero oznacza to,
że inne obiekty nie korzystają z danego obiektu i obiekt może zostać usunięty.
Definicja interfejsu realizującego rejestrację umów o usługi
telekomunikacyjne w języku COM IDL generatora MIDL firmy
Microsoft wygląda następująco:

#ifndef DO_NO_IMPORTS

import "unknwn.idl";

import "wtypes.idl";

#endif

 

[ object, uuid(96C3D182-CF70-11d5-BCE4-00C0DFAA8D1D) ]

interface i_rejestracja : IUnknown{

HRESULT autoryzacja([in, string] LPSTR kod, [in, string] LPSTR haslo,
[out] int* wynik);

HRESULT czy_klient_istnieje([in, string] LPSTR PESEL, [out] int*
wynik);

HRESULT dodaj_nowego([in, string] LPSTR PESEL, [in, string] LPSTR
nazwisko, [in, string] LPSTR imie, [in, string] LPSTR adres, [out] int* wynik);

HRESULT rejestruj([in, string] LPSTR PESEL, [in, string] LPSTR
numer_tel, [out, size_is(128)] LPSTR informacje, [out] int* wynik);

HRESULT czy_aktywna([in, string] LPSTR numer_umowy, [out] int* wynik);

HRESULT zakoncz([out] int* wynik);

};

Zdefiniowany interfejs nazywa się i_rejestracja i
jest funkcjonalnie zgodny z interfejsem i_rejestracjaOperations, który
został przypomniany na początku tego artykułu. Składnia definiowania
interfejsu (dla obiektów COM) różni się trochę od tej, pokazanej przy
okazji omawiania architektury CORBA. Istotne jest jednak pojawienie się
identyfikatora IID, który identyfikuje ten interfejs. Identyfikator ten
jest następujący:

96C3D182-CF70-11d5-BCE4-00C0DFAA8D1D. Został on wygenerowany
przy pomocy generatora guidgen firmy Microsoft. Warto tutaj wspomnieć, iż
pomimo tego, że nie istnieje instytucja zajmująca się odgórnym nadawaniem
identyfikatorów, narzędzie guidgen zapewnia ich unikalność. Interfejs
dziedziczy (co podobnie jak w C++ zapisujemy przy użyciu dwukropka) z
interfejsu IUnknown; odzwierciedla to na poziomie jego definicji, że ma
on metody QueryInterface, Addref oraz Release. Przy pomocy słów
kluczowych import, do pliku z definicją interfejsu włączono pliki unknwn.idl
oraz wtypes.idl; odbywa się to analogicznie do włączania plików nagłówkowych
przy pomocy dyrektywy #include w programach napisanych w języku C
lub C++. W pierwszym z plików znajduje się definicja interfejsu IUnknown
i jego metod (QureryInterface, Addref, Release) oraz definicja
interfejsu IClassFacotry, posiadającego metodę CreateInstance, służącą
do stworzenia obiektu klasy na podstawie jej identyfikatora CLSID17 
W drugim pliku znajdują się definicje typów danych. Słowo kluczowe object
w linii z identyfikatorem oznacza, że definiowany jest interfejs związany z
komponentem COM18 . Na interfejs składa się dziewięć funkcji: trzy
dziedziczone z interfejsu IUnknown oraz sześć zdefiniowanych przez
programistę: autoryzacja, czy_klient_istnieje, dodaj_nowego,
rejestruj
, czy_aktywna, zakoncz. Definicja metody rejestruj
wygląda następująco:

HRESULT rejestruj([in, string] LPSTR PESEL, [in, string] LPSTR numer_tel, [out,
size_is(128)] LPSTR informacje, [out] int* wynik);

HRESULT jest typem zwracanym przez metodę.19 
Pierwszy argument wywołania metody PESEL jest typu LPSTR, który
odpowiada wskaźnikowi do znaku (char*)20  w języku C lub C++.
W nawiasie kwadratowym zdefiniowano, iż argument PESEL jest przekazywany
z miejsca wywołania do metody (in) i ma być traktowany jak łańcuch
znaków (string). Drugi argument numer_tel jest również łańcuchem
znaków przekazywanym do metody. Atrybut informacje jest zwracany z
procedury
do miejsca wywołania i jest on – podobnie jak pozostałe
– łańcuchem znaków z tą różnicą, że określony jest jego rozmiar (size_is(128)).
Jest to wymagane dla łańcuchów zwracanych. Ostatnim parametrem jest wynik,
będący wskaźnikiem do zmiennej typu integer. Wynik jest przekazywany z
funkcji do miejsca wywołania.

Definicja powyższego interfejsu w języku C wygląda
następująco21 :

MIDL_INTERFACE("96C3D182-CF70-11d5-BCE4-00C0DFAA8D1D")

typedef struct i_rejestracjaVtbl{

HRESULT (*QueryInterface)(i_rejestracja *This,REFIID riid,void **ppvObject);

ULONG (*AddRef)(i_rejestracja *This);

ULONG (*Release)( i_rejestracja *This);

HRESULT (*autoryzacja)(i_rejestracja *This, LPSTR kod, LPSTR haslo,
int *wynik);

HRESULT (*czy_klient_istnieje)(i_rejestracja *This,LPSTR PESEL,int*wynik);

HRESULT (*dodaj_nowego)(i_rejestracja *This,LPSTR PESEL,LPSTR
nazwisko, LPSTR imie, LPSTR adres,int *wynik);

HRESULT (*rejestruj)(i_rejestracja *This, LPSTR PESEL,LPSTR
numer_tel,LPSTR informacje, int *wynik);

HRESULT (*czy_aktywna)(i_rejestracja *This, LPSTR numer_umowy, int*wynik);

HRESULT (*zakoncz)(i_rejestracja *This, int *wynik);

}i_rejestracjaVtbl;

interface i_rejestracja{

struct i_rejestracjaVtbl *lpVtbl;

};

Dodatkowy argument This nie występujący w
specyfikacji interfejsu, związany jest z identyfikacją obiektu, do którego
odnoszą się wywoływane funkcje [1]. Interfejs może być zdefiniowany również
w języku C++. Przykładowa (ale działająca) definicja interfejsu i_rejestracja
została przedstawiona poniżej. Posłuży ona też do dalszej implementacji
komponentu COM. Narzędzie MIDL generuje na podstawie definicji w IDL zarówno
definicję interfejsu w języku C jak i w języku C++. Pomimo
innego zapisu, są one sobie funkcjonalnie równoważne. Warto zwrócić uwagę
na to, że w zapisie interfejsu w języku C++ użyto dziedziczenia (z
interfejsu IUnknown) do wyrażenia faktu istnienia w interfejsie funkcji QueryInterface,
AddRef, Release
22 . Makro MIDL_INTERFACE(…) jest
rozwijane w różny sposób, zależnie od wersji bibliotek i narzędzi, ale na
potrzeby tych rozważań można przyjąć, że jest ono rozwijane do słowa
kluczowego struct.23 

MIDL_INTERFACE("96C3D182-CF70-11d5-BCE4-00C0DFAA8D1D")

i_rejestracja : public IUnknown{

public:

virtual HRESULT STDMETHODCALLTYPE autoryzacja(

/* [string][in] */ LPSTR kod,

/* [string][in] */ LPSTR haslo,

/* [out] */ int __RPC_FAR *wynik) = 0;

virtual HRESULT STDMETHODCALLTYPE czy_klient_istnieje(

/* [string][in] */ LPSTR PESEL,

/* [out] */ int __RPC_FAR *wynik) = 0;

virtual HRESULT STDMETHODCALLTYPE dodaj_nowego(

/* [string][in] */ LPSTR PESEL,

/* [string][in] */ LPSTR nazwisko,

/* [string][in] */ LPSTR imie,

/* [string][in] */ LPSTR adres,

/* [out] */ int __RPC_FAR *wynik) = 0;

virtual HRESULT STDMETHODCALLTYPE rejestruj(

/* [string][in] */ LPSTR PESEL,

/* [string][in] */ LPSTR numer_tel,

/* [size_is][out] */ LPSTR informacje,

/* [out] */ int __RPC_FAR *wynik) = 0;

virtual HRESULT STDMETHODCALLTYPE czy_aktywna(

/* [string][in] */ LPSTR numer_umowy,

/* [out] */ int __RPC_FAR *wynik) = 0;

virtual HRESULT STDMETHODCALLTYPE zakoncz(

/* [out] */ int __RPC_FAR *wynik) = 0;


};

Wszystkie metody zadeklarowane w powyższej strukturze są
abstrakcyjne (co wynika z faktu, że interfejs nie definiuje metod), stąd człon
=0 po każdej metodzie. Myśląc w kategoriach C++ (a nie modelu COM),
interfejs jest po prostu klasą abstrakcyjną. Makro STDMETHODCALLTYPE
określa konwencję wywołań funkcji24 , konwencja ta jest standardowo używana
przez Windows API.

Implementacja przykładowego komponentu COM

Najłatwiej jest zaimplementować komponent COM używając
dziedziczenia z klasy interfejsu, chociaż możliwa jest implementacja
strukturalna w „czystym” języku C (jak i każdym innym języku
mającym wskaźniki do funkcji). Przedstawiona poniżej klasa CMyImpl
implementuje funkcjonalność komponentu COM. Łatwo zauważyć, że
klasa-implementacja implementuje metody interfejsu i_rejestracja oraz
metody interfejsu IUnknown (QureyInterface, AddRef, Release). Dodatkowo
klasa posiada konstruktor (CMyImpl), destruktor (~CMyImpl) oraz
zmienną m_cRef służącą do zliczania odwołań do komponentu COM.
Zgodnie z konwencją przyjętą w języku C++, deklaracja klasy i
definicja jej metod są rozdzielone i znajdują się odpowiednio w plikach z
rozszerzeniem *.h oraz *.cpp. Plik myimpl.h wygląda następująco:

struct CMyImpl : i_rejestracja{

HRESULT __stdcall QueryInterface(REFIID riid,void** ppv);

ULONG __stdcall AddRef(void);

ULONG __stdcall Release(void);

HRESULT STDMETHODCALLTYPE autoryzacja(

/* [string][in] */ LPSTR kod,

/* [string][in] */ LPSTR haslo,

/* [out] */ int __RPC_FAR *wynik);

HRESULT STDMETHODCALLTYPE czy_klient_istnieje(

/* [string][in] */ LPSTR PESEL,

/* [out] */ int __RPC_FAR *wynik);

HRESULT STDMETHODCALLTYPE dodaj_nowego(

/* [string][in] */ LPSTR PESEL,

/* [string][in] */ LPSTR nazwisko,

/* [string][in] */ LPSTR imie,

/* [string][in] */ LPSTR adres,

/* [out] */ int __RPC_FAR *wynik);

HRESULT STDMETHODCALLTYPE rejestruj(

/* [string][in] */ LPSTR PESEL,

/* [string][in] */ LPSTR numer_tel,

/* [size_is][out] */ LPSTR informacje,

/* [out] */ int __RPC_FAR *wynik);

HRESULT STDMETHODCALLTYPE czy_aktywna(

/* [string][in] */ LPSTR numer_umowy,

/* [out] */ int __RPC_FAR *wynik);

HRESULT STDMETHODCALLTYPE zakoncz(

/* [out] */ int __RPC_FAR *wynik);

CMyImpl();

~CMyImpl();

unsigned long m_cRef;


};

Poniżej znajduje się plik myimpl.cpp25 , w którym
zdefiniowano metody klasy CMyImpl, pewne stałe oraz pewne funkcje
pomocnicze takie, jak DecrementLockCount(), IncrementLockCount(),
ObjectCreated(), ObjectDestroyed()
, związane z zarządzaniem komponentem i
jego serwerem. Funkcje pomocnicze nie są jednak istotne z punktu widzenia
modelu COM, a jedynie z punktu widzenia tej konkretnej implementacji.26 

extern "C" const IID IID_i_myinterface;

long g_fClassRegistered = FALSE;

unsigned long g_dwRegister = 0;

void DecrementLockCount();

void IncrementLockCount();

void ObjectCreated();

void ObjectDestroyed();

Stała IID_i_myinterface, to identyfikator GUID
interfejsu i_myinterface, g_fClassRegistered określa, czy
klasa-fabryka została zarejestrowana w rejestrze systemu Windows. Funkcje IncrementLockCount()
oraz DecrementLockCount() służą do zarządzania rejestracją
fabryki komponentów: pierwsza z nich rejestruje w razie potrzeby obiekt i jego
fabrykę, a druga wyrejestrowuje. Funkcje ObjectCreated i ObjectDestroyed
realizują zliczanie liczby komponentów będących w użyciu – gdy ich
liczba jest równa zero, proces serwera – może być zakończony. Powyższe
funkcje są funkcjami użytkownika. Ponieważ ich działanie jest pomocnicze,
nie uwidoczniono ich implementacji. Znajdujący się poniżej konstruktor klasy CMyImpl
ustawia na wartość jeden licznik odwołań do komponentu (nie mylić z
licznikiem obiektów serwera!), zwiększa o jeden licznik obiektów (ObjectCreated)
i w razie potrzeby rejestruje fabrykę obiektów (IncrementLockCount).

CMyImpl::CMyImpl(){

m_cRef=1;

ObjectCreated();

IncrementLockCount();

}

Znajdujący się poniżej destruktor klasy CMyImpl
wyrejestrowywuje, jeśli trzeba, fabrykę obiektów (DecrementLockCount)
i kończy (jeśli zniszczono ostatni obiekt) działanie serwera (ObjectDestroyed).

CMyImpl::~CMyImpl(){

DecrementLockCount();

ObjectDestroyed();

}

Poniższa funkcja QueryInterface służy do
odpytywania o interfejsy, argument riid jest identyfikatorem GUID
interfejsu, o który zapytuje komponent. Zmiennej wskaźnikowej ppv
zostanie przypisany (jeśli wywołanie się powiedzie) wskaźnik do interfejsu
identyfikowanego przez riid, lub NULL w przypadku niepowodzenia
(kiedy komponent nie implementuje danego interfejsu). Funkcja27  IsEqualGUID
zwraca wartość TRUE, gdy dwa identyfikatory GUID są równe.
Rzutowanie *ppv = (i_rejestracja*) this jest rzutowaniem w górę i jest
konieczne, aby interfejsowi i_rejestracja (czyli klasie abstrakcyjnej, która
jest klasą bazową dla CMyImpl) przypisać wskaźnik do klasy CMyImpl28 ,
która z kolei jest klasą pochodną, implementującą interfejs.29 
Poprzez wywołanie metody AddRef zwiększana jest o jeden wartość
licznika odwołań do komponentu COM.

HRESULT __stdcall CMyImpl::QueryInterface(REFIID riid,void** ppv){

if (IsEqualGUID(riid,IID_IUnknown) || IsEqualGUID(riid, IID_i_rejestracja))

*ppv = (i_rejestracja*) this;

else{

*ppv=NULL;

return E_NOINTERFACE;

}

((IUnknown*)*ppv)->AddRef();

return S_OK;

}

Funkcja AddRef zwiększa o jeden wartość licznika
odwołań do komponentu COM.30 

/ULONG __stdcall CMyImpl::AddRef(void){

InterlockedIncrement((long*) &m_cRef);

return m_cRef;

}

Funkcja Release zmniejsza o jeden wartość licznika
odwołań do komponentu COM; jeśli jest ona równa zero, obiekt jest
usuwany.

ULONG __stdcall CMyImpl::Release(void){

unsigned long count;

count = m_cRef - 1;

if (InterlockedDecrement((long*)&m_cRef) == 0){

count = 0;

delete this;

}

return count;

}

Zaprezentowane powyżej funkcje służą do tworzenia i
niszczenia komponentu oraz realizują wymagania standardu COM. Funkcje
znajdujące się poniżej realizują funkcjonalność obiektu w sensie
implementacji metod interfejsu i_rejestracja. Podobnie, jak w przypadku
architektury CORBA, zaprezentowano szkielet; każda funkcja wyświetla
przekazane do niej argumenty i ewentualnie zwraca jakąś wartość.

HRESULT STDMETHODCALLTYPE CMyImpl::autoryzacja(


/* [string][in] */ LPSTR kod,

/* [string][in] */ LPSTR haslo,

/* [out] */ int __RPC_FAR *wynik){

printf("n Server::autoryzacja(kod=%s, haslo=%s)",kod,haslo);

wynik = 0;

return S_OK;

}

HRESULT STDMETHODCALLTYPE CMyImpl::czy_klient_istnieje(


/* [string][in] */ LPSTR PESEL,

/* [out] */ int __RPC_FAR *wynik){

printf("n Serwer::czy_klient_istnieje(PESEL=%s),",PESEL);

*wynik = 1;

return S_OK;

}

HRESULT STDMETHODCALLTYPE CMyImpl::dodaj_nowego(


/* [string][in] */ LPSTR PESEL,

/* [string][in] */ LPSTR nazwisko,

/* [string][in] */ LPSTR imie,

/* [string][in] */ LPSTR adres,

/* [out] */ int __RPC_FAR *wynik){

printf("n dodaj_nowego(PESEL=%s, nazwisko=%s, imie=%s, adres=%s)",PESEL,nazwisko,imie,adres);

*wynik = 1;

return S_OK;

}

HRESULT STDMETHODCALLTYPE CMyImpl::rejestruj(


/* [string][in] */ LPSTR PESEL,

/* [string][in] */ LPSTR numer_tel,

/* [size_is][out] */ LPSTR informacje,

/* [out] */ int __RPC_FAR *wynik){

printf("n serwer::rejestruj(PESEL=%s, numer_tel=%s)",PESEL,numer_tel);

*wynik = 1;

strcpy(informacje, "No: 123456");

return S_OK;

}

HRESULT STDMETHODCALLTYPE CMyImpl::czy_aktywna(


/* [string][in] */ LPSTR numer_umowy,

/* [out] */ int __RPC_FAR *wynik){

printf("n serwer::czy_aktywna(numer umowy = %s)",numer_umowy);

*wynik = 1;

return S_OK;

}

HRESULT STDMETHODCALLTYPE CMyImpl::zakoncz(


/* [out] */ int __RPC_FAR *wynik){

printf("n serwer::zakoncz()");

*wynik = 5;

return S_OK;

}

Następnym etapem jest utworzenie serwera komponentu oraz
rozbudowanie powyższych funkcji tak aby miały wymaganą funkcjonalność.

c.d.n.

Literatura

[1] Clemens Szyperski Oprogramowanie Komponentowe WNT
2001

[2] Dokumentacja techniczna modelu COM

[3] B. Eckel Tinking in Java Prentice Hall PTR

[4] Professional Java Server Programming Wrox 1999


1. Przedstawiono szkielet realizujący komunikację. Jego
przypomnienie ma również pozwalać na porównanie ze szkieletem obiektu COM o
takiej samej funkcjonalności.

2. Oczywiście przy założeniu, że ORB został zainicjowany,
a referencja do interfejsu której będziemy używali, jest referencją do
obiektu zdalnego.

3. Oraz z kilku interfejsów związanych z realizacją architektury CORBA, nie
wprowadza on jednak własnych operacji w stosunku do interfejsu i_rejestracja.
Na tym poziome rozważań wystarczy przyjąć, że interfejs i_rejestrajca jest
tożsamy z i_rejestracjaOperations.

4. Chodzi o same języki obiektowe a nie architektury typu CORBA, trzeba pamiętać,
że te ostatnie nie stanowią integralnej części języka.

5. Sygnatura funkcji, to iloczyn kartezjański typów argumentów,
strzałka, typ wartości zwracanej.

6. Bardziej precyzyjnie: wskaźnik do znaku.

7 Dla uproszczenia zapisu użyto makra #define PRZYPISZ(a,b,c,d)
(a).pf1=(b); (a).pf2=(c); (a).pf3=(d);

8. Czyli sparametryzowanych w podany powyżej sposób.

9. Wskaźnika na poziomie języka programowania

10. Oczywiście różne komponenty mogą implementować te
same interfejsy.

11. Globalnie unikatowe identyfikatory pozwalają na
manipulację obiektami i interfejsami. W systemie Windows identyfikatory
zarejestrowanych obiektów są przechowywane w rejestrze systemu Windows.
Do jego przeglądania można użyć narzędzia regedit.

12. Za znalezienie serwera komponentu odpowiada biblioteka COM.

13. Oczywiście nic nie stoi na przeszkodzie, aby zbudować
nowy komponent, który „na nowo” implementuje „stare”
interfejsy. Zauważmy jednak, że w tym wypadku „nowy” komponent będzie
używany dokładnie tak samo jak stary.

14 Tzn. takim, którego serwer znajduje się w systemie zdalnym.

15. Jeśli tak to zamiast NULL podaje się wskaźnik do
interfejsu IUnknown obiektu „nadrzędnego” agregującego.

16. Jest to raczej informacja o tym, jakimi typami serwerów jesteśmy
zainteresowani. Wartość CLSCTX_SERVER oznacza, że szukamy serwera wewnątrz-procesowego,
zewnątrz procesowego lub zdalnego. Wartość INPROC_SERVER oznacza, że
preferujemy serwer wewnątrz-procesowy.

17. Klasa implementuje ten interfejs. Wymagane jest, aby klasa
była zarejestrowana w systemie.

18. Nie zaś interfejs związany z RPC, jako że MIDL może być
również używany do generowania stopek RPC.

19. Ale wartość zwracana mówi o tym, czy wywołanie powiodło
się i nie jest tożsama z wartością zwracaną z tej samej funkcji w
interfejsie i_rejestracjaOperations. Służy ona np. do raportowania błędów
związanych z RPC.

20. Definicja typu LPSTR znajduje się w pliku wtypes.idl.

21. Nieco uproszczony dla czytelności w stosunku do
wygenerowanego przez MIDL.

22. Cały czas należy pamiętać, że dziedziczenie użyte do
implementacji komponentu nie ma nic wspólnego z, nie istniejącym zresztą,
dziedziczeniem na poziomie modelu COM.

23. Strukturę można traktować jak klasę, której wszystkie składowe są
publiczne.

24. Argumenty są przekazywane od lewa do prawa poprzez wartość
(za wyjątkiem wskaźników lub referencji), wywoływana funkcja zdejmuje swoje
argumenty ze stosu.

25. Oczywiście opisy nie wchodzą w skład tego pliku.

26. Implementacja tych funkcji zostanie zaprezentowana przy
okazji omówienia serwera komponentu.

27. W C++ jest to funkcja inline, w C jest to makro.

28. Słowo kluczowe this użyte w obrębie metody klasy
reprezentuje wskaźnik do obiektu, którego metodę wywołano.

29. Należy zauważyć, że „od środka” nie odróżniamy
interfejsów i_rejestracja i IUnknown – jest to dozwolone; użytkownik
obiektu nie zdaje sobie z tego sprawy.

30. Użycie funkcji InterlockedIncrement zapewnia, że robi to tylko
jeden wątek w danej chwili.