W tym rozdziale omówione są konstrukcje dostępne w języku TLCC, ze szczególnym zwróceniem uwagi na podobieństwa i różnice w stosunku do C++ i Javy. Dla każdej omawianej konstrukcji podany jest przykład.
Ponieważ wynikowy kod tworzony jest w C++, więc składnia języka TLCC również jest zbliżona do C++. Jednak nie wszystkie elementy składniowe z C++ są dostępne. Przykładowo, nie ma możliwości używania wskaźników, ponieważ umożliwiają one bezpośrednie operacje w pamięci na obiektach. Tymczasem konkretny obiekt może się znajdować w innym procesie (np. na innej maszynie), więc dostęp do niego powinien się odbywać wyłącznie przy pomocy referencji systemu CORBA.
Każdy plik zawierający kod w TLCC jest zasadniczo ciągiem
definicji klas. W pliku może również się znajdować definicja funkcji
main(), która jest wykonywana w momencie tworzenia nowej
sesji dla użytkownika. W definicji klasy bezpośrednio wstawia się
kod metod. Oto prosty przykład definicji klasy.
class A
{
private:
int value;
public:
A(int value)
{
this.value = value;
}
int getValue()
{
return value;
}
}
Dostępne jest, tak jak w C++, wielokrotne dziedziczenie. Składnia
przekazywania argumentów do konstruktorów z nadklas jest również taka
sama.
class B : A
{
private:
string caption;
public:
B(int value, string
caption) : A(value)
{
this.caption = caption;
}
}
Dostępne są następujące rodzaje pól i metod:
Dzięki atrybutom dzielonym możliwa jest łatwa wymiana danych między sesjami. Można tworzyć specjalne obiekty dzielone, z dzielonymi wszystkimi atrybutami, pełniące rolę bazy danych dla aplikacji.
Aby uprościć generowanie kodu w HTML-u można wykorzystać
specjalne funkcje - wizualizatory. Umożliwiają one bezpośrednie wstawianie
dowolnego napisu (w tym przypadku HTML-a) w wielu wierszach, bez konieczności
stosowania cudzysłowów. Każda klasa może mieć jeden domyślny wizualizator
(default visualizer)
oraz wiele wizualizatorów zwykłych. Podczas działania aplikacji można
przełączać wizualizatory przy pomocy wbudowanej funkcji set_visualizer().
Kolejny przykład demonstruje klasę z wizualizatorami.
class Node
{
private:
int value;
public:
Node()
{
value = 0;
}
default visualizer
showPlus()
[
<p>Value is: #value#.</p>
<p>It is greater or equal 0.</p>
]
visualizer showMinus()
[
<p>Value is: <font color="red">#value#</font>.</p>
<p>It is less than 0.</p>
]
void setValue(int
value)
{
this.value = value;
if (value >= 0)
set_visualizer(showPlus);
else
set_visualizer(showMinus);
}
int getValue()
{
return value;
}
}
W przedstawionym kodzie znaki # pełnią specjalną funkcję,
ponieważ pozwalają umieszczać wyrażenia lub instrukcje wewnątrz kodu
HTML. Jeśli będzie to wyrażenie, to jego wartość dopisze się w odpowiednim
miejscu do generowanego przez wizualizator napisu. Natomiast gdy są
to instrukcje, to zostaną wykonane. Bloki '#' oraz bloki
wizualizacji '[]' można wielokrotnie zagnieżdżać. Oto
przykład takiej konstrukcji:
visualizer showMultiplicationTable()
[
<table align="center">
#{
for(int
y = 1; y <= 10; y++)
[
<tr>
#{
for(int
x = 1; x <= 10; x++)
[<td align="center">#x*y#</td>]
}#
</tr>
]
}#
</table>
]
Przykład ten jest wizualizatorem, który wyświetla tabliczkę mnożenia.
Przejście z trybu wstawiania kodu w HTML-u do trybu instrukcji odbywa
się jedynie przy pomocy znacznika '#'. W szczególności wstawioną
instrukcją może być blok '{}', tak jak w podanym przykładzie.
Przy pomocy funkcji set_root() można
ustawić korzeń wizualizacji, czyli obiekt, od którego rozpoczyna się
wyświetlanie strony WWW. Aby wewnątrz wizualizatora pewnego obiektu
wyświetlić inny obiekt należy użyć wbudowanej funkcji visualize().
W ten sposób wszystkie obiekty, które reprezentują poszczególne elementy
strony mogą tworzyć drzewo wyświetlania. Można również wprowadzić
takie zależności między obiektami, aby graf wyświetlania nie był drzewem
(ponieważ będzie zawierał cykle). Oto przykładowy kod, który korzysta
z klasy Node.
class Service
{
private:
Node node;
public:
Service()
{
node = new Node();
}
default visualizer
show()
[
<html>
<head></head>
<body>
#visualize(node)#
</body>
</html>
]
}
void main()
{
set_root(new
Service());
}
Wraz z definicją klasy Node przedstawiony kod stanowi kompletny
program w tym języku. Na razie jego działanie jest bardzo proste -
sprowadza się do wypisania informacji, że bieżąca wartość jest równa
0. W kolejnych punktach tego rozdziału klasa Service zostanie
zmodyfikowana w taki sposób, aby można było zmieniać tą wartość.
Aby obiekty wchodzące w skład strony WWW były interaktywne,
trzeba skojarzyć z poszczególnymi odnośnikami ciągi instrukcji (akcje).
Gdy użytkownik klika na odnośnik, to aplikacja wykonuje związany z
nim kod. Łatwo można zmodyfikować wizualizator w klasie Service
tak, aby były dostępne odnośniki pozwalające zwiększać i zmniejszać
bieżącą wartość.
class Service
{
...
default visualizer
show()
[
<html>
<head></head>
<body>
#visualize(node)#
<p><a href=#action{node.setValue(node.getValue()+1)}#>
increment value</a></p>
<p><a href=#action{node.setValue(node.getValue()-1)}#>
decrement value</a></p>
</body>
</html>
]
}
W bloku, który występuje po słowie action można
umieszczać dowolne ciągi instrukcji. Mogą one korzystać ze zmiennych
lokalnych zadeklarowanych w wizualizatorze, przy czym istotna jest
wartość takiej zmiennej podczas tworzenia dowiązania (wartość ta zostaje
przekazana w tym odnośniku). Gdy mamy bardzo dużo różnych akcji, to
takie podejście pozwala obsługiwać je lokalnie w obiektach, a nie
ręcznie kodować w URL i rozpoznawać podczas przyjmowania żądania HTTP
przez aplikację (opis protokołu HTTP znajduje się w pozycji [13]).
Łatwo można także wykorzystać w akcjach zmienne lokalne, których wartości
są automatycznie kodowane w URL.
Tworzenie formularzy i odczytywanie z nich danych również
jest ułatwione. Z poszczególnymi elementami formularza przy pomocy
funkcji field możemy wiązać zmienne, na które
będą automatycznie przypisane odpowiednie wartości, gdy użytkownik
wyśle ten formularz. Istnieje także możliwość umieszczania w formularzu
akcji przy pomocy bloku form_action. Zatem klasę
Service można rozszerzyć w taki sposób, aby użytkownik mógł
podać wyświetlaną wartość.
class Service
{
...
default visualizer
show()
[
<html>
<head></head>
<body>
#visualize(node)#
<p><a href=#action{node.setValue(node.getValue()+1)}#>
increment value</a></p>
<p><a href=#action{node.setValue(node.getValue()-1)}#>
decrement value</a></p>
#int value#
<p>
<form action=#form_action()#
method="get">
New value:
<input type="text" name=#field(value)#
value=#node.getValue()#>
<input type="submit" value="Set
value">
#form_action{node.setValue(value)}#
</form>
</p>
</body>
</html>
]
}
W podanym przykładzie wykorzystywana jest zmienna lokalna value,
na którą pobierana jest wartość podana przez użytkownika. Pierwsze
wywołanie funkcji form_action() w nagłówku
formularza powoduje wygenerowanie odpowiedniego URL dla atrybutu action.
Wewnątrz formularza można używać funkcji field(),
żeby utworzyć nazwę pola formularza (atrybut name). Na końcu
w bloku form_action umieszcza się akcję wykonywaną
po wysłaniu formularza (zmienna value ma już odpowiednią
wartość, więc można z niej skorzystać).
W celu tworzenia w przedstawionym przeze mnie języku
większych aplikacji, niezbędna jest możliwość dzielenia kodu źródłowego
na moduły. Zatem potrzebna jest instrukcja, która pozwoli dołączyć
jakieś inne pliki, zawierające dodatkowe klasy. W TLCC dostępna jest
specjalna instrukcja uses. Oto przykład
jej użycia.
uses utils in "modules/utils.tlcc";
...
... new utils::Dictionary();
...
Na początku pliku deklarujemy moduły, z których korzystamy. Podajemy
przy tym nazwę modułu (przy pomocy której będziemy się do niego odwoływać
w programie) oraz (opcjonalnie) ścieżkę do pliku. Podczas translacji
na C++ nastąpi sprawdzenie, czy moduł ten nie wymaga rekompilacji.
Natomiast w dalszej części tego przykładu pokazana jest składnia używana przy odwoływaniu się do klasy z modułu. Jeśli w innych używanych modułach nie ma klasy o tej samej nazwie, to można używać bezpośrednio nazwy klasy, bez konieczności poprzedzania ją nazwą modułu.
Użyteczną rzeczą jest możliwość wykorzystania istniejących
bibliotek dla C/C++. Dlatego w TLCC powinna być możliwość umieszczania
kodu napisanego bezpośrednio w C/C++. Kod taki nie jest tłumaczony
przez translator, tylko przepisywany do pliku wynikowego. Aby wstawić
kod w C/C++ należy umieścić go między znacznikami '@'. Oto
przykład:
@#include <stdio.h>@
...
void main()
{
...
@printf("Hello world");@
}
Aby zbyt często nie łączyć kodu napisanego w TLCC z kodem w C/C++
najlepiej napisać pomocniczy moduł, który pośredniczy w wywołaniach
funkcji z zewnętrznych bibliotek. Kolejny przykład zawiera fragment
takiego modułu, który obsługuje funkcję printf().
@#include <stdio.h>@
class System
{
static void
echo(string str)
{
@printf((char *) str);@
}
}
W ten sposób można prosto zaimplementować obsługę np. bazy danych,
korzystając z bibliotek dla C/C++. Istnieje również możliwość napisania
narzędzia, które na podstawie plików nagłówkowych będzie generowało
odpowiednie moduły.
Oto przykład pomocniczej klasy pośredniczącej między bazą danych MySQL
[10] a programem napisanym w języku TLCC.
@
#include<iostream>
#include<iomainp>
#include<sqlplus.hh>
@
class Database
{
private:
@
Connection connection;
Query query;
Result result;
Result::iterator iter;
Row row;
@
public:
Database(string db, string
host, string user, string
passwd)
{@
connection.connect((char *) db, (char *) host,
(char *) user,
(char *) passwd);
@}
void executeQuery(string
content)
{@
query = connection.query();
query << (char *) content;
result = query.store();
iter = result.begin();
@}
void resetIterator()
{@
iter = result.begin();
@}
void hasMoreRows()
{@
return iter != result.end();
@}
void loadNextRow()
{@
row = *iter;
iter++;
@}
string getField(string
name)
{@
return CORBA::string_dup(row[(char *) name]);
@}
string getField(int
index)
{@
return CORBA::string_dup(row[index]);
@}
}
Często zdarza się, że do tworzenia stron WWW wchodzących
w skład serwisu używane są specjalne edytory generujące kod HTML.
Zatem wygodnie by było wykorzystywać pliki utworzone przez edytory
jako wizualizatory w aplikacji. Jest to bardzo proste, oto przykład:
visualizer showPage() in
"page.html";
Zamiast pełnej definicji wizualizatora podajemy po prostu nazwę pliku,
w którym się znajduje. W takim pliku-wizualizatorze można wstawiać
wszystkie konstrukcje dostępne w zwykłych wizualizatorach.
Dzięki złożonym typom danych oraz specjalnym operacjom na nich, zarządzanie informacjami zgromadzonymi w obiektach jest łatwiejsze. W tym punkcie zostaną omówione napisy, obiekty, struktury i tablice.
W TLCC używanie napisów jest wygodniejsze niż w C++, dzięki specjalnemu
typowi string. Oto przykłady operacji na łańcuchach:
string str = "abc"; | // ustawienie wartości zmiennej |
str += "def"; | // dołączenie napisu |
int length = str; | // pobranie długości napisu |
char c = str[2]; | // pobranie litery 'c' |
Natomiast tworzenie i usuwanie obiektów jest tak łatwe jak w C++, mimo że w TLCC są to obiekty systemu CORBA. Wystarczy użyć odpowiednio operatorów new i delete. Istnieją jednak specjalne odmiany operatora new. Jedna z nich pozwala utworzyć dany obiekt nie w bieżącym procesie, ale w pewnym (najmniej obciążonym) serwerze obiektów dzielonych. Dzięki temu można uzyskać równomierne rozmieszczenie obiektów dzielonych na serwerach, przez co obsługa danych jest rozproszona. Operator ten nazywa się new_shared. Jeszcze inną odmianą tego operatora jest new_separated, dzięki której utworzony obiekt będzie odseparowany od bieżącej sesji użytkownika, w której domyślnie tworzone są nowe obiekty. W takim przypadku obiekt ten nie będzie odczytywany z dysku wraz z sesją, lecz dopiero w momencie, gdy wystąpi odwołanie do niego.
W TLCC można korzystać ze struktur, które odpowiadają strukturom udostępnianym
przez środowisko CORBA. Ich zadaniem jest grupowanie danych, dzięki
czemu można np. przekazać w wyniku funkcji więcej informacji. Struktury
są zawsze przekazywane przez wartość, a więc odwołania do nich są
zawsze lokalne. Oto przykład definicji struktury.
struct Example
{
string str;
int value;
}
Ostatnim typem danych, który pozostał do omówienia w tym punkcie są
tablice. W TLCC są dostępne dodatkowe operacje na tablicach, dzięki
którym można dynamicznie zmieniać ich wielkość. Dlatego tablica może
służyć do grupowania danych w obiekcie, podobnie jak tabela w bazie
danych. Oto składnia tych operacji:
string[] strings; | // utworzenie pustej tablicy napisów |
strings <- "abc"; | // dodanie napisu do tablicy |
int length = strings; | // pobranie wielkości tablicy (length=1) |
strings-; | // zmniejszenie wielkości tablicy o 1 |
Przykłady zastosowań złożonych typów danych w konkretnej aplikacji znajdują się w rozdziale 5.
W TLCC dostępne są wbudowane funkcje, które pozwalają określić parametry sesji. Funkcja transient_session() powoduje, że bieżąca sesja będzie usunięta po dłuższym czasie nieaktywności. Funkcję tą można wywołać z parametrem, który określa liczbę sekund, jaką sesja musi być nieaktywna, aby została usunięta. Istnieje także funkcja persistent_session(), która sprawia, że bieżąca sesja nie będzie skasowana.
Wszystkie obiekty tworzone przez użytkownika są obiektami systemu CORBA. Oznacza to, że można się do nich swobodnie odwoływać (mając referencję do takiego obiektu) z dowolnego procesu (nawet na innej maszynie). Ponadto dzięki zastosowaniu mechanizmu POA, który jest częścią standardu CORBA, wszystkie obiekty są trwałe - będą automatycznie zapisane na dysku przed usunięciem z pamięci. Potrzeba taka zachodzi wtedy, gdy dany obiekt przez pewien czas nie jest używany lub w przypadku restartu serwera.
Dzięki temu, że obiekty są trwałe, wszystkie dane użytkownika w serwisie WWW mogą być przechowywane w jego obiektach. Zamiast używać bazy danych można więc wykorzystać obiekty do składowania różnych informacji. Jest to prostsze rozwiązanie niż odwzorowywanie obiektów na tabele oraz każdorazowy odczyt/zapis tych danych z/do bazy, gdy nadchodzi żądanie od użytkownika. Ponadto jeśli chcemy zaprogramować serwis WWW w sposób obiektowy (np. przy pomocy J2EE), to będziemy musieli na podstawie bazy danych odtwarzać obiekty użytkownika, które realizują funkcjonalność aplikacji i następnie po wykonaniu określonej akcji, zapisywać je do bazy. Takie podejście jest bardzo niewygodne w implementacji i wymaga napisania sporej ilości pomocniczego kodu (do komunikacji z bazą).
Automatyczny zapis obiektów na dysk w TLCC działała w taki sposób, że dla każdego serwera obiektów oraz serwera obiektów dzielonych wykorzystywany jest specjalny plik, który pełni rolę składnicy obiektów. Wszystkie czynności związane ze składowaniem obiektów są wykonywane automatycznie.
Ponadto dzięki zastosowaniu technologii CORBA, widoczność wszystkich tworzonych obiektów nie ogranicza się tylko do napisanej aplikacji. Obiekty te mogą być współdzielone przez wiele aplikacji napisanych w tym języku, a nawet przez dowolne inne programy. Dzięki temu, że CORBA obsługuje wiele języków programowania (w przeciwieństwie do RMI w Javie) jest możliwość napisania programu, np. w C++ lub w Javie, przetwarzającego dane, które są zawarte w obiektach dzielonych. Aby otrzymać referencję do obiektu dzielonego wystarczy wysłać odpowiednie żądanie do serwera. Analogicznie, gdy chcemy otrzymać jakieś informacje o obiektach wchodzących w skład sesji użytkowników, to wywołujemy metody z serwera obiektów (np. pobranie sesji o określonym identyfikatorze lub korzenia wizualizacji dla pewnej sesji).
Przy wyborze języka docelowego dla translatora brałem pod uwagę C++ i Javę. Zdecydowałem się na C++, ponieważ jest on szybszy od Javy. Wstępne testy pokazały, że implementacja MICO CORBA [9] (wykorzystująca C++) jest english razy szybsza niż CORBA używana przez IBM J2RE 1.3.0. Ponadto zwykły kod pisany w C++ jest również znacząco szybszy.
Język TLCC jest podobny do Javy, jeśli pominiemy dodatkowe konstrukcje związane z wizualizacją i składowaniem danych. Łatwo można by go zmodyfikować w taki sposób, aby miał on składnię taką samą jak Java. Jednak miałoby to sens tylko wtedy, gdyby można było wykorzystywać klasy biblioteczne Javy (np. klasę Object zamiast wbudowanego typu object). Niestety nie jest to możliwe, ponieważ zakładam, że do wszystkich używanych klas można się odwoływać zdalnie. Natomiast obiekty biblioteczne w Javie nie mogą być obsługiwane w ten sposób - wymaga to napisania odpowiednich interfejsów i dodatkowego kodu związanego z systemem CORBA wewnątrz klas, których używamy (czyli zmiany implementacji tych klas). W przeciwnym razie nie będziemy mogli takiego zewnętrznego obiektu swobodnie używać, ze względu na to, że odwołania do niego mogą pochodzić z innego procesu (być może na innej maszynie).
Zatem lepiej tłumaczyć TLCC na C++ i zapewnić, że jego składnia będzie zbliżona do C++. Jednak pewne ograniczenia są niezbędne, ponieważ kod, który piszemy zostanie przetłumaczony zgodnie z wymaganiami technologii CORBA. Nie jest możliwe używanie wskaźników, ponieważ dają one możliwość bezpośredniego dostępu do pamięci obiektów. Natomiast w wynikowym programie użytkownik nie korzysta z obiektu, który stworzył przy pomocy operatora new, lecz z pewnego obiektu pomocniczego, który jest jedynie pośrednikiem między oryginalnym obiektem a kodem użytkownika.
To samo dotyczy wzorców klas w C++ (ang. templates) i przeciążania operatorów. Jednak te wszystkie wymienione cechy nie są konieczne nawet przy pisaniu dużych aplikacji. Dowodem jest Java, w której żadnej z tych cech nie ma. Jedyne większe różnice (pomijając konstrukcje związane z wizualizacją i składowaniem danych) między TLCC a Javą są takie, że Java posiada interfejsy i nie umożliwia wielodziedziczenia. Natomiast w TLCC istnieje wielodziedziczenie, a nie ma interfejsów - czyli tak jak w C++.
Dostępne są natomiast pozostałe konstrukcje z C++, w szczególności wszystkie rodzaje instrukcji, operatory, typy itp. Usuwanie obiektów jest jawne (operator delete), ponieważ mamy do czynienia z rozproszonymi obiektami. Zatem reasumując: