Distribuované objekty

Základní princip je velmi jednoduchý. Objekt je identifikován svou adresou; jestliže tedy ve skutečnosti leží v jiném adresovém prostoru, potřebuje zástupce, který jej bude reprezentovat. Takovému zástupci se v OpenStepu říká proxy:

Objekt O1 v prvním procesu využívá proxy P, aby mohl posílat zprávy objektu O2, který leží ve zcela jiném procesu. Z hlediska objektu O1 se však proxy nijak neliší od samotného objektu O2; pošleme-li proxy jakoukoli zprávu, proxy ji zakóduje a prostřednictvím runtime systému odešle cílovému procesu. Tam runtime systém zprávu dekóduje a předá objektu O2; ten zprávu zpracuje a vrátí výslednou hodnotu; ta se opět zakódovaně předá zpět a proxy ji objektu O1 vrátí stejně, jako kdyby byla zpráva zpracována lokálně.

Tento přístup umožňuje nejen předávání zpráv libovolnou cestou -- ať již mezi jednotlivými procesy prostřednictvím jádra, nebo třeba mezi různými počítači prostřednictvím počítačové sítě. Důležitým faktem je to, že proxy zprávu (a samozřejmě také všechny její parametry) zakóduje přesně definovaným způsobem, který není závislý na architektuře nebo operačním systému. Proces 1 tedy může běžet například na Sunu, v OpenStepu pod operačním systémem Solaris; proxy zprávu Objective C zakóduje do nezávislého balíčku, a ten předá po síti procesu 2, který pracuje dejme tomu na počítači IBM PC pod operačním systémem Windows NT. Tamní runtime systém balíček převede -- samozřejmě nikoli na zprávu Objective C, ale na zprávu odpovídající protokolu OLE. Podobně je v principu možná komunikace mezi libovolnými objektovými systémy, stačí, aby jejich runtime obsahoval kód pro převádění lokálních zpráv na "balíčky" distribuovaných objektů. DO dokáží pracovat s vlastními "balíčky" stejně dobře jako s "balíčky" odpovídajícími standardům OLE a CORBA; runtime pro DO je navíc krom OpenStepu k dispozici pro Solaris, HP-UX, Digital UNIX a Windows NT.

Základní princip distribuovaných objektů je tedy velmi prostý. Stojí ale za to si uvědomit, že jeho faktická implementace zdaleka tak jednoduchá není; ukažme si např. potenciální problémy, které mohou nastat při kódování parametrů a návratové hodnoty zprávy:

Podívejme se nejprve na návratovou hodnotu: je zřejmé, že volající proces musí počkat, než příjemce zprávu zpracuje a vrátí návratovou hodnotu. Jestliže však zpráva žádnou hodnotu nevrací, není k tomuto čekání žádný důvod, a příjemce může klidně zpracovávat zprávu zatímco odesilatel pokračuje ve zpracování dalších příkazů. Objective C proto také nabízí modifikátor oneway, který můžeme použít s typem void; odešleme-li pak zprávu deklarovanou např. takto:

-(oneway void)jakasiZprava;

nebude odesilatel na nic čekat, a ihned po předání zprávy poběží dále -- zatímco příjemce bude zprávu zpracovávat.

I u nejjednodušších parametrů předávaných hodnotou je zapotřebí korektně vyřešit převody mezi různou reprezentací číselných hodnot a mezi různým pořadím bytů v různých architekturách -- zatímco např. dvaatřicetibitový NEXTSTEP na počítači NeXT ukládá celé číslo 1000 jako čtyři byty 0x00, 0x00, 0x03, 0xE8, bude stejné číslo v šestnáctibitových Windows uloženo jako dva byty v opačném pořadí: 0xE8, 0x03.

Ještě složitější situace je u parametrů, předávaných referencí (prostřednictvím ukazatele). Obecné řešení je poměrně komplikované -- runtime systém musí
(1) alokovat na straně příjemce zprávy dostatečně veliký úsek paměti pro parametr (již zjištění této velikosti není triviální, a mimo Objective C může být dokonce i nemožné);
(2) zkopírovat do tohoto prostoru obsah parametru z volajícího procesu;
(3) předat zprávu;
(4) zkopírovat obsah prostoru zpět do parametru ve volajícím procesu;
(5) uvolnit alokovanou paměť u příjemce.

Objective C proto nabízí modifikátory in a out, které můžeme použít pro specifikaci je-li zapotřebí provést všechny tyto kroky -- modifikátor in znamená, že parametr je pouze vstupní, out značí pouze výstupní parametr. Odesíláme-li tedy prostřednictvím DO např. zprávu

-(void)vstup:(int in *)vstup vystup:(int out *)vystup oba:(int *)oba;

provede runtime následující akce:
(1) alokuje u příjemce prostor pro tři celá čísla (vstup, vystup, oba);
(2) zkopíruje čísla z míst, na které ukazují pointery vstup a oba na straně volajícího do odpovídajících míst na straně příjemce. Kopírovat obsah vystupu se neobtěžuje, protože ten je označen jako pouze výstupní;
(3) předá zprávu příjemci s adresami míst, alokovaných v kroku (1);
(4) zkopíruje obsah míst vystup a oba od příjemce na odpovídající místa na straně odesilatele. Kopírovat vstup není zapotřebí, protože ten je označen jako pouze vstupní;
(5) uvolní paměť.

Zajímavé možnosti nastávají i v případě, že předávaným parametrem je objekt. V takovém případě musí runtime systém automaticky vytvořit na straně příjemce proxy, odpovídající předanému objektu, a předat příjemci jeho adresu:

Na obrázku poslal objekt O1 objektu O2 zprávu, která obsahovala odkaz na objekt X1 (umístěný samozřejmě ve stejném adresovém prostoru jako O1). vidíme, že runtime automaticky vytvořil další proxy na straně objektu O2 a tu mu předal; objekt O2 se samozřejmě na proxy zase dívá "jako na objekt X1" -- jakoukoli zprávu, kterou pošle proxy, ve skutečnosti zpracuje objekt X1.

Objective C zde nabízí další modifikátor, umožňující optimalizaci spolupráce mezi procesy: představme si, že objekt X1 je nějakým velmi jednoduchým a nevelikým objektem -- může se jednat například o NSString nebo NSNumber. V takovém případě -- zvláště je-li objekt neměnný -- není výše popsané řešení s vytvořením proxy právě šikovné: při každém přístupu k objektu musíme skrz síť, a ta je přece jen relativně pomalá. Deklarujeme-li však zprávu, která předává objekt, takto:

-(void)tadyJeObjekt:(bycopy id)x;

nevytvoří se na straně příjemce proxy, ale kompletní kopie předaného objektu:

Je zřejmé, že v případě, že objekt O2 bude s objektem X1 komunikovat velmi často, je to daleko výhodnější.

Situace je ve skutečnosti ještě složitější: zatím jsme vůbec neřešili problémy jak vytvořit první proxy? Jak vyhledat cestu mezi oběma procesy, kde proxy vezme cílovou adresu, na kterou má odeslat zakódovanou zprávu? K tomu všemu slouží další objekt, tzv. connection. Jak na straně odesilatele, tak i na straně příjemce je connection; proxy nekomunikuje přímo s druhým procesem, ale s connection:

Obrázek ukazuje skutečný postup odeslání zprávy z objektu O1 objektu O2:

(0) někdy dříve bylo navázáno spojení mezi oběma objekty connection (na mechanismus navazování spojení se podíváme v odstavci, věnovaném přímo třídě NSConnection). Při navázání spojení se vždy na straně "klienta" (proces 1) vytvoří proxy, reprezentující hlavní objekt na straně "serveru" (proces 2);
(1) objekt O1 zašle zprávu "objektu O2" -- ve skutečnosti tedy proxy;
(2) proxy zprávu zakóduje a hotový balíček předá connection ve vlastním procesu;
(3) hotové spojení mezi objekty connection se využije pro předání balíčku do procesu 2;
(4) connection v procesu 2 balíček převede zpět na zprávu, a tu předá objektu O2.

Podobně probíhá veškerá komunikace; pouze objektů proxy je více. Objektů connection může být více v případě, že jeden proces komunikuje s více procesy než s jedním; každý connection se potom stará o "své" objekty proxy. Komunikace mezi objekty connection je samozřejmě obousměrná; proto jsme v minulém odstavci psali pojmy klient a server v uvozovkách: tyto pojmy se vztahují ke způsobu navázání spojení; využívat již navázané spojení lze obousměrně (peer to peer).



NSConnection

Objekty třídy NSConnection reprezentují connection -- každé spojení mezi dvěma procesy je tedy zajišťováno dvojicí objektů NSConnection. Z hlediska navázání spojení je vždy jeden proces "serverem" -- ten vytvoří svůj objekt NSConnection a zaregistruje jej u name serveru sítě pod nějakým jménem. Navíc musí server určit 'hlavní objekt' (root object), jehož proxy automaticky dostane každý klient. Libovolný počet "klientů" si pak může vyžádat vytvoření vlastního objektu NSConnection pro spojení se "serverem" se zadaným jménem; pokud takové jméno existuje, vytvoří se na straně klienta automaticky objekt NSConnection, naváže se spojení mezi ním a serverem, a vytvoří se proxy, reprezentující u klienta 'hlavní objekt' serveru.

typický kód serveru pak vypadá takto:

// hlavní objekt
id root=[MujHlavniObjekt new];
// každý thread má svůj standardní connection
id myconn=[NSConnection defaultConnection];
// určíme hlavní objekt
[myconn setRootObject:root];
// zaregistrujeme connection
if (![myconn registerName:@"Jméno serveru..."]) {
// nelze registrovat ... buď již je toto
// jméno zaregistrováno, nebo je
// problém s name serverem sítě
}

Kód klienta je ještě mnohem jednodušší:

id server=[NSConnection
rootProxyForConnectionWithRegisteredName:@"Jméno serveru..."
host:@"*"];
if (server==nil) {
// server se zadaným jménem
// není k dispozici
}

Jestliže nyní klient pošle jakoukoli zprávu "serveru" -- například

[server haloJsiTam];

dostane tuto zprávu postupem, který jsme popsali v minulém odstavci, objekt root v serveru.

Třída NSConnection nabízí řadu dalších služeb, které usnadňují řízení komunikace mezi oběma procesy. Pomocí metody +(NSArray*)allConnections např. můžeme získat seznam všech connection, které jsou v tomto procesu (přesněji threadu) otevřeny; metody setReplyTimeout:(NSTimeInterval)interval a setRequestTimeout:(NSTimeInterval)interval určují jak dlouho má connection čekat na reakci druhého procesu, než prostřednictvím NSNotification (se kterým jsme se seznámili v minulých dílech) informuje každého, koho to zajímá, o tom, že spojení bylo přerušeno. Řada dalších zpráv umožňuje řídit spojení, zjišťovat statistiky ohledně nevázaného spojení, rozhodovat zda se konkrétní spojení smí navázat nebo ne a tak dále.



NSProxy

Objekty třídy NSProxy reprezentují proxy; z programátorského hlediska tedy na nich vlastně není co popisovat, protože proxy se vždy chová jako objekt, na který se odkazuje.

Přesto existuje jedna specifická zpráva, kterou interpretuje právě proxy (přesněji řečeno, jeho podtřída NSDistantObject -- což je z hlediska tohoto orientačního popisu lhostejné). Musíme si uvědomit, že odesílání zprávy je ve skutečnosti ještě o něco složitější, než jak jsme je dosud popsali: uvedli jsme, že proxy zabalí nejprve zprávu a její parametry do balíčku, a ten předá (prostřednictvím objektu NSConnection) cílovému procesu. Aby však bylo možné zprávu a její parametry zabalit, musí proxy znát nejprve typy parametrů, které skutečný objekt ve zprávě očekává (připomeňme rozbor komplikovaného předávání parametrů různých typů), a na ty se musí nejprve zeptat. Při odeslání jednoduché zprávy tedy po síti musí proběhnout (nejméně) čtyři pakety:

(1) klient->server -- proxy se ptá cílového objektu na typy parametrů pro danou zprávu;
(2) server->klient -- cílový objekt odesílá popis typů parametrů zprávy;
(3) klient->server -- proxy odesílá "balíček" se zprávou a jejími parametry;
(4) server->klient -- cílový objekt vrací výsledek zprávy (tento krok si ušetříme u zpráv typu oneway).

Pro zvýšení efektivity můžeme ušetřit první dva kroky jednoduchým způsobem: informujeme proxy jednorázově o typech parametrů všech zpráv, které je cílový objekt schopen zpracovat (samozřejmě to není možné v případech, kdy cílový objekt zprávy dynamicky přesměrovává; to je ale dost vyjímečný případ). Objective C nabízí prostředek pro zápis seznamu zpráv a typů jejich parametrů; ten se jmenuje protokol. Každému proxy pak můžeme zprávou setProtocolForProxy:(Protocol *)proto -- chceme-li -- přidělit protokol, který popisuje zprávy, zpracovávané objektem, který proxy reprezentuje. Tím si uspoříme odesílání prvních dvou paketů -- proxy vytvoří ze zprávy a z jejích parametrů balíček na základě protokolu.



Ostatní služby FoundationKitu

Ačkoli je FoundationKit především objektová knihovna, existuje přece jen několik služeb, které je daleko pohodlnější reprezentovat klasickými funkcemi jazyka C. Většina těchto funkcí je pouze nadstavbou nad jádrem a nabízí služby, které v běžných API požadujeme přímo od jádra (a v NEXTSTEPu je skutečně zajišťoval přímo MACH). Připomeňme si ale, že OpenStep může pracovat nad libovolným jádrem; píšeme-li proto pro něj programy, nemůžeme využívat služby žádného konkrétního jádra, protože bychom se tím zbavili přenositelnosti. Jako obvykle se seznámíme jen s nejzajímavějšími službami:

První skupina funkcí umožňuje programátorům užší spolupráci se systémem virtuální paměti: funkce NSPageSize(void) vrací velikost jedné stránky, zatímco funkce NSRoundDownToMultipleOfPageSize(unsigned byteCount) a NSRoundUpToMultipleOfPageSize(unsigned byteCount) umožňují pohodlné zaokrouhlování na hranice stránek. Virtuální paměť můžeme i fakticky alokovat funkcí NSAllocateMemoryPages(unsigned byteCount) a uvolňovat pomocí funkce NSDeallocateMemoryPages(void *pointer, unsigned byteCount); rychlé kopírování rozsáhlých úseků dat ve virtuální paměti umožňuje funkce NSCopyMemoryPages(const void *source, void *destination, unsigned byteCount) -- čtenářům z prostředí DOSu nebo Windows připomeneme, že typ unsigned je samozřejmě dvaatřicetibitový. Minimální praktický smysl naproti tomu má služba NSRealMemoryAvailable(void), která zjistí rozsah reálné paměti, instalované v počítači; tu a tam se ale může hodit, např. pro automatické optimalizování výpočtu.

Pro optimalizaci způsobu, jakým aplikace využívá adresový prostor, můžeme v OpenStepu využít tzv. zóny. Zóna je z hlediska programátora vlastně samostatným adresovým prostorem pro alokaci a uvolňování bloků paměti; jinými slovy, bloky paměti alokované v jedné a téže zóně budou vždy v adresovém prostoru aplikace "vedle sebe", a nikdy mezi ně nebudou zamíchány bloky z jiné zóny. Je zřejmé, že to umožňuje omezit fragmentaci adresového prostoru (jinými slovy, lepší využití virtuální paměti) -- alokujeme-li objekty pro jednotlivé funkční celky aplikace ze samostatných zón, bude po ukončení funkčního celku a uvolnění jeho objektů k dispozici souvislý úsek adresového prostoru.

Zónu můžeme vytvořit funkcí NSZone *NSCreateZone(unsigned startSize, unsigned granularity, BOOL canFree), která umožňuje určit i krok, ve kterém bude zóna narůstat a zmenšovat se (a tak vlastně volit mezi úsporou paměti nebo výkonu). Dalšího zrychlení můžeme docílit u zón pro objekty, které budou zapotřebí po celou dobu běhu aplikace -- určíme-li jako hodnotu canFree NO, nebude možné bloky paměti v zóně uvolňovat, zato však bude alokace bloku extrémně rychlá. Funkce NSRecycleZone(NSZone *zone) zónu jako celek uvolní. Pro práci s bloky paměti v zóně jsou k dispozici funkce void *NSZoneMalloc(NSZone *zone, unsigned size), void *NSZoneCalloc(NSZone *zone, unsigned numElems, unsigned numBytes), void *NSZoneRealloc(NSZone *zone, void *pointer, unsigned size) a void NSZoneFree(NSZone *zone, void *pointer), které celkem přesně odpovídají standardním službám malloc, calloc, realloc a free; pro alokaci objektů samozřejmě máme k dispozici zprávu +(id)allocWithZone:(NSZone *)zone. Standardní zónu, která se vytvoří automaticky při startu programu a ve které pracují služby typu malloc a calloc můžeme samozřejmě zjistit pomocí funkce NSZone *NSDefaultMallocZone(void); můžeme také získat zónu kteréhokoli alokovaného bloku funkcí NSZone *NSZoneFromPointer(void *pointer) a zónu kteréhokoli objektu metodou -(NSZone*)zone.

OpenStep definuje řadu typů, které reprezentují bod, obdélník, rozměry blíže neurčeného objektu a podobně; ve FoundationKitu jsou pak funkce pro všechny potřebné geometrické operace -- jako je ověření je-li bod součástí obdélníku, vyhledání průniku dvou obdélníků, vytvoření minimálního obdélníku obsahujícího dva zadané obdélníky... Nebudeme plýtvat místem v tomto článku vyjmenováváním všech geometrických funkcí (je jich více než třicet); programátor mezi nimi nalezne vše, co může při práci s body nebo obdélníky v ploše potřebovat.

Funkce NSString *NSUserName(void) vrátí jméno uživatele, v rámci jehož konta program pracuje; podobně pomocí funkce NSString *NSHomeDirectory(void) můžeme zjistit, kde leží domovský adresář aktivního uživatele a pomocí funkce NSString *NSHomeDirectoryForUser(NSString * userName) nalezneme domovský adresář kteréhokoli uživatele. Funkce void NSLog(NSString *format, ...) zapíše požadované informace do systémového logu (ať již je v konkrétním systému realizován jakkoli); ve formátovacím řetězci můžeme kromě všech standardních argumentů typu printf použít i argument %@, který zajistí vypsání hodnoty libovolného objektu.

Pomocí funkce NSString *NSLocalizedString(NSString *key, NSString *comment) získáme lokalizovanou verzi řetězce key v jazyce, zvoleném uživatelem pro komunikaci (pokud je samozřejmě překlad součástí překladových tabulek). Zajímavá je položka comment -- z hlediska samotného programu nemá totiž vůbec žádný smysl. OpenStep však obsahuje utilitu, která na základě zdrojového kódu automaticky připraví překladové tabulky, a zapíše do nich tyto poznámky, takže překladatel tabulek je bude mít později k dispozici (i když zdrojové texty programu nedostane).

Nakonec se můžeme zmínit o čtyřech velmi zajímavých funkcích, které umožňují v případě potřeby jazyk Objective C vlastně interpretovat (tj. vyhodnocovat jeho výrazy za běhu): funkce Class NSClassFromString(NSString *aClassName) vyhledá třídu zadaného jména a vrátí ji; podobně funkce SEL NSSelectorFromString(NSString *aSelectorName) za běhu přeloží zprávu na selektor -- tj. takovou reprezentaci zprávy, kterou je přímo možné použít při běhu. Inverzní funkce NSString *NSStringFromClass(Class aClass) a NSString *NSStringFromSelector(SEL aSelector) pak umožňují zjistit jméno třídy nebo zprávu za běhu.





(obsah)


Copyright (c) Ondra Čada