Bevezetés a CORBA architektúrába

 

Ahhoz, hogy az elkövetkezõ fejezetek tartalmát megérthessük, szükségünk van a CORBA szabvány bizonyos fokú ismeretére. Mivel a szabvány eléggé szerteágazó és bonyolult, ezért ebben a fejezetben csak egy áttekintésre törekszünk. Az érdeklõdõ olvasónak a következõ könyvet tudjuk ajánlani:

Michi Henning - Steve Vinoski:

Advanced CORBA Programming with C++

A fenti kötet nagy elõnye a hivatalos szabvány dokumentummal (TODO) szemben, hogy programozóknak készült, és így szinte minden, a CORBA használata során felmerülõ kérdésre választ kaphatunk belõle.

1. A CORBA története

A CORBA szabványt az OMG (Object Management Group) hozta létre. Az OMG egy ipari konzorcium (TODO), amely azért jött létre, hogy megoldást találjon a heterogén, hordozható osztott-rendszerek létehozása során felmerülõ problémákra. Az OMG 1989-ben alakult, és mára már több mint 800 tagja van.

A CORBA szabvány elsõ változata 1991-ben jelent meg, ezt több javított verzió követte. Jelenleg a 2.3 verzió az aktuális, mi is ezt fogjuk tárgyalni. Érdemes megjegyezni, hogy már készülõben van a 3.0 verzió, melynek legfontosabb újdonsága a CORBA komponents modell. Ez a modell a Windows világból már ismert OLE/ActiveX, és a JAVA világból származó Enterprise JAVABeans specifikációk adaptálása a CORBA-hoz. Segítségével a fejlesztõk újrafelhasználható CORBA komponenseket hozhatnak létre.

2. A CORBA jellemzõi

A CORBA célja osztott, objektum-orientált alkalmazások fejlesztésének segítése. A CORBA a következõ jellemzõkkel rendelkezik:

3. Alapfogalmak

Ebben a szakaszban a CORBA alapvetõ fogalmait igyekszünk ismertetni. A fogalmak neve ismerõs lehet, de a CORBA-ban ezekhez a nevekhez speciális jelentés tartozik, amely nem feltétlenül egyezik meg azzal a jelentéssel, amit az olvasó megszokott.

A következõ ábra a CORBA architektúráját szemlélteti:

<TODO>

4. Az OMG IDL (Interface Definition Language)

 

Ahhoz hogy egy kliens kéréseket intézhessen egy CORBA objektumhoz, ismernie kell az objektum által nyújtott felülelet. A CORBA-ban az objektumok felületét az IDL nyelven írhatjuk le. Az IDL nem programozási nyelv, egyedüli célja a felületek leírása. Az alábbiakban egy egyszerû IDL definíciót láthatunk:

interface Account {

void deposit(in long amount);

void withdraw(in long amount);

long balance();

};

 

A fenti példa egy banki számlát reprezentáló objektum felületét írja le. Az objektumnak három mûvelete van, melyek segítségével pénzt tehetünk be, pénzt vehetünk ki, illetve lekérdezhetjük a számla egyenlegét.

Mivel az IDL deklaratív nyelv, nem alkalmas számítások leírására, csakis a kommunikációs felületek definiálhatóak benne. Ahhoz, hogy IDL definícókban lévõ információt programjainkban felhasználhassuk, szükségünk van nyelvi leképezésekre. Egy nyelvi leképezés azt adja meg, hogy az IDL hogyan fordítható le egy adott programozási nyelvre. A nyelvi leképezés rögzíti, hogy az IDL nyelv egyes konstrukcióinak a programozási nyelv mely konstrukciói felelnek meg. A leképezés ezenkívül azt is megadja, hogy az adott nyelven írt alkalmazások hogyan érhetik el a különbözõ CORBA szolgáltatásokat.

A jelenleg használt programozási nyelvek legtöbbjéhez létezik nyelvi leképezés. Ezek legnagyobb részét és OMG alkotta meg, és a CORBA szabvány részét képezik. Ezen dolgozat egyik célja éppen egy nyelvi leképezés definiálása a Clean programozási nyelvhez.

Az IDL definíciókat általában fileokban tároljuk. Az IDL fileokból a nekik megfelelõ nyelvi elemeket egy IDL fordító (IDL compiler) állítja elõ. Az IDL fordító a CORBA szoftver termékek része. Mivel a gyakorlati CORBA fejlesztésekhez használt programozási nyelvek köre szûk (C++ és JAVA), ezért a legtöbb IDL fordító csak ezt a két nyelvet támogatja. A diplomamunka keretében elkészült egy IDL fordító is, amely az IDL fileokat Clean-re fordítja.

5. Az IDL nyelv elemei

5.1 Beépített típusok

Az IDL a következõ beépített típusokkal rendelkezik:

5.2 Elnevezett típusok

A typedef kulcsszó használatával új nevet adhatunk egy típusnak:

typedef short TemperatureType;

 

A typedef-ek használata javítja az IDL definíciók olvashatóságát. Ezenkívül azért is szükség van rájuk, mert bizonyos helyzetekben csak névvel rendelkező típusok használata engedélyezett. Ilyen például a struktúrák elemeinek típusa vagy az IDL műveletek paramétereinek típusa.

 

5.3 Felsorolási típusok

Az IDL felsorolási típusai nagyon hasonlóan a C++-ból megismert felsorolási típusokhoz:

enum Color { red, green, blue };

 

Különbség a C++-hoz képest, hogy nem adhatóak meg az egyes nevekhez tartozó numerikus értékek.

 

5.4 Direkt szorzat típusok (struktúrák)

struct Person {

string name;

short age;

string profession;

};

 

5.4 Unió típusok

Az IDL tervezõi szakítottak a C/C++ unió típusával, annak gyenge típusossága miatt. Minden IDL unió típusnak van egy diszkriminátora (discriminant), amelynek értéke dönti el, hogy az unió éppen milyen típusú értéket tartalmaz. A diszkriminátornak egész típusúnak kell lennie (short, long, char, boolean, felsorolási típusok). Lehetõség van egy default ág megadására, amely akkor aktív, ha a diszkriminátor értéke egyik felsorolt értékkel sem egyezik meg. Egy példa:

union UselessUnion switch (short) {

case 0:

long member1;

case 1:

case 2:

short member2;

default:

Color member3;

};

 

Egy gyakori alkalmazása az unió típusnak az opcionális értékek megadása:

union AgeOpt switch (boolean) {

case TRUE:

unsigned short age;

};

 

5.5 Tömb típusok

Az IDL-ben lehetõségünk van mind egy, mind többdimenziós tömbök használatára. Az tömb elemeinek típusa tetszõleges lehet. Például:

typedef Color ColorVector[10];

typedef Color ColorMatrix[5][10];

 

5.6 Szekvencia típusok

Az IDL szekvenciák megfelelnek a programozás elméletbõl megismert szekvencia típus fogalmának. Egy IDL szekvencia hossza és elemeinek típusa tetszõleges lehet. A programozónak lehetõsége van a maximális hossz megadására. Például:

typedef sequence<Color> Colors;

typedef sequence<long, 100> Numbers;

 

Az első szekvencia hossza tetszőleges lehet, míg a másodiké maximum 100.

5.7 Rekurzív típusok

Struktúrák és szekvenciák felhasználásával lehetõségünk van rekurzív szerkezetû típusok definiálására. Például:

struct Tree {

long value;

sequence<Tree> children;

};

 

Az ilyen struktúrák felhasználásával tetszőleges bonyolultságú adatstruktúrák vihetők át a klients és a szerver program között.

5.8 Konstansok

Az IDL konstansok szintaxisa és szemantikája megegyezik a C++ konstansaival. Példák:

const float PI = 3.14159;

const string LAST_WORDS = „My god, it’s full of stars!"

const Color FAVORITE_COLOR = blue;

const long THE_NUMBER = 42;

 

5.9 Interfészek

Az IDL nyelv legfontosabb célja, hogy lehetõvé tegye az objektumok által nyújtott felület leírását. Ezeket a felületeket interfész típusok (interface types) írják le. Az interfész típusok hasonlatosak a JAVA interfész fogalmához, azaz csak a objektumok felületét adják meg, az implementációt nem. Az interfész tuljadonképpen mûveleti szignatúrák gyûjteménye. Bármely CORBA objektum megvalósíthat egy adott interfészt azzal, hogy implementálja az interfészben szereplõ mûveleteket. Az interfész típusok egyenértékûek más IDL típusokkal, azaz használhatjuk õket struktúrák elemeiként, mûveletek ki és bemenõ paramétereiként stb. Ahol az IDL definícióban egy interfész típus szerepel, ott futás közben egy, az adott interfészt megvalósító objektumra mutató objektum referencia kell, hogy szerepljen.

Egy egyszerű példa:

interface Bank {

Account create_account(in long initial_balance);

void destroy_account(in Account account);

};

 

A fenti példa egy Bank felületét írja le. Ha egy kliens program meghívja a Bank objektum create_account műveletét, akkor egy Account típusú objektum referenciát kap vissza, melyet felhasználhat arra, hogy az Account objektumnak CORBA kéréseket küldjön, például lekérdezze az egyenleget.

Mûveletek definiálása

Egy mûvelet definicíója a következõ részekbõl áll:

A visszatérési érték tetszõleges IDL típus lehet. Ha a mûvelet nem ad vissza eredményt, akkor azt a C++-hoz hasonlóan, a void kulcsszóval jelöljük.

Minden paraméternek van egy neve, típusa, és iránya (direction). A paraméterátadás iránya háromféle lehet:

A következő példa szemlélteti a paraméterátadási lehetőségeket:

void op1(in long n1, out long n2, in out long n3);

 

Kivételkezelés

Hasonlóan a JAVA-hoz, lehetőségünk van kivételek megadására a műveletek definiálásakor. Minden kivétel egy külön típus, melyek szerkezetileg hasonlóak a struktúrákhoz:

exception RangeError {

unsigned long min_value;

unsigned long max_value;

};

 

Miután definiáltuk a kivétel típusát, a művelet definíciójában jeleznünk kell, hogy a művelet kiválthatja az adott kivételt:

void can_fail(in long l) raises (RangeError);

 

A felhasználó által definiált kivételeken kívül minden IDL mûvelet kiválthat néhány elõre definiált kivételt is. Ilyenek például: OBJECT_NOT_EXIST, COMM_FAILURE stb.

Attribútumok

A műveletek kívül az IDL interfészek tartalmazhatnak un. attribútumokat is. Egy attribútum nem más, mint a CORBA objektum állapotának egy része melyet az interfészen keresztül lekérdezni/módosítani lehet. Például:

attribute short temperature;

readonly attribute average_temperature;

 

A nyelvi leképezések feladata annak megadása, hogy a kliens programok hogyan tudják lekérdezni/módosítani ezeket az attribútumokat. A legelterjedtebb megoldás egy set_<ATTRIBUTE NAME> és egy get_<ATTRIBUTE NAME> művelet generálása az interfész egyéb műveletei mellé.

Öröklődés

Lehetőség van arra is, hogy interfészeink örököljenek egymástól. Ez hasonló a JAVA által használt megoldáshoz. Például:

interface Vehicle {

...

};

interface Car : Vehicle {

...

};

Ahhoz, hogy egy objektum megvalósítsa a Car interfészt, meg kell valósítania a Vehicle interfész metódusait is. Ha valahol a szülő interfészre mutató objektum referenciát kell használnunk, használhatjuk egy gyerek interfészre mutató referenciát (widening). A nyelvi leképezések arra is lehetőséget nyújtanak, hogy egy szülő interfészre mutató referenciát a gyerek interfészre mutató referenciává konvertáljunk (narrowing).

Minden IDL interfész implicite örököl az Object nevű interfésztől. Ez azt jelenti, hogy lehetőségünk van olyan IDL műveleteket definiálni, melyek bármilyen típusú interfésszel működnek. Például:

interface HashTable {

void add(in KeyType key, in Object value);

Object lookup(in KeyType key);

}

 

Egy objektum referencia lehet NIL. Ez az objektum referencia „nem mutat sehová", ha megpróbáljuk felhasználni egy CORBA kérés céljaként, hibajelzést kapunk. A nil referenciát például a fenti lookup mûvelet használhatja annak jelzésére, hogy nem létezik adott kulcsú objektum a hash táblában.

5.10 Az IDL modulrendszere

Nagyobb alkalmazások készítéséhez elengedhetetlenül szükséges egy modulrendszer, melynek segítségével IDL definícióinkat rendszerezhetjük és az esetleges névütközéseket kizárhatjuk. Az IDL modulrendszere a C++-ból ismert namespace-okhoz hasonlatos. Lehetõségünk van egy hierarchikus modulrendszer kialakítására, tetszõleges mélységben.

Példa:

module A {

module B {

// IDL definitions

typedef short Temperature;

}

// More IDL definitions

}

 

Minden IDL definícióhoz kétféle nevet rendelhetünk: a rövid nevet, illetve a teljes nevet (FQN-Fully Qualified Name). A fenti Temperature típus rövid neve Temperature, a teljes neve pedig A::B::Temperature. A teljes nevet felhasználhatjuk arra is, hogy a modulrendszeren belül máshol elhelyezkedő definíciókra hivatkozzunk:

typedef sequence<A::B::Temperature> Temps;

 

Az OMG által definiált konstansok, típusok, interfészek stb. mind a CORBA modulon belül helyezkednek el, így például a fent említett Object típus teljes neve CORBA::Object.

5.11 Az Any típus

Utoljára hagytuk az any típus tárgyalását annak speciális jellege miatt. Az any típus egyike az IDL beépített típusainak. Sajátossága, hogy bármely más IDL típus értékét képes tárolni. Ez nagyon hasznossá teszi akkor ha nem tudjuk elõre, hogy milyen típusú értékeket szeretnénk átvinni a kliens és a szerver között.

Az any típus hasonló a C++ void * típusához. Nagy előnye vele szemben, hogy a konkrét érték mellet típusinformációt is magában hordoz, így például egy long-ot tartalmazó any-ből nem lehet egy sztring értéket kiolvasni. Azt, hogy egy any értékét hogyan lehet beállítani és kiolvasni, a nyelvi leképezés határozza meg.

Az any típushoz kapcsolódik egy másik CORBA típus, a TypeCode. A TypeCode típus, mint a neve is mutatja, típusinformációt hordoz. Segítségével tudjuk például meghatározni, hogy egy any-ben tárolt érték tulajdonképpen milyen típusú is. A TypeCode típus definícióját a CORBA szabvány IDL-ben adja meg.

6. Az ORB pszeudo-objektum

A CORBA szabvány a programozó rendelkezésére álló szolgáltatásokat un. pszeudo-objektumok segítségével definiálja. A pszeudo-objektumok hasonlatosak a felhasználó által definiálható objektumokhoz, de nem feltétlenül igazak rájuk azok a szabályok, amelyek a valódi objektumokra igazak. Például, a pszeudo objektumok referenciáit nem lehet átküldeni egy CORBA kérés során a szerver oldalra, nem hozható létre tetszõleges számú belõlük stb. A szabvány a pszeudo-objektumokat (pszeudo)IDL segítségével írja le. Ennek legnagyobb elõnye, hogy ezeket az objektumokat a programozó ugyanolyan módon használhatja, mint az általa definiált objektumokat.

A legfontosabb pszeudo objektum az ORB objektum. Ennek a definíciója az alábbi:

module CORBA {

interface ORB {

string object_to_string(in Object obj);

Object string_to_object(in string obj);

Object resolve_initial_references(in string service);

// more IDL operations

}

}

 

Az ORB interfész nagyon sok mûveletet tartalmaz, de nekünk csak a fenti három a lényeges, mivel csak ezekkel fogunk találkozni a késöbbiekben.

Az object_to_string és a string_to_object mûveletek segítségével objektum referenciák és sztringek között konvertálhatunk. Erre például akkor van szükség, ha egy szerver objektum referenciáját el szeretnénk juttatni a kliens programhoz. A sztring formátumu objektum referenciát IOR-nak (Interoperable Object Reference) nevezzük. Az Interoperable elnevezés arra utal, hogy az IOR formátuma szabványosított, így az egyik CORBA implementáció által elõállított IOR-t be fel tudja használni egy másik CORBA implementációt használó program is.

A resolve_initial_references művelet segítségével bizonyos fontos CORBA objektumok referenciáit szerezhetjük meg. Ilyen lehet például a Naming Service (lásd később), a Portable Object Adapter, etc.

7. A CORBA szolgáltatások

Az alkalmazásfejlesztés megkönnyítésére az OMG definiált néhány szolgáltatást (CORBAServices), melyeket a CORBA-t használó programok igénybe vehetnek. Ezeknek a szolgáltatásoknak a felülete OMG IDL-ben van megadva. A fontosabb szolgáltatások a következõk:

8. Az Interface Repository

Az Interface Repository (IR) nem más, mint olyan CORBA objektumok halmaza, amelyet az IDL fileokban tárolt információt teszik elérhetûvé CORBA alkalmazások számára. Ez olyan alkalmazásokban lehet fontos, ahol futás közben van szükség típusinformációra. Ilyen alkalmazás lehet például egy „object explorer", mellyel a rendszerben levõ objektumokat vizsgálhatjuk meg, üzeneteket küldhetünk nekik stb.

Az IR-t általában egy, a CORBA szoftverhez adott szerverprogram valósítja meg. Némely IDL fordító képes arra, hogy az IDL fileok tartalmát „feltöltse" az IR-be, illetve hogy az IR tartalma alapján generáljon kódot.

9. A Dynamic Invocation Interface

Amikor CORBA alkalmazást írunk, általában pontosan tudjuk, hogy mi a használni kívánt szerver objektumok felülete. Az IDL fordító azzal segíti a mi munkánkat, hogy minden IDL interfészhez definiál un. stub-okat. A stub egy olyan nyelvi elem az általunk használt programozási nyelvben, mely segítségével a CORBA kéréseket biztonságos módon küldhetjük el. Például, ha adott az alábbi CORBA interfész:

interface Echo {

short echo_int(in short what);

};

Egy C++ IDL fordító ebből a definícióból a következő stub-ot generálja:

class Hello {

CORBA::short echo_int(CORBA::short what);

};

Amikor a kliens program meghívja a fenti metódust, a metódus átalakítja a what paramétert átviteli formátumra (marshalling), elküld egy CORBA kérést a szervernek a paraméterrel, megvárja az eredményt, visszaalakítja azt C++-formátumra (unmarshalling), majd visszaadja az eredményt a hívónak.

Látható, hogy a fenti módszer sok hibalehetőségtől megkíméli a felhasználót. Nem fordulhat elő, hogy túl sok, vagy túl kevés paramétert adunk át a műveletnek, olyan objektumnak küldünk üzenetet, melynek nincs is ilyen művelete etc.

A fenti módszer csak akkor működik, ha fordítási időben ismerjük a használni kívánt CORBA objektumok felületét, azaz az IDL fordító által generált stub-okat hozzáfordítottuk a programunkhoz. Mi a helyzet akkor, ha ezt nem tudjuk vagy nem akarjuk megtenni?

Ilyen helyzetekre ígér megoldást a CORBA Dynamic Invocation Interface (DII) szolgáltatása. A DII használatához meg kell adnunk a célobjektumot, a meghívni kívánt művelet nevét, a paraméterek típusát és értékét, valamint a művelet által kiváltható kívételek típusát. Ezeket az adatokat vagy az Interface Repository-ból szerezhetjük meg, vagy bekérhetjük a felhasználótól stb. Miután megadtuk az adatokat, utasítjuk az ORB-t a kérés elküldésére, a válasz megérkezte után pedig feldolgozhatjuk az eredményt.

A DII dinamikus volta miatt nem csoda, hogy a paramétereket és a visszatérési értéket is any típusként kell kezelni. Egyes CORBA szoftverekben az is előfordul, hogy a stub függvényeket a DII segítségével valósítják meg. Ennek a módszernek a hátránya a nagy erőforrásigény és a lassúság.

A stub-oknak van egy „párja" a szerver oldalon, melyeket skeleton-oknak nevezünk. A skeleton-ok a CORBA objektumok implementálásában segítik a programozót. C++-ban például minden skeleton egy absztrakt osztály virtuális függvényekkel, melybõl a programozónak kell a CORBA objektumot megvalósító osztályt származtatnia.

A DII-nek is van szerver oldali párja, a DSI (Dynamic Skeleton Interface). A DSI úgy mûködik, hogy a programozó megadhat egy speciális függvényt, a DIR-t (Dynamic Implementation Routine), melyet az ORB meghív, ha az adott objektumhoz kérés érkezik. A DIR megkapja a mûvelet nevét és általános formában a paramétereket. Feladata a mûvelet eredményének és az kimenõ paraméterek értékének kiszámítása.

A DII-t és a DSI-t azért tárgyaljuk ilyen mélységben, mert sok olyan szoftver, amely a CORBA-t illeszti egy adott programozási nyelvhez, ezt a két szolgáltatást használja az illesztés megvalósításához. A mi Clean-CORBA interfészünk is erre a két technológiára épül.

10. A MICO CORBA implementáció

A fejezet lezárásaként szóljunk néhány szót arról a CORBA implementációról, amelynek segítségével ez a diplomamunka készült.

A MICO egy szabadon felhasználható, nyílt forráskódú (open source) CORBA megvalósítás. Szinte teljes egészében támogatja a CORBA 2.3 szabványt, és elég sokan használják mint akadámiai, mint üzleti körökben. Bõvebb információ a

http://www.mico.org

címen lelhető fel.

A MICO-nak van néhány jellemzője, amelyek szinte ideálissá teszik az ebben a munkában történő felhasználásra:

 

 

 

 

 

 

 

Ismerkedés a Clean programozási nyelvel

Hasonlóan a CORBA-t bemutató fejezethez, ez a fejezet arra törekszik, hogy az olvasót megismertesse a Clean programozási nyelvel. Ez azért hasznos, mert így a funkcionális nyelvekben kevésbé járatos olvasó is képes követni a késõbbi fejezetek tartalmát. Helyhiány miatt nem törekedhetünk a nyelv teljes bemutatására. Az érdeklõdõ olvasónak egy, az Interneten szabadon elérhetõ könyvet tudunk ajánlani (TODO).

1. A Clean nyelvrõl általában

A Clean egy funkcionális programmozási nyelv, amelyet egy, a hollandiai Nijmegen egyetemen mûködõ kutatócsoport hozott létre, és fejleszt ma is. A nyelv rendelkezik a modern funkcionális nyelvektõl elvárható fontosabb jellemzõkkel, azaz:

2. Kifejezések

Egy Clean kifejezés literálokból és függvény applikációkból épül fel. A Clean beépített típusai a következõk:

Egy függvény applikáció a fv. nevébõl és a fv. paramétereibõl épül fel. Például:

add 1 1 Þ 2

inc 5 Þ 6

not True Þ False

A Clean nyelvben az aritmetikai, logikai operátorok is függvények, azzal a különbséggel, hogy lehetőség van őket infix alakban írni:

2 * 2 Þ 4

True && False Þ False

1 == 1 Þ True

Az operátoroknak van priorítása, amely általában megfelel a matematikából ismert konvencióknak. A programozónak lehetõsége van saját operátorokat definiálni.

A legfontosabb adatszerkezet a Clean-ben a lista. A Clean lista típusai hasonlóak a Lisp nyelv listáihoz. Minden listának van egy feje (head), illetve egy törzse (tail). A törzs lehet üres is. Például, az 1,2,3 lista a következõképpen írható le Clean-ben:

[1:[2:[3:[]]

Itt a : mûvelet megfelel a Lisp cons mûveletének, a [] pedig a Lisp nil konstansának. A könnyebb kezelhetőség kedvéért a fenti lista [1, 2, 3] formában is írható.

Mivel a lista típusok nagyon fontos részei a nyelvnek, ezért a nyelv biztosít néhány további eszközt listák előállítására. Ha a lista egy számtani sorozat elemeit tartalmazza, elég csak néhány elemet megadni:

[1..10] Þ [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

[1,3..10] Þ [1, 3, 5, 7, 9]

[1..] Þ [1, 2, 3, .......... (végtelen sorozat)

A Clean-ben alkalmazott lusta kiértékelés biztosítja, hogy a fenti végtelen lista használata egy programban nem vezet végtelen ciklushoz.

Ha bonyolultabb listákat akarunk létrehozni, akkor szükségünk lehet un. listakifejezések (list comprehensions) használatára. Például:

[ x + y | x <- [1..2], y <- [1..3] \\ x == y] Þ [2, 4]

A fenti kifejezés hasonló a matematikában használatos halmazkifejezésekhez. A középső szakaszban található két un. generátor két sorozatot állít elő, melyek elemei az x és y párokba kerülnek. Azokra a párokra, melyekre a harmadik szakaszban található kifejezés igaz, kiértékelődik az első kifejezés, és az ennek eredményeiből alkotott lista lesz a teljes kifejezés eredménye.

A Clean rendszer nagyon sok előre definiált függvényt tartalmaz listákon. Néhány ezek közül:

Listákon kívül lehetõségünk van rendezett párok használatára is. Egy rendezett pár a következõképpen írható: (1, 2). A rendezett párok jelentõségét azt adja, hogy Clean-ben minden függvénynek csak egy visszatérési értéke lehet, ezért több értéket csak párok használatával tudunk visszaadni.

3. Függvénydefiníciók

Egy Clean program nem más, mint függvény definíciók halmaza. Egy függvénydefiníció a következõ formájú:

<fv nev> <formalis parameterek> = <kifejezes>

Például:

add x y = x + y

Minden függvénynek van egy típusa, melynek megadása nem kötelezõ, de növeli a kód olvashatóságát. Ha nem adjuk meg a típust, a fordító levezeti nekünk. A fenti fv. típusa:

add :: Integer Integer -> Integer

A függvényeket alkalmazhatjuk úgy is, hogy nem adjuk meg meg minden paraméterüket. Ekkor egy un. curried függvényt kapunk. Például:

inc = add 1

A fenti függvény típusa az alábbi:

inc :: (Integer -> Integer)

Mint látható, az inc egy argumentum nélküli (konstans) fv, amelynek eredménye egy olyan egyváltozós függvény, amely az argumentumához egyet ad hozzá. Használata:

inc 1 Þ 2

Nagyon fontos eszköze a funkcionális nyelveknek az un. mintaillesztés (pattern matching). Segítségével függvényeinket a matematikában megszokott módon definiálhatjuk:

isZero 0 = True

isZero x = False

A fenti függvényt úgy értékeli ki a rendszer, hogy sorban veszi a bal oldalon található mintákat, és megvizsgálja, hogy a fv. aktuális paraméterei illeszkednek-e a mintára. Ha igen, a függvény értéke az illeszkedõ egyenlõség jobb oldalán található kifejezés lesz.

Mintáinkban nem csak számokat használhatunk, hanem sztringeket, listákat stb. is:

length2 :: [Integer] -> Integer

length2 [] = 0 // Üres lista

length2 [_:xs] = 1 + (length2 xs) // Olyan lista, amelynek törzsét xs-nek

// nevezzük el

Mint a fenti példa mutatja, a mintaillesztés arra is használható, hogy az adatstruktúráinkat részekre bontsuk, a részeket elnevezzük, és a részeket a függvénydefiníció jobb oldalán felhasználjuk. Az aláhúzásjel egy olyan minta, amelynek bármely érték illeszkedik. Akkor érdemes használni, ha az adott értéket nem akarjuk felhasználni a függvényérték kiszámításánál.

Gyakran elõfordul, hogy függvényeink kiszámításához szükségünk van segéddefiníciókra, akárcsak a matematikában. Ezeket a definíciókat egy where kifejezésnek nevezett szerkezetben adhatjuk meg:

length2 [_:xs] = 1 + len

where

len = length2 xs

Mint látható, a segéddefiníciókban felhasználhatók a fv formális paraméterei.

Egy utolsó példaként adjuk meg a fentebb már emlegetett map függvény definícióját egészeket tartalmazó listákra:

map :: [Integer] [Integer->Integer] -> [Integer]

map [] fun = []

map [x:xs] = [fun x:map xs fun]

4. Polimorfizmus és paraméteres típusok

Az elõzõekben ismertetett lista típus un. paraméteres típus azaz eleminek típusa tetszõleges lehet. Létrehozhatunk egészek listáját, függvények listáját stb. Az ilyen típusokat más néven polimorfikus típusoknak is szokás nevezi.

Azokat a függvényeket, amelyek polimorfikus típusokkal végeznek mûveleteket, szintén polimorfikusnak nevezzük. Az egyik legegyszerûbb ilyen fv a length fv:

length :: [a] -> Int

length [] = 0

length [_:xs] = 1 + (length xs)

A fv típusában szereplő a betű egy un. típusváltozó (type variable). Ezeket a változókat a fordítóprogram használja annak megállapítására, hogy a program statikusan helyes-e (type correct).

Fontos észrevennünk, hogy a length fv eredménye csak a lista szerkezetétől függ, és nem a konkrét lista elemektől. Ez teszi lehetővé, hogy bármilyen elemtípusú listára definiáljuk.

A korábban említett rendezett párok is polimorfikus típusok. Az alábbi függvény visszaadja egy rendezett pár első tagját:

fst :: (a, b) -> a

fst (x,_) = x

A típusdeklaráció azt mondja ki, hogy a fv visszatérési értéke megegyezik az argumentumként kapott pár első tagjának típusával.

Utolsó példaként tekintsük a map fv immár általánosított definícióját:

map :: [a] (a -> b) -> [b]

map [] = .....

5. Algebrai és rekord típusok

Az eddig megismert típusok eléggé szegényes lehetőséget nyújtanak adatszerkezetek létrehozására. Az algebrai típusok megszűntetik ezt a kényelmetlenséget. Egy algebrai típusdefiníció azt adja meg, hogy a típus értékei milyen módon hozhatóak létre, azaz milyen a típusértékhalmaz szerkezete. Például:

:: Color = Red | Green | Blue

:: List = Cons Int List

| Nil

:: Person = Person String Int String // Name, Age, Address

Az első definíció egy nagyon egyszerű típust ad meg, amelynek összesen három értéke lehet. A második definíció egy rekurzív típust ad meg, hasonlót a Clean beépített lista típusához. A harmadik definíció egy rekord értékeit tárolja.

Mint látható, az algebrai típusok magukban foglalják a programozáselméletben definiált direkt szorzat és unió típusokat. A definíció jobb oldalán található, az alternatívákat megadó neveket adatkonstruktoroknak (data constructor), a típus nevét pedig típuskonstruktornak (type constructor) nevezzük. A kettő lehet ugyanaz, mint azt a Person típusnál láttuk.

Hasonlóan a lista típusokhoz, az algebrai típusokat is paramétezhetjük más típusokkal:

:: Tree a = Leaf a | Node (Tree a) (Tree a)

Most már megérthetjük, miért hívjuk a típus nevét típuskonstruktornak: azért mert egy már létező típusból (a) egy új típust (Tree a) hoz létre.

Az algebrai típusokat kezelő függvényeket a legegyszerűbben a struktúrájuk szerinti rekurzió segítségével definiálhatjuk. Ehhez segítségünkre van, hogy a mintaillesztés algebrai típusokra is működik:

sum_nodes :: (Tree Int) -> Int

sum_nodes (Leaf i) = i

sum_nodes (Node a b) = (sum_nodes a) + (sum_nodes b)

Az algebrai típusok legnagyobb problémája, hogy a bennük tárolt információk csak mintaillesztés segítségével és csak pozíciónálisan érhetők el:

name (Person n _ _) = n

Ha egy adatkonstruktornak sok paramétere van, fárasztó lehet a paraméterek sorrendjének megjegyzése. Erre nyújt megoldást a Clean rekord típusa:

:: Person = { name :: String, age :: Int, address :: String }

A rekord komponenseit az imperatív nyelvekben megszokott szintaxissal érhetjük el:

get_name p = p.name

Rekord típusú értéket az értékek felsorolásával hozhatunk létre:

{ Person | name = „Wyat Earp", age = „75", address = „Tombstone"}

Lehetőségünk van egy régi rekordból egy új előállítására az un. funkciónális update operátorral:

set_name :: Person String -> Person

set_name person newname = {p & name = newname}

Mivel funkcionális nyelvben vagyunk, ezért természetesen nincs mód egy rekord érték módósítására, csak egy új előállítására. Ha ügyes fordítóprogramunk van, akkor az rájöhet, hogy a régi rekordot már senki nem használja, így a módosítás biztonságosan elvégezhető. Éppen ezt segíti elő a Clean által bevezetett unique típusrendszer.

A Clean rekordtípusainak egyetlen komoly hátránya van: a mezőneveknek egyedieknek kell lenniük, azaz nem lehet két rekord típusban egy „name” mező.

7. Egzisztenciális típusváltozók

Tekintsük újra a régi lista típusunk definícióját:

:: List a = Cons a List | Nil

Ennek a típusnak van egy súlyos hátránya: a lista összes elemének ugyanolyan típusúnak kell lennie. Ezt másképpen úgy is megfogalmazhatjuk, hogy az a típusváltozó univerziálisan van kvantálva, azaz egy List típusú érték létrehozásakor az összes Cons adatkonstruktrornak ugyanolyan típusú argumentumot kell kapnia. A Clean lehetővé teszi egzisztenciálisan kvantált típusváltozók használatát:

:: List = E.a: Cons a List | Nil

A fenti definíció azt jelenti, hogy a típusváltozó értéke az egyes adatkonstruktorok alkalmazásánál más és más lehet, például:

(Cons ‘a’ (Const 5 Nil))

A fenti kifejezés típusa List, amelyben nem szerepel már az elemek típusa. Ez azt is jelenti, hogy a típusinformáció elveszik, így bizonyos kifejezéseknek már nem lehet meghatározni a típusát. Például:

head (Cons x _) = x

Ennek a fv-nek nincsen meghatározható típusa, így a fordító hibajelzést ad.

Úgy tűnhet, hogy az egzisztenciális típusváltozók haszna kevés, hiszen nem tudunk hozzáférni az ilyen típusú értékekben tárolt információhoz. A megoldás az, hogy az adatok mellett az adatokat manipuláló fv-eket is is eltároljuk az adatszerkezetben, így a „külvilágnak" már nem kell azzaz törõdnie, hogy milyen típusú értékeket tárolunk az adatszerkezetünkben. Ennek a módszernek az érdekessége, hogy segítségével objektum-orientált stílusban programozhatunk egy funkcionális nyelvben. A következõ példában Drawable típusú objektumokat definiálunk, amelyek képesek arra, hogy kirajzolják magukat a képernyõre, illetve arréb lehet mozgatni õket.

:: Drawable = E.a: {

local_state :: a,

draw :: a Canvas -> Canvas,

moveBy :: a Int -> a

};

A local_state mező tárolja az objektum belső állapotát, amely objektumonként különböző típusú lehet. A draw fv kirajzolja az objektumot egy Canvas-ra, amely a képernyőt reprezentáló típus. A moveBy fv elmozgatja az objektumot a megadott távolsággal. A fenti típus az objektumorientált stílusban egy absztrakt ősosztálynak felel meg, hiszen csak a műveletek definícióját tartalmazza, az implementációt nem.

Ahhoz, hogy a fenti két műveletet fel tudjuk használni, szükségünk lesz segédfüggvényekre, amelyek elrejtik az objektumok lokális állapotát:

draw :: Drawable Canvas -> Canvas

draw d canvas = d.draw d.local_state canvas

moveBy :: Drawable Int -> Drawable

moveBy d distance = {d & d.local_state = d.moveBy d.local_state distance}

Amikor a fordító ezeket a függvénydefiníciókat elemzi, akkor a Drawable típus definíciója alapján el tudja dönteni, hogy például a d.draw fv első argumentumának típusa megegyezik a d.local_state mező típusával, azaz a kifejezés statikusan helyes.

Most már létrehozhatunk néhány Drawable típusú objektumot:

createPoint :: Int Int -> Drawable

createPoint x y = { Drawable |

local_state = (x, y),

draw ls canvas = <code to draw a point on the canvas>,

moveBy ls distance = (x + distance, y + distance)

}

Az objektumorientált programozásban használt terminológiával szólva a createPoint fv egy konstruktor. Az osztályok fogalma itt nem jelenik meg explicite. Azok az objektumok tartoznak egy „osztályba", melyeket ugyanaz a konstruktor hozott létre.

8. A Clean modulrendszere

A Clean egy nagyon egyszerû, egyszintû (flat) modulrendszert tartalmaz, amely a MODULE-2 modulrendszeréhez hasonlít a leginkább. Minden modul két részbõl áll: a definíciós részből és az implementációs részből. A definíciós részben a függvények típusát, illetve az adattípusok definícióját adhatjuk meg. Ha egy adattípusnak csak a nevét adjuk meg a definíciós részben, akkor absztakt adattípust hoztunk létre, hiszen az adott típus belső felépítéséhez csak az implementációs részben megadott függvények férhetnek hozzá.

9. Típusosztályok

Az előzőkben megismerhettünk polimorfikus függvényeket, amelyeknek az volt a jellemzőjük, hogy sokféle különböző típusra használhatóak voltak. Ezeknek a függvényeknek az volt a korlátjuk, hogy minden típusra ugyanaz volt a definíciójuk. Mi a helyzet akkor, ha olyan függvényeket szeretnénk, amelyek különböző típusokra különbözőképpen működnek (un. ad-hoc polimorfizmus). Például szeretnénk egy show nevű függvényt definiálni, amely előállítja az argumentumként kapott érték sztring alakját. Hogyan lehetne ezt megoldani? Az egyik megoldás az, hogy különbözőképpen nevezzük el a függvényenket, pl. showInt, showBool, showList etc. Ez a megoldás egyrészt kényelmetlen, másrészt nem elég általános. Nem tudjuk pl. megírni a showList függvényt úgy, hogy az elemekre meghívjuk a megfelelő show függvényt, és összekonkatenáljuk az eredményt. Más megoldásra van szükség. Ez a megoldás az un. típusosztályok (type classes) használata.

Egy típusosztály nagyon hasonló a JAVA nyelv interfész fogalmához. A típusosztály nem más, mint függvénydefiníciók halmaza, ahol minden függvény ugyanazzal a típusváltozóval van paraméterezve. Például:

class Showable a where

show :: a -> String

A fenti Clean kód egy Showable nevű típusosztályt definiált, amelybe egy fv tartozik. Bármely Clean típus eleme lehet ennek a típusosztálynak azzal, hogy implementálja a típusosztály függvényeit. Ezt egy un. példánydeklarációval (instance declaration) adhatjuk meg:

instance Showable Bool where

show True = „True"

show False = „False"

instance Showable [a] where

show [] = „"

show [x:xs] = concat (show x) (show xs)

A listákon értelmezett show fv az elemtípus show fv-ét használja fel a sztring forma előállításához. A fordítóprogram feladata eldönteni, hogy a program egy adott pontján a show fv melyik példányát kell felhasználni.

Természetesen egy típusosztály egynél több függvényt is tartalmazhat. Így például definiálhatók algebrai struktúrákat leíró típusosztályok, ezek segítségével pedig általánosított műveletek, például mátrixszorzás stb. Persze ekkor a programozó dolga biztosítani, hogy a műveletekre valóban igazak legyenek az algebrai struktúra axiómái.

10. Unique típusok

A Clean nyelv legnagyobb újítása az un. uniqueness típusrendszer bevezetése. Ennek a típusrendszernek a célja kettõs:

Bevezetésként foglalkozzunk az interaktív programok funkcionális nyelvben való írásának nehézségeivel. A probléma gyökere a referenciális átlátszatóság, vagyis az, hogy egy függvény visszatérési értéke csak az argumentumainak értékétõl függhet. Egy interaktív programra ez sajnos nem igaz. Tekintsük például a getc fv-t, amely egy karakter olvas be a felhasználótól:

getc :: Char

Ennek a fv-nek nincs argumentuma, mégis minden kiértékelésekor más értéket ad vissza, tehát nem igaz rá a referenciális átláthatóság.

A fenti problémára többféle megoldás lehetséges:

getc :: World -> (Char, World)

Itt a World egy absztrakt adattípus, amely arra szolgál, hogy a világ állapotát szolgál. Természetesen ez az állapot nem tárolható, de arra alkalmas ez az eljárás, hogy a referenciális átláthatóságot visszacsempésszük a nyelvbe, hiszen a getc most már jól viselkedő Clean fv, amelynek a kimenete csak a bemenetétől függ.

Az állapotátmenetet használó rendszerek fő problémája az állapotok elágazása:

(getc w, getc w)

Mi az értéke ennek a kifejezésnek? A válasz az, hogy ez a kiértékelés sorrendjétől függ, azaz megint elvesztettük a funkcionális nyelvekre jellemző determinisztikusságot. A megoldást az jelentené, ha a világot megváltoztató függvények között sorrendet tudnánk felállítani, azaz pontosan meg tudnánk határozni melyik következik melyik után. Pontosan erre nyújt lehetőség a Clean uniqueness típusrendszere.

Az uniqueness típusrendszer úgy működik, hogy a függvények argumentumaihoz un. uniqueness attribútumokat rendelhetünk. Egy argumentum unique, ha a fv kiértékelése pillanatában a függvénynek kizárólagos hozzáférése van az argumentumhoz. A fenti példában a getc függvénynek nincs kizárólagos hozzáférése a w argumentumhoz, mert a másik getc fv is „használja" azt. Az, hogy a kizárólagos hozzáférés mit jelent, a Clean nyelv szemantikája alapján pontosan definiálni lehet.

A fentiek szerint a megoldás az, hogy az állapotátmenet függvényeinket úgy definiáljuk, hogy a világot jelképezõ argumentum és visszatérési érték unique attribútumot kapjon. Ezt Clean-ben a típusnév elõtt álló csillaggal jelöljük:

getc :: *Word -> (Char, *World)

Ha ezzel a definícióval probáljuk a (getc w, getc w) kifejezést lefordítani, a fordító típushibát jelez.

Állapotátmenet függvények használata esetén kényelmetlen lehet, hogy az egyre újabb világok jelölésére új változókat kell bevezetni:

getc2 w = ((c1, c2), w2)

where

(c1, w1) = getc w

(c2, w2) = getc w1

Ennek kiküszöbölésére a Clean bevezetett egy új szintaktikus formát, amely megjelenésében nagyon hasonlít az imperatív nyelvek szekvencia programkonstrukciójához:

getc2 w

# (c1, w) = getc w

# (c2, w) = getc w

= ((c1, c2), w)

Ez a nyelvi szerkezet majdnem ugyanaz, mint a where szerkezet, azzal a különbséggel, hogy az egyenlőségjelek bal oldalán található változók érvényességi köre csak az őket követő sorokra terjed ki (azaz a bal és jobb oldalon található w nem ugyanaz, ez nem értékadás, csak szintaktikus cukor (syntactic sugar) !)

 

 

 

 

 

 

 

 

 

A Clean-CORBA leképezés

Ahogy az elõzõ fejezetben említettük, ahhoz, hogy a CORBA szolgáltatásait egy adott programozási nyelvbõl elérhessük, szükségünk van egy nyelvi leképezésre (language mapping). Ebben a fejezetben egy ilyen nyelvi leképezést definiálunk a Clean programozási nyelvhez.

Egy nyelvi leképezésnek a következõket kell tartalmaznia:

A fentieknek megfelelõen ez a fejezet több alfejezetre bomlik, melyek a leképezés különbözõ részeit ismertetik. A száraz ismertetést példák bemutatásával igyekszünk érdekesebbé tenni, ezenkívül igyekszünk a leképezés mögött álló tervezési döntéseket ismertetni.

TODO: IDL Compiler

1. Az IDL fordító

Az IDL fileokból a megfelelõ Clean definíciók elõállítását az IDL fordítóprogram végzi. Ennek bemenete egy IDL file, kimenete pedig két Clean forrásfile. Az elsõ file a modul definíciós file, amely a modul által kiexportált típusok és függvények definícióját tartalmazza. A másik file az implementációs file, amely a függvények törzsét tartalmazza. Az IDL fordító használatára egy késõbbi szakaszban láthatunk példákat.

1. Az IDL modulrendszer

Az IDL file-okban található azonosítóknak ugyanaz az azonosító felel meg a Clean kódban is. Például, ha adott a következõ IDL deklaráció:

enum Color { Red, Green, Blue };

 

A fenti IDL típusnak a Clean-ban is egy Color nevû típus fog megfelelni.

Bonyoltabb a helyzet akkor, ha a definíció egy IDL modulban helyezkedik el, pl.

module Useless {

enum An_enum { First, Second, Third };

};

 

Mivel a Clean modulrendszere, ellentétben az IDL-ével, nem hierarchikus, ezért nem tehetjük meg, hogy az IDL moduloknak Clean modulokat feleltetünk meg. Ehelyett azt a módszert választjuk, amit a CORBA-C leképezés is követ, vagyis a Clean-beli azonosító nevében elhelyezzük az õt tartalmazó modul(ok) nevét, ezzel kerülve el az esetleges névütközéseket. A fenti példában tehát a Clean-beli típus neve Useless_An_enum lesz.

Sokszor elõfordul, hogy az IDL fordítónak automatikusan kell Clean azonosítókat generálnia. Ezek az azonosítók mindig két egymás melleti aláhúzás jelet (__) tartalmaznak, így kerülve el a névütközéseket az IDL fileokban használt azonosítókkal.

2. Az egyszerû típusok megfeleltetése

Az alábbi táblázat tartalmazza a beépített IDL típusokat, és a nekik megfelelõ Clean típusokat:

short

Int

unsigned short

Int

long

Int

unsigned long

Int

float

Real

double

Real

char

Char

wchar

Int

bool

Boolean

octet

Int

string

String

wstring

[ Int ]

 

Mivel a Clean-ben csak egyetlen egész típus van, ezért minden IDL-beli egész típusnak ezt feleltetjük meg. Ez problémát okozhat akkor, ha olyan értékeket adunk át az IDL-ben definiált mûveletek paramétereként, melyek Int típusúak ugyan, de kívül esnek az adott IDL egész típus értelmezési tartományán. Az implementácíó feladata, hogy ezeket a hibás értékeket kiszûrje. Hasonló a helyzet a lebegõpontos típusokkal.

Az IDL tartalmaz egy un. korlátozott (bounded) sztring típust is, ahol a programozó megszabhatja, hogy mennyi lehet az adott sztring maximális hossza. Ezt a mi leképezésünk (egyenlõre) nem támogatja.

Bár a leképezés támogatja a wchar és wstring típusokat, a Clean-ben nincs UNICODE támogatás, így a fenti típusok használata eléggé nehézkes.

A fenti táblázatból hiányzik néhány IDL típus, amelyeket a jelenlegi Clean leképezés nem támogat. Ezek a következõk:

Az IDL-nek a fentieken kívül van még két elõredefiniált típusa, az Any és a TypeCode, melyekrõl a következõ szakaszban lesz szó.

3. A TypeCode típus

Az Any típus leképezésének ismertetése elõtt szólnunk kell a TypeCode típusról. A TypeCode típus, ahogy a neve is mutatja, típusinformációt tárol, azaz segítségével az IDL típusokra vonatkozó információhoz futás közben is hozzáférhetünk.

Minden IDL típusnak van egy fajtája (kind), amely egyértelmûen meghatározza, milyen információt kell a TypeCode-nak tárolnia az adott típusról. Külön fajta tartozik minden beépített típushoz, és minden típuskonstrukcióhoz.

A CORBA szabvány a TypeCode típust is egy IDL definíció segítségével definiálja. Ez azzal az elõnnyel jár, hogy a nyelvi leképezéseknek nem kell külön megadniuk a TypeCode típus leképezését, mert az leképezés többi részébõl már következik. Hátránya ennek a megközelítésnek, hogy az így kapott típus használata egy kissé kényelmetlen lehet, mert nem használja ki a típus speciális jellemzõit.

A mi leképezésünkben a TypeCode típusnak a következõ algebrai típus felel meg:

:: TypeCode :== Tk_Short

, Tk_UShort

, Tk_Long

, Tk_ULong

, Tk_Float

, Tk_Double

, Tk_Char

, Tk_Bool

, Tk_TypeCode

, Tk_Any

, Tk_String Int // bound

, Tk_Struct String String [(String, TypeCode)] // name repoid [(name, type)]

, Tk_Sequence Int TypeCode // bound, member type

, Tk_Array Int TypeCode // nmembers, member type

etc.

A sztring és szekvencia típusoknál a ‘bound’ argumentum a sztring és a szekvencia maximális hosszát adja meg. Ha nincs korlátozás, akkor ez az érték 0.

Példaképpen tekintsük a következõ IDL típust:

struct APrettyBoringStruct {

short member1;

string member2;

sequence<long> member3

};

 

Ezt a típust a következõ TypeCode írja le:

Tk_Struct "APrettyBoringStruct" "IDL:APrettyBoringStruct:1.0" [

(„member1", Tk_Short),

(„member2", (Tk_String 0)),

(„member3", (Tk_Sequence 0 Tk_Long))

]

 

Azért, hogy a felhasználónak ne kelljen ezeket a bonyolult kifejezéseket magának leírnia, az IDL-CLEAN fordító minden, az IDL file-ban talált típushoz generál egy fv-t, amely az adott típus TypeCode-ját adja vissza. Például, a fenti típus TypeCode-ját a APrettyBoringStruct__tc függvény segítségével kaphatjuk meg.

4. Az Any típus

Ahogy az elõzõ fejezetekben elmondtuk, az Any típus bármely létezõ IDL típus értékét képes tárolni. Ennek megfelelõen a Clean nyelvre való leképezése is egy kicsit bonyolultabb, mint a többi típusé.

Az Any típusnak a következõ algebrai típus felel meg a Clean-ben:

// TODO:

:: CorbaAny := CorbaShort Int

, CorbaUShort Int

, CorbaLong Int

, CorbaULong Int

, CorbaChar Char

, CorbaBool Boolean

, CorbaString Int String

, CorbaSequence TypeCode Int [CorbaAny]

etc.

 

Ahogy látható, minden IDL típushoz, vagy típus konstrukcióhoz tartozik egy Clean adatkonstruktor. A konstruktor neve meghatározza a Any által tartalmazzott érték típusát. Ahol további típusinformációra van szükség (például a szekvencia típusnál tárolni kell a szekvencia elemeinek a típusát is), ott az adatkonstruktor paraméterei hordozzák ezt az információt.

Például, az elõzõ szakaszban definiált IDL struktúra egy példányát a következõ Clean kifejezés írja le:

CorbaStruct APrettyBoringStruct__tc [

(„member1", (CorbaLong 42)),

(„member2", (CorbaString 0 „foo")),

(„member3", (CorbaSequence CorbaLong 0 [(CorbaLong 1), (CorbaLong 2)]

]

 

Azért, hogy Any típusú értékek létrehozását egyszerûbbé és biztonságosabbá tegye, az IDL-CLEAN fordító minden típushoz generál két függvényt:

<T>__encode :: <T> -> Any

<T>__decode :: <T> *World -> (MayBe T, *World)

 

Az encode fv egy adott típusú értéket konvertál Any-vé, a decode fv pedig a megadott Any-ben tárolt értéket próbálja az adott típussá konvertálni. Ezeket a mûveleteket a CORBA szabvány beszúrásnak (insertion), illetve kivételnek (extraction) nevezi. A decode fv eredményében szereplõ MayBe típus definíciója a következõ:

:: MayBe a :== Just a | Nothing

 

Mint sejthetõ, a decode fv (Just result)-ot ad vissza, ha a dekódolás sikerült, és Nothing-ot, ha nem.

A decode fv *World argumentuma azt fejezi ki, hogy a dekódolás során esetleg a program környezetét érintõ mûveleteket is végre kell hajtani. Például, ha az Any egy objektum referenciát tartalmaz, és mi egy adott típusú objektum referenciát szeretnénk kiolvasni az Any-ból, akkor a rendszernek kapcsolatot kell teremtenie azzal a távoli rendszerrel, ahol az objektum van, hogy megtudja hogy az objektum az adott típusú-e.

5. Felsorolási típusok

Leképezésünk az IDL felsorolási típusainak (természetes módon) algebrai típusokat feleltet meg. Például:

enum Color { Red, Green, Blue };

 

Ennek az IDL típusnak a következõ Clean típus felel meg:

:: Color :== Color_Read | Color_Green | Color_Blue;

 

Az adatkonstruktorok nevébe azért került bele a típus neve, hogy elkerüljük a névütközéseket a más modulokban deklarált, hasonló nevû felsorolási értékekkel.

A használat megkönnyítése érdekében az IDL-CLEAN compiler minden felsorolási típushoz elkészíti az == és a toString függvények példányait, így a programozónak ezek már a rendelkezésére állnak.

6. Struktúrák (Direkt szorzat típusok)

Az IDL struktúráknak rekord típusok felelnek meg a Clean-ben. A rekord típus neve megegyezik a struktúra nevével, a rekord mezõnevei pedig a struktúra mezõinek a neveivel. Például:

struct Person {

string name;

long age;

string address;

};

 

Ennek az IDL struktúrának a következõ Clean rekord felel meg:

:: Person :== {

name :: String,

age :: Int,

address :: String

};

 

Ahogy az IDL ismertetésénél említettük, az IDL exception (kivétel) típusai nagyon hasonlóak a direkt szorzat típusokhoz. Ebbõl következik, hogy nyelvi leképezésük tökéletesen megegyezik azokéval.

7. Unió típusok

Az unió típus leképezésénél az a probléma merül fel, hogy nincs olyan Clean nyelvi elem, amely tökéletesen megfelelne az IDL nyelvi elemnek. A legközelebbi analógiát a Clean algebrai típuskontrukciói kínálják, de az algebrai típusoknál az adatkonstruktor neve dönti el, hogy a az adott változó éppen melyik értéket tartalmazza. Az IDL unió típusainál ezzel szemben a diszkrimináns értéke határozza meg ugyanezt. Az ellentmondást úgy oldjuk fel, hogy az diszkrimináns értékeibõl generáljuk az adatkonstruktorok neveit. Például:

union PrettyUseless switch (short) {

case 0:

short member1;

case 1:

case 2:

long member2;

default:

string member3;

};

 

Ennek az IDL definíciónak az alábbi Clean definíció felel meg:

:: PrettyUseless = PrettyUseless_0 CORBA_Short

| PrettyUseless_1 CORBA_Long

| PrettyUseless_2 CORBA_Long

| PrettyUseless__default CORBA_short CORBA_String

 

A default konstruktor azért rendelkezik két argumentummal, mert az unió értékén kívül tárolnia kell a diszkriminátor értékét is.

A fenti módszernek a fõ problémája, hogy nem támogatja azt az esetet, amikor a diszkriminátor típusa char. Ekkor ugyanis a karakter értékébõl csak elég nehézkesen lehet egy Clean-beli azonosítót generálni (mi legyen például a ‘\n’ karakter megfelelője?). Egy alternatív leképezés lehet az alábbi: az adatkonstruktorok nevét nem a diszkrimináns értékekből, hanem az unió mezőneveiből képezzük. Ekkor a fenti esethez hasonlóan néhány esetben tárolnunk kell a diszkriminátor értékét is. Az alternatív leképezésre példa:

:: PrettyUseless = PrettyUseless_member1 CORBA_Short

| PrettyUseless_member2 CORBA_Short CORBA_Long

| PrettyUseless_member3 CORBA_Short CORBA_String

 

8. Szekvencia és tömb típusok

A szekvencia típusok természetes módon képezhetõk le a Clean nyelvre: minden ilyen típusnak egy Clean lista felel meg. Hasonló a helyzet az IDL tömb típusival is. Egy példa:

typedef sequence<long> ASequence;

 

Ennek a következő Clean deklaráció felel meg:

:: ASequence = [CORBA_Long]

 

Érdekesebb a helyzet akkor, ha a szekvencia elemtípusa egy un. anonym típus, tehát egy olyan típus, melynek nincsen neve. Például:

typedef sequence <sequence<long> > AnotherSequence;

 

Milyen típusnév szerepeljen ekkor a Clean deklarációban? A válasz az, hogy az IDL fordítóprogram automatikusan generál nevet az ilyen típusoknak, ahogy az az alábbiakban látható:

:: AnotherSequence__member_type = [CORBA_Long]

:: AnotherSequence = [AnotherSequence__member_type]

 

Ahogy a fenti példában látható, az anonym típus neve az őt „tartalmazó’” típus nevéből képződik.

10. Konstansok

A funkcionális nyelvekben a konstansok szerepét argumentum nélküli függvények töltik be. Ez az alapja az IDL konstansok Clean-be való leképezésének is:

const long THE_ANSWER = 42;

const float PI = 3.14159;

 

A megfelelő Clean deklarációk:

THE_ANSWER :: CORBA_Long

THE_ANSWER = 42

PI :: CORBA_Float

PI = 3.14159

 

11. Interfész típusok

Ebben a szakaszban az IDL interface típusainak leképezését fogjuk ismertetni. Ellentétben a leképezés többi részével, az interfészek és mûveleteik leképezése más a kliens és a szerver oldalon. A kliens oldali programozó az interfész használatához akar segítséget kapni, míg a szerver oldali programozó az interfész mûveleteinek megvalósításához. Ez a szakasz a kliens oldali leképezést ismerteti, a szerver oldalra késõbb térünk ki.

A leképezés minden interfész típusnak egy átlátszatlan (opaque) típust feleltet meg. Ezek olyan típusok, melyeket belsõ struktúrája a programozó számára hozzáférhetetlen, és csak elõre adott függvényeken keresztül lehet elérni õket.

Példaképpen tekintsük az egyik elõzõ fejezetben leírt Bank interfészt. Az ehhez tartozó Clean definíció a következõ:

:: Bank

 

Mint látható, a modul definíciós file-jában csak a típus nevét adtuk meg, a teljes definíció az implementációs file-ban található.

Az IDL fordító minden interfész típushoz generál egy <INTERFACE NAME>__nil nevű függvényt, amely egy adott típusú üres objektum referenciát hoz létre.

A programozónak gyakran van szüksége arra, hogy az egymással öröklési kapcsolatban lévő objektum referenciák között konvertáljon. A konverziónak két fajtája van: a szülő osztályra való konverzió (widening) és a leszármazott osztályra való konverzió (narrowing). Példaképpen tekintsük az alábbi két interfészt:

interface A {

};

interface B : A {

};

 

Ha adott egy B típusú objektum referenciánk, akkor az öröklődés tulajdonságai miatt ezt tekinthetjük A típusú referenciának is. Objektum orientált nyelvekben, mint például a C++, ez a folyamat automatikus, a mi leképezésünkben viszont a programozónak explicite jeleznie kell a konverziót. Ezt az IDL fordító által generált widen függvények segítségével teheti meg. Például, ha adott egy Bank típusú referenciánk, ezt a következő kifejezés segítségével alakíthatjuk át Object típusúra:

(CORBA_Object__widen ref)

 

A fordított esetben tegyük fel, hogy adott egy A típusú referenciánk. Ha tudjuk, hogy az általa mutatott CORBA objektum B típusú, akkor jogunk van ezt a referenciát B típusúra konvertálni. Ezt a narrow függvények segítségével tehetjük meg. Példánkban az IDL fordító a következő függvényt generálja:

B__narrow :: A *World -> (MayBe B, *World)

 

A *World típusú argumentum azért szükséges, mert annak eldöntéséhez, hogy a mi objektum referenciánk tényleg B típusú-e, esetleg kapcsolatot kell teremteni a távoli rendszerrel, ahol az objektum van. A MayBe típus használata pedig azt az előbb már kifejtett gondolatot fejezi ki, hogy egy A típusú objektum nem feltétlenül B típusú.

12. Műveletek

Az IDL fordító minden, az IDL interfészben szereplõ mûvelethez egy Clean függvényt generál, amelynek kiértékelésével hívhatja meg a programozó a CORBA objektum megfelelõ mûveletét. Például, a már említett Account interfészhez az alábbi függvényeket generálja a fordító:

Account_deposit :: Account CORBA_Long *World

-> ((ResultOrException CORBA_Void CorbaException), *World)

Account_withdraw :: Account CORBA_Long *World

-> ((ResultOrException CORBA_Void CorbaException), *World)

Account_balance :: Account *World

-> ((ResultOrException CORBA_Long CorbaException), *World)

 

Ahogy látható, a fenti függvényeknek két „kötelező” paramétere van: Az Account típusú paraméter a célobjektumot azonosítja, azaz azt a CORBA objektumot, amelynek a műveletét meg szeretnénk hívni. A *World paraméter azt fejezi ki, hogy ezek a függvények potenciálisan változásokat hozhatnak létre a funkcionális programot körülvevő világon.

A függvények visszatérési értéke egy pár, amelynek első tagja a valódi visszatérési értéket tartalmazza, vagy pedig a kérés végrehajtási során fellépő kivételt adja meg. Erre azért van szükség, mert a Clean nyelv nem támogatja a kivételkezelést.

Ha a műveletnek vannak out vagy inout típusú paraméterei, akkor a Clean függvény ezek hívás utáni értékét is visszaadja, például:

void balance2(out long balance);

 

Ehhez a művelethez a következő Clean fv generálódik:

Account_balance2 :: Account *World

-> (((ResultOrException (CORBA_Void, CORBA_Long) CorbaException), *World))

 

Minden, az IDL interfészben definiált attribútumhoz két függvény generálódik: egy az attribútum lekérdezésére, és egy a beállítására. Például:

attribute long balance3;

Ehhez az attribútumhoz a következő két Clean fv generálódik:

Account__get_balance3 :: Account *World

-> ((ResultOrException CORBA_Long CorbaException), *World)

Account__set_balance3 :: Account CORBA_Long *World

-> ((ResultOrException CORBA_Void CorbaException), *World)

13. A MICO Binder szolgáltatása

Mielõtt a példaprogramok tárgyalásába kezdenénk, szót kell ejtenünk a MICO Binder szolgáltatásáról. A MICO Binder a CORBA Naming Service szolgáltatás egyszerûsített változatának tekinthetõ. Segítségével a kliens programok könnyebben hozzájuthatnak a szerver objektumok referenciáihoz. Mûködése a következõ: amikor egy CORBA szerver folyamat elindul, elkezd figyelni a Binder-től érkező kérésekre egy megadott TCP porton. A kliens program a CORBA::ORB::bind() metódus meghívásával hozzájuthat a szerver folyamat által megvalósított objektumok referenciáihoz. A bind() metódusnak két paramétere van: az első egy IP cím, TCP port páros, a második pedig egy un. repository ID. A repository ID nem más, mint egy CORBA interfész típus teljes neve egy speciális formátumban. Például, az előbb tárgyalt Account interfész repository ID-ja „idl:Account:1.0”. A bind metódus hozzákapcsolódik az adott IP címen és TCP porton figyelő szerver folyamathoz, és kérést intéz hozzá, hogy adjon vissza egy megadott típusú objektumra mutató referenciát. Ha a szerver folyamat megvalósít ilyen típusú objektumot, akkor visszaad egy referenciát, egyébként hiba lép fel.

14. Példák

Ennyi száraz elmélet után ideje, hogy gyakorlati példákkal is szolgáljunk. Ebben a szakaszban két példát ismertetünk: az egyik a klasszikus „Hello, World!" program CORBA stílusban, a másik pedig a már korábban ismertetett Account interfész használatát bemutató program. Mivel a szerver oldali leképezést még nem ismertettük, ezért feltesszük, hogy a szerver programok már korábban megírásra kerültek egy másik programozási nyelvben (például C++-ban).

14.1 A Hello, World! példaprogram

Az elosztott alkalmazásunk készítéséhez először is definiálnunk kell a kliens és a szerver közti felületet:

interface Hello {

void hello_world();

};

 

A fenti definíciókat tároljuk el egy hello.idl nevű fileban. Ahhoz, hogy ezt az interfészt Clean-ből használni tudjuk, le kell futtatnunk a Clean IDL fordítót:

bash$ idl2clean hello.idl

A fordító két file-t hoz létre, amelyek a Hello interfészhez tartozó Clean definíciókat tárolják.

Most már készen állunk a kliens program megírására:

1 module hello_client

2

3 import StdEnv

4

5 import hello

6

7 Start w

8 # (orb, _, w) = CORBA_ORB_init [] w

9 # (Just obj, w) = CORBA_ORB_bind orb "IDL:Hello:1.0" "inet:localhost:4242" w

10 # (Just obj, w) = Hello__narrow obj w

11 = Hello_hello_world obj w

 

A program a következőképpen működik:

A fenti programot a Clean fejlesztő környezet segítségével fordíthatjuk le. A kipróbálásához először el kell indítanunk a szerver folyamatot:

bash$ ./hello_server -ORBIIOPAddr inet:localhost:4242 &

 

A -ORBIIOPAddr argumentum azt határozza meg, hogy a szerver folyamat melyik IP címen és melyik TCP porton figyeljen a MICO Binder-től érkező kérésekre.

A szerver indítása után elindíthatjuk a kliens folyamatot is:

bash$ ./hello_client

 

A futás eredménye remélhetőleg egy „Hello, World!” üzenet lesz.

14.2 Az Account példaprogram

Az Account interfészt már korábban ismertettük, most csak a kliens programot fogjuk bemutatni:

module account_client

import StdEnv

import account

Start w

# (orb, _, w) = CORBA_ORB_init ["xxx"] w

# (Just obj, w) = CORBA_ORB_bind orb "IDL:Account:1.0" "inet:localhost:4242" w

# (Just obj, w) = Account__narrow obj w

# (Result b1, w) = Account_balance obj w

# (_, w) = Account_deposit obj 20 w

# (Result b2, w) = Account_balance obj w

# (_, w) = Account_withdraw obj 10 w

# (Result b3, w) = Account_balance obj w

= ("\n\nInitial balance: " +++ (toString b1) +++ "\nBalance after deposit: " +++ (toString b2) +++ "\nBalance after withdraw: " +++ (toString b3) +++ "\n\n", w)

 

A program szerkezete hasonló az elõzõ példaprograméhoz. Elõször inicializáljuk a CORBA rendszert, majd megszerezzük az Account objektum egy referenciáját. Ezután különbözõ kéréseket intézünk az objektumhoz, végül kiiratjuk az objektum végsõ állapotát, azaz a számla egyenlegét.

15. A Dynamic Invocation Interface

Ahogy egy előző fejezetben említettük, a Dynamic Invocation Interface (DII) célja, hogy a programok anélkül tudjanak CORBA kéréseket elküldeni, és az eredményt feldolgozni, hogy fordítási időben tudniuk kellene a használni kívánt IDL műveletek nevét, paramétereik típusát stb. Ebben a szakaszban a DII Clean nyelvre való leképezését ismertetjük.

Leképezésünk nagyon egyszerű: A DII használatát egyetlen függvény, a corba_invoke valósítja meg. Ennek definíciója az alábbi:

corba_invoke :: CORBA_Object String [CorbaArg] TypeCode [TypeCode] *World -> (Any, CorbaException, [CorbaArg], *World)

 

A paraméterek jelentése a következõ:

:: CorbaArg = InArg String Any

| OutArg String Any

| InOutArg String Any

| InArgTemplate String TypeCode

| OutArgTemplate String TypeCode

| InOutArgTemplate String TypeCode

 

Minden paraméterátadási módhoz két adatkonstruktor tartozik. Az elsõnél meg kell adni a paraméter nevét és értékét, a másodiknál pedig a nevét és a típusát. A második formára out típusú paraméterek esetén van szükség, mivel ilyenkor a paraméter csak a szerver oldalon kap értéket.

Felmerülhet az olvasóban, hogy miért van szükség ennyi információ megadására, miért kell például a visszatérési érték típusát is megadni. A válasz az, hogy a CORBA alapjául szolgáló hálózati protokoll, az IIOP, nem hordoz típusinformációt, így a CORBA szoftver nem tudná a felhasználói program segítsége nélkül dekódolni a vonalon érkezõ információt.

A corba_invoke fv értéke egy rendezett négyes, melynek elemei a következő jelentést hordozzák:

Végezetül lássunk egy példát a DII használatára. Tegyük fel, hogy az Account interfész deposit nevû mûveletét szeretnénk meghívni. Ezt a következõképpen tehetjük meg:

corba_invoke ref „deposit" args CorbaVoid [] w

where

args = [(InArg „amount" (CorbaLong 10))]

 

16. A szerver oldali leképezés

16.1 A szerver program

Az elõzõ szakaszokban a CORBA kliens oldali aspektusait tárgyaltuk, vagyis azt, hogy egy kliens program hogyan képes CORBA objektumoknak kéréseket küldeni, és ezek eredményét feldolgozni. A szerver oldali leképezés ezzel szemben azt definiálja, hogy egy szerver program hogyan valósíthat meg CORBA objektumokat, és hogyan szolgálhatja ki az objektumoknak érkezõ kéréseket.

A funkcionális programmozás eszközeivel egy CORBA szerver folyamat egy állapotátmenet rendszernek (state transition system) tekinthetõ. A rendszer események hatására lép át egyik állapotból a másikba. A mi esetünkben az események a CORBA objektumokhoz érkezõ kérések. A rendszer állapota három tényezõbõl tevõdik össze:

A Clean rendszer tartalmaz egy Object IO-nak nevezett keretrendszert (framework), amellyel eseményvezérelt alkalmazások valósíthatóak meg. Sajnos az Object IO újabb verziói csak Windows platformon érhetõek el, míg ez a dolgozat UNIX rendszereken készült. Így magát a keretrendszert nem tudjuk felhasználni, de felhasználjuk a keretrendszer által nyújtott modellt (design reuse).

A rendszer állapotát a PSt (program state) típus tárolja. Ennek definíciója a következõ:

:: PSt l

= { ls :: l

, io :: IOSt l

}

 

Mint látható, a program állapota két részbõl áll. Az ls a local state, ezt a programozó adhatja meg. Az io az IO state, ebben tárolódik az IO rendszer állapota. A programozó az IO állapothoz csak függvényeken keresztül férhet hozzá. Ezeket a függvényeket késõbb részletesen is ismertetni fogjuk.

A szerver folyamatot a CORBA_Server_run függvény meghívásával futtathatjuk. Ennek definíciója:

CORBA_Server_run :: CORBA_ORB l (ServerInitFunc l *World) *World -> *World

 

Az elsõ paraméter az ORB-t reprezentáló objektum, a második pedig a program kiinduló állapota. Ha a programnak nincs lokális állapota (például késõbbi példaprogramjainknak sincs), akkor ide a Void értéket írhatjuk.

A harmadik paraméter az inicializáló függvény, melynek feladata a program kezdeti állapotának létrehozása. Ebben a függvényben hozhatjuk létre a program CORBA objektumait.

16.2 CORBA objektumok

Ahogy egy elõzõ fejezetben említettük, a CORBA objektumokat servant-oknak nevezett programozási nyelvbeli objektumok valósítják meg. A mi esetünkben a servant-ok Clean rekordok lesznek. A rekordok kétféle komponenst tartalmaznak:

Például, a már sokszor említett Account típushoz tartozó servant objektumokat az alábbi típus írja le:

:: Account__servant l = E.a: {

ls :: a,

impl_deposit :: (a, (PSt l)) CORBA_Long *World

-> ((a, (PSt l)), (ResultOrException CORBA_Void CorbaException), *World),

impl_withdraw :: (a, (PSt l)) CORBA_Long *World

-> ((a, (PSt l)), (ResultOrException CORBA_Void CorbaException), *World),

impl_balance :: (a, (PSt l)) *World

-> ((a, (PSt l)), (ResultOrException CORBA_Long CorbaException), *World)

};

 

A fenti definíció nem kevés magyarázatra szorul. Elsőként jegyezzük meg, hogy az Account_servant típus két típusváltozóval van paraméterezve. Az első változó a program lokális állapotának a típusa. Az a nevű változó a már említett egzisztenciális típusváltozó. Erre azért van szükség, hogy egy IDL interfésznek többféle implementációját is megadhassa a programozó.

A rekord típus ls nevű mezője tartalmazza az objektum lokális állapotát. A többi függvény az egyes IDL műveleteket implementálja. A függvények paraméterlistájában és visszatérési értékében szereplő (a, (PSt l)) pár azt jelzi, hogy ezek a függvények un. állapotátmenet függvények, azaz változásokat okozhatnak mind az objektum lokális állapotában, mint a globális programállapotban.

A fenti definíciót az IDL fordító generálja, a programozóra hárul a feladat, hogy a függvények definícióját megadja. Ezt úgy teheti meg, hogy létrehozza a fenti rekord típus egy példányát. Egy megfelelő definíció az alábbi:

account_servant = { Account__servant |

ls = 0,

impl_deposit = my_deposit,

impl_withdraw = my_withdraw,

impl_balance = my_balance

}

where

my_deposit (ls, ps) what w

= ((ls + what, ps), Result Void, w)

my_withdraw (ls, ps) what w

= ((ls - what, ps), Result Void, w)

my_balance (ls, ps) w

= ((ls, ps), Result ls, w)

 

Ahogy fent látható, az objektum lokális állapota nem más, mint a számla egyenlege. Az objektum mûveletei ezt állapotot kérdezik le, illetve változtatják meg.

Ahhoz, hogy egy objektum a külvilág számára is látható legyen, be kell jegyeztetni az ORB-nél. Ezt esetünkben az Account_servant_open fv végzi, melynek definíciója:

Account__servant_open :: (PSt l) (Account__servant l) *World -> (CORBA_Object, (PSt l), *World)

 

A függvény bemenete a program állapota és az általunk megadott servant objektum. A fv bejegyezteti a servant-ot az IO rendszerbe, így amikor egy kérés érkezik a servant által reprezentált CORBA objektumhoz, a rendszer az általunk megadott függvényeket fogja meghívni a kérés kiszolgálásához.

Elérkezett az ideje, hogy a teljes Account szerver programot bemutassuk:

module account_server

import StdEnv

import account

Start w

# (orb,_,w) = CORBA_ORB_init [] w

= CORBA_Server_run orb Void ServerInit w

where

ServerInit ps w

# (obj, ps, w) = Account__servant_open ps account_servant w

= (ps, w)

account_servant = ....

 

A fenti programban semmi új az eddigiekhez képest. Inicializáljuk az ORB-t, majd elindítjuk az eseménykezelő rendszert. A ServerInit függvényben létrehozunk egy servant-ot, és bejegyeztetjük az ORB-vel. Az eseménykezelő rendszer gondoskodik róla, hogy a kliensektől érkezett kérések eljussanak az általunk létrehozott servant objektumokhoz.

 

A leképezés implementációja

Ahhoz, hogy egy Clean programozó a CORBA szolgáltatásait élvezhesse, két dologra van szüksége:

Ebben a fejezetben ennek a két szoftverkomponensnek az implementációjáról lesz szó.

1 A Clean-C interfész

Ahhoz, hogy egy funkcionális programozási nyelv széles körben elterjedjen, szükség van egy olyan eszközre, amelyel a más programozási nyelvekkel készült szoftverkönyvtárakat el lehet érni. Ezt az eszközt Idegen Nyelvi Interfésznek (Foreign Language Interface-FFI) nevezik. Mint sok más funkcionális nyelv, a Clean is rendelkezik egy FFI eszközzel. Ezt az eszközt fogjuk a továbbiakban ismertetni.

A Clean fordító a programokat egy absztrakt gépnek szóló utasításokra, az un. ABC-kódra fordítja le. Ennek a gépnek van egy ccall nevű utasítása, amelynek segítségével C-ben írt függvények hívhatóak meg. A Clean fordító arra is lehetőséget ad, hogy egy Clean függvény törzsét ezen az ABC nyelven adjuk meg. Ez hasonló a más nyelvekben ismert inline assembly lehetőséghez.

Egy példa egy C függvény meghívására:

:: IntArrayPtr :== Int

:: Env :== Int

array_create :: !Int !*Env -> (!IntArrayPtr, !*Env)

array_create size env = code {

ccall array_create "I:I:I"

}

 

A fenti kód egy array_create nevű Clean függvényt definiál, amely a hasonló nevű C függvényt hívja meg. A ccall utasítás első argumentuma a meghívandó C függvény neve, a második argumentum pedig a paraméterátadást szabályozza. A második argumentum három részből áll, amelyek kettősponttal vannak elválasztva. A három rész a következő:

Az átvihető paraméterek típusa erősen korlátozott. A következő típusok a megengedettek:

Lehetőség van pointerek használatára is, ezeket a Clean-ben Int típusként kezelhetjük.

A paraméterek típusa elött látható felkiáltójel azt jelzi, hogy a függvény szigorú (strict) az adott paraméterben. Ez azt jelenti, hogy a függvény értéke nem határozható meg az adott paraméter kiértékelése nélkül. Mivel a paraméterek értékét mi egy C függvénynek szeretnénk átadni, ezért minden paraméternek strict-nek kell lennie.

A fent bemutatott módszer fő problémája az, hogy nem elég biztonságos. A Clean fordító nem tudja ellenőrizni, hogy a C függvénynek valóban annyi paramétere van, amennyit a programozó megadott, a paraméterek típusa egyezik-e stb. Másrészről a fenti kód megírása elég fárasztó és monoton munka. Ezeknek a problémáknak a megoldására a Clean fejlesztőrendszerhez tartozik egy htoclean nevű szoftver, amely a fenti interfész kód megírását nagyban leegyszerűsíti. A htoclean bemenete egy C header file, amely C függvény deklarációkat tartalmaz. A program ezek alapján legenerálja a megfelelő Clean forrásfileokat. Annak, hogy ehhez a projektünkhez mégsem a htoclean-t használtuk, az az oka, hogy a szoftver meglehetősen primitív: csak nagyon egyszerű szerkezetű header fileokat tud feldolgozni, nem képes kezelni a felhasználó által definiált típusokat stb.

Ahogy az előzőkben említettük, a Clean-C interfész meglehetősen primitív, ami jelentősen megnehezítette a szoftverünk elkészítését. A főbb problémák a következők:

A fenti problémák jelentős részét megoldaná, ha a Clean valamelyik más funkcionális nyelv külső interfészét használná, például a Haskell-hez készült Green Card-ot.

  1. A Clean – CORBA interfész

Ennek a Clean modulnak a segítségével érheti el a programozó az ORB szolgáltatásait. Ez a modul valósítja meg az elözö fejezetben ismertetett Any és TypeCode típusokat, a CORBA_ORB_Init, CORBA_Server_run függvényeket stb.

A modul több egymásra épülő rétegből áll, melyet az alábbi ábra szemléltet:

<TODO>

A legalsó réteg maga az ORB szoftver, amely egy C++ felületen keresztül érhető el. Erre épül egy modul, amely egy C nyelvi felületet nyújt. Erre a rétegre azért van szükség, mert az előző szakaszban ismertetett Clean-C interfészen keresztül csak C hívási felületű függvények érhetőek el, és az átvihető paraméterek típusa is erősen korlátozott. A C nyelvű rétegre épül a következő réteg, amely a C függvényeket használó Clean függvényeket tartalmaz. Ez a réteg eléggé primitív szolgáltatásokat nyújt, ezért a Clean programozó számára nem elérhető.

A legfelső réteg az, amit a Clean programozó használni fog. Ez a réteg definiálja az előző fejezetben megismert adattípusokat és függvényeket. A legfelső réteg által nyújtott programozási felület teljes definíciója az A mellékletben TODO található meg.

3. Az IDL fordítóprogram

Az IDL fordítóprogram célja, hogy a felhasználó által megadott IDL definíciókból a nyelvi leképezés alapján kódot generáljon. Minden IDL fordító támogat egy vagy több célnyelvet (target language), amelyekhez képes kódot generálni. A mi célunk olyan IDL fordító előállítása, amely a Clean nyelvet támogatja.

Egy IDL fordítóprogram két részből áll:

3.1 A front end

A front end megvalósítására többféle lehetőség kínálkozik:

Ennek a módszernek a legnagyobb hátránya, hogy sok munkával jár, és nem hasznosíthatjuk a már meglévő IDL fordítók programkódját (nincs újrafelhasználás – reuse).

Az IR azért lehet segítségünkre a front end megírásához, mert némely IDL fordító, köztük a MICO IDL fordítója is képes rá, hogy az általa beolvasott IDL fileokból ne kódot generáljon, hanem tartalmukat töltse egy Interface Repository-ba. Ezután a mi IDL fordítónknak már nincs más feladata, mint hogy az IR tartalmát szabványos CORBA eszközökkel kiolvassa, és ebből egy olyan közbülső reprezentációt generáljon, amelyet majd a back end használni fog. Ezt a módszert alkalmazza a tclMico TCL-CORBA (TODO) interfész szoftver is.

A három lehetőség közül mi a harmadikat választottuk az alábbi szempontok miatt:

A fentiek alapján az mi IDL fordítónk a következő lépéseket hajtja végre:

Felmerülhet a kérdés, hogy az IR tartalmát kiolvasó Clean kód hogyan működik. Nos, ez a kód nem más, mint amit a mi IDL fordítónk generálna akkor, ha inputként az Interface Repository felületét megadó IDL fileot adnánk meg neki. A kód első verziója természetesen még kézzel készült, mert ekkor az IDL fordító még nem létezett. Ahogy a fordító működni kezdett, a kód újra lett generálva és a fordító következő verziója már az előző verzió által generált kódot használta. A fordítóprogramok világában ezt a folyamatot bootstrapping-nak nevezik. A kód kézzel való megírása jó alkalom volt a Clean-IDL leképezés a gyakorlatban való kipróbálására is.

3.2 A back end

A back end feladata a kódgenerálás a front end által előállított belső reprezentáció alapján. A belső reprezentáció esetünkben nem más, mint egy Clean algebrai adattípus, mely az IDL fileokban található információt tárolja:

: IRObject = IRRepository [IRObject]

| IRModule CORBA_ModuleDescription [IRObject]

| IRConstant CORBA_ConstantDescription

| IRStruct CORBA_TypeDescription [IRObject]

| IRException CORBA_ExceptionDescription

| IRUnion CORBA_TypeDescription [IRObject]

| IREnum CORBA_TypeDescription

| IRAlias CORBA_TypeDescription

| IRInterface CORBA_InterfaceDescription [IRObject]

| IROperation CORBA_OperationDescription

| IRAttribute CORBA_AttributeDescription

 

A fent látható CORBA_… típusok az Interface Repository által használt IDL struktúrák, amelyek a különböző IDL objektumok információit tartalmazzák. A kódgenerálás a fenti IRObject típusú struktúrának a rekurzív bejárásával történik. A struktúrát valójában kétszer kell bejárni: egyszer a Clean definíciós modul, egyszer pedig az implementációs modul elkészítéséhez. Az IDL fordító körülbelül 1000 sor Clean kód. Összehasonlításképpen, a MICO IDL fordító C++ kódgenerátora körülbelül 10000 sornyi C++ kód. A különbség magyarázható egyrészt a Clean-IDL leképezés egyszerűségével, másrészt a funkcionális programozási nyelvek nagy kifejezőerejével.

 

 

A Clean-CORBA interfész

definition module Corba

import StdEnv, StdDebug

from StdString import String;

////////////////////////////////

// UTILITIES

////////////////////////////////

:: ResultOrException a b = Result a | Exception b

// If there was an exception, the result is CorbaVoid

// Luckily, lazy evaluation prevents the system from trying to decode

// it into another CORBA type...

corba_result_create :: .a CorbaException -> ResultOrException .a CorbaException

:: MayBe a = Just a | Nothing

just :: (MayBe u:a) -> u:a

is_just :: (MayBe .a) -> Bool

toStr :: !.a -> String

map_with_env :: (a *env -> (b, *env)) [a] *env -> ([b], *env)

map_maybe_with_env :: (a *env -> (MayBe b, *env)) [a] *env -> (MayBe [b], *env)

////////////////////////////////

// TYPECODE

////////////////////////////////

:: TypeCode = Tk_Null |

Tk_Void |

Tk_Short |

Tk_Long |

Tk_UShort |

Tk_ULong |

Tk_Float |

Tk_Double |

Tk_Boolean |

Tk_Char |

Tk_Octet |

Tk_Any |

Tk_String Int |

Tk_TypeCode |

Tk_ObjRef String String |

Tk_Struct String String [(String, TypeCode)] |

Tk_Sequence Int TypeCode |

Tk_Union String String TypeCode [(String, Any, TypeCode)] |

Tk_Enum String String [String] |

Tk_Alias String String TypeCode |

Tk_Array Int TypeCode |

Tk_Exception String String [(String, TypeCode)] |

Tk_Recursive String

instance == TypeCode

////////////////////////////////

// CORBA::Any

////////////////////////////////

:: Any = CorbaNull |

CorbaVoid |

CorbaShort Int |

CorbaLong Int |

CorbaUShort Int |

CorbaULong Int |

CorbaFloat Real |

CorbaDouble Real |

CorbaBoolean Bool |

CorbaChar Char |

CorbaOctet Int |

CorbaString String |

CorbaAny Any |

CorbaTypeCode TypeCode |

CorbaObjRef CORBA_Object |

CorbaStruct TypeCode [(String, Any)] | // tc [(name, type)]

CorbaSequence TypeCode [Any] | // tc [members]

CorbaUnion TypeCode Any Any | // tc discriminator value

CorbaEnum TypeCode String | // tc value

CorbaArray TypeCode [Any] | // tc [members]

CorbaException TypeCode [(String, Any)]

//

// If there is no active element in a CorbaUnion, the second argument

// is CorbaNull.

//

////////////////////////////////

// EXCEPTION

////////////////////////////////

:: CompletionStatus = Completed_Yes | Completed_No | Completed_Maybe

:: CorbaException = NoException |

SystemException String CompletionStatus Int |

UserException String Any

corba_exception_encode :: CorbaException -> Any

////////////////////////////////

// NAMEDVALUE

////////////////////////////////

// The xxxTemplate constructors are used to give the name and type of

// arguments, without specifying a value

:: CorbaArg = InArg String Any

| OutArg String Any

| InOutArg String Any

| InArgTemplate String TypeCode

| OutArgTemplate String TypeCode

| InOutArgTemplate String TypeCode

corba_arg_get_value :: CorbaArg -> Any

////////////////////////////////

// REQUEST

////////////////////////////////

corba_invoke :: CORBA_Object String [CorbaArg] TypeCode [TypeCode] *World -> (Any, CorbaException, [CorbaArg], *World)

////////////////////////////////

// CORBA::Void

////////////////////////////////

// There is no void type in Clean

:: CORBA_Void :== Void

CORBA_Void__tc :: TypeCode

CORBA_Void__decode :: Any *World -> (MayBe CORBA_Void, *World)

CORBA_Void__encode :: CORBA_Void -> Any

////////////////////////////////

// CORBA::Short

////////////////////////////////

:: CORBA_Short :== Int

CORBA_Short__tc :: TypeCode

CORBA_Short__decode :: Any *World -> (MayBe CORBA_Short, *World)

CORBA_Short__encode :: CORBA_Short -> Any

////////////////////////////////

// CORBA::Long

////////////////////////////////

:: CORBA_Long :== Int

CORBA_Long__tc :: TypeCode

CORBA_Long__decode :: Any *World -> (MayBe CORBA_Long, *World)

CORBA_Long__encode :: CORBA_Long -> Any

////////////////////////////////

// CORBA::UShort

////////////////////////////////

:: CORBA_UShort :== Int

CORBA_UShort__tc :: TypeCode

CORBA_UShort__decode :: Any *World -> (MayBe CORBA_UShort, *World)

CORBA_UShort__encode :: CORBA_UShort -> Any

////////////////////////////////

// CORBA::ULong

////////////////////////////////

:: CORBA_ULong :== Int

CORBA_ULong__tc :: TypeCode

CORBA_ULong__decode :: Any *World -> (MayBe CORBA_ULong, *World)

CORBA_ULong__encode :: CORBA_ULong -> Any

////////////////////////////////

// CORBA::Float

////////////////////////////////

:: CORBA_Float :== Real

CORBA_Float__tc :: TypeCode

CORBA_Float__decode :: Any *World -> (MayBe CORBA_Float, *World)

CORBA_Float__encode :: CORBA_Float -> Any

////////////////////////////////

// CORBA::Double

////////////////////////////////

:: CORBA_Double :== Real

CORBA_Double__tc :: TypeCode

CORBA_Double__decode :: Any *World -> (MayBe CORBA_Double, *World)

CORBA_Double__encode :: CORBA_Double -> Any

////////////////////////////////

// CORBA::Boolean

////////////////////////////////

:: CORBA_Boolean :== Bool

CORBA_Boolean__tc :: TypeCode

CORBA_Boolean__decode :: Any *World -> (MayBe CORBA_Boolean, *World)

CORBA_Boolean__encode :: CORBA_Boolean -> Any

////////////////////////////////

// CORBA::Char

////////////////////////////////

:: CORBA_Char :== Char

CORBA_Char__tc :: TypeCode

CORBA_Char__decode :: Any *World -> (MayBe CORBA_Char, *World)

CORBA_Char__encode :: CORBA_Char -> Any

////////////////////////////////

// CORBA::Octet

////////////////////////////////

:: CORBA_Octet :== Int

CORBA_Octet__tc :: TypeCode

CORBA_Octet__decode :: Any *World -> (MayBe CORBA_Octet, *World)

CORBA_Octet__encode :: CORBA_Octet -> Any

////////////////////////////////

// CORBA::Any

////////////////////////////////

:: CORBA_Any :== Any

CORBA_Any__tc :: TypeCode

CORBA_Any__decode :: Any *World -> (MayBe CORBA_Any, *World)

CORBA_Any__encode :: Any -> Any

////////////////////////////////

// CORBA::TypeCode

////////////////////////////////

:: CORBA_TypeCode :== TypeCode

CORBA_TypeCode__tc :: TypeCode

CORBA_TypeCode__decode :: Any *World -> (MayBe CORBA_TypeCode, *World)

CORBA_TypeCode__encode :: TypeCode -> Any

////////////////////////////////

// CORBA::String

////////////////////////////////

// This is the unbounded version

:: CORBA_String :== String

CORBA_String__tc :: TypeCode

CORBA_String__decode :: Any *World -> (MayBe CORBA_String, *World)

CORBA_String__encode :: CORBA_String -> Any

////////////////////////////////

// CORBA::Object

////////////////////////////////

//

// The object references are not unique, because if they were, EVERYTHING

// which contains object references (Any etc.) would have to be unique.

// Instead, the unique *World argument passed to 'corba_invoke' is used to

// serialize object invocations.

//

:: CORBA_Object

CORBA_Object__create :: CORBA_Object -> CORBA_Object

CORBA_Object__tc :: TypeCode

CORBA_Object__decode :: Any *World -> (MayBe CORBA_Object, *World)

CORBA_Object__encode :: CORBA_Object -> Any

CORBA_Object_is_a :: CORBA_Object String *World -> (Bool, *World)

CORBA_Object__nil :: *World -> (CORBA_Object, *World)

class CORBA_Object__widen_class a where

CORBA_Object__widen :: a -> CORBA_Object

instance CORBA_Object__widen_class CORBA_Object

////////////////////////////////

// ORB

////////////////////////////////

:: CORBA_ORB

CORBA_ORB_init :: [String] *World -> (CORBA_ORB, [String], *World)

CORBA_ORB_bind :: CORBA_ORB String String *World -> (CORBA_ORB, MayBe CORBA_Object, *World)

////////////////////////////////

// SERVER REQUEST

////////////////////////////////

:: CORBA_ServerRequest

CORBA_ServerRequest_operation :: CORBA_ServerRequest *env -> (String, *env)

CORBA_ServerRequest_set_arg_types :: CORBA_ServerRequest [CorbaArg] *World -> *World

CORBA_ServerRequest_get_arg :: CORBA_ServerRequest Int *World -> (Any, *World)

CORBA_ServerRequest_set_arg :: CORBA_ServerRequest Int Any *World -> *World

CORBA_ServerRequest_set_result :: CORBA_ServerRequest Any *World -> *World

CORBA_ServerRequest_set_exception :: CORBA_ServerRequest Any *World -> *World

//CORBA_ServerRequest_set_result_or_exception :: CORBA_ServerRequest (a *World -> (Any, *World)) a *World -> *World

////////////////////////////////

// SERVER SIDE BINDING

////////////////////////////////

//

// The server side binding uses a simplified version of the ObjectIO

// library.

//

// The process state

:: PSt l

= { ls :: l

, io :: IOSt l

}

// The private IO state

:: IOSt l

// Initialization function called by CORBA_Server_run

:: ServerInitFunc l *env :== (PSt l) env -> ((PSt l), env)

// The empty initialization function

NoInit :: (PSt l) *env -> ((PSt l), *env)

// Empty local state

:: Void = Void

// Wrapper function around the servants created by the programmer

// Can be used to support DSI (Dynamic Skeleton Interface)

:: CORBA_Servant_Wrapper l *env = E.obj: {

// The servant supplied by the programmer

obj :: obj,

// Function to dispatch requests to the servant supplied by the

// programmer

dispatch :: (PSt l) CORBA_ServerRequest obj env -> (((PSt l), obj), env)

};

// This function runs the CORBA server process

// It does the following:

// - It initializes the local process state from the first argument

// - It evaluates the second argument, producing an initial process state

// - It server CORBA requests until the CORBA_Server_exit function is

// evaluated

CORBA_Server_run :: l (ServerInitFunc l *World) *World -> *World

// Instruct the CORBA server process to exit

CORBA_Server_exit :: (PSt l) -> (PSt l)

// Registers a new CORBA servant

// CORBA_Server_add_servant ps repoid servant -> ps

CORBA_Server_add_servant :: (PSt l) String (CORBA_Servant_Wrapper l *World) *World -> (CORBA_Object, (PSt l), *World)

// Returns the reference of the currently executing object

// Only valid during the serving of a request

CORBA_Server_get_current :: (PSt l) -> CORBA_Object

// Local Variables:

// tab-width: 4

// End:

 

 

 

Irodalomjegyzék

 

OMG homepage

CORBA specification

CORBA book

MICO homepage

tclMico homepage

Clean homepage

Clean tutorial

Haskell homepage

Green Card homepage