A NesC programozási nyelv

Komponensek

A komponensek specifikációja

Látható, hogy egy komponens specifikációs része azonos a modulok és a konfigurációk esetében.
Egy specifikációs részt a következő grammatikai szabályok alapján írhatunk le:

specification: { uses-provides-list } uses-provides-list: uses-provides uses-provides-list uses-provides uses-provides: uses specification-element-list provides specification-element-list specification-element-list: specification-element { specification-elements } specification-elements: specification-element specification-elements specification-element specification-element: declaration interface renamed-identifier parametersopt

Tehát egy specifikáció megvalósított (provides), vagy felhasznált (uses) specifikációs elemeknek a halmaza. Specifikációs elem lehet egy interfész példány egy parancs vagy egy esemény. Azt már említettük, hogy egy komponensnek meg kell valósítania azokat a parancsokat, amelyek a megvalósított interfészekben vannak deklarálva, illetve azokat az eseményeket, amelyek a felhasznált interfészekben vannak. Ezen kívül még meg kell valósítania a komponensnek azokat a parancsokat és eseményeket, amelyeket ő maga vezet be a specifikációs részben. Sokszor előfordul az az eset, hogy több felhasznált vagy megvalósított specifikációs elem van egy specifikációban. Ekkor ezeket össze lehet vonni egy {} blokkba úgy, hogy a blokk elé tesszük a megfelelő kulcsszót (uses, provides).

Például a következő két specifikáció egyenértékű:
module A1 { uses interface X; uses interface Y; } ... module A1 { uses { interface X; interface Y; } } ...


Amennyiben a specifikációs elem egy interfész példány akkor annak a következő szabályok alapján adhatunk nevet (azaz renamed-identifier-t a következőképpen adhatjuk meg):
renamed-identifier: identifier identifier as identifier


Azaz a teljes leírás interface X as Y. Fontos megjegyezni, hogy itt X egy interfész típusnak a neve, Y pedig ennek a típusnak egy példánya. Amennyiben azt szeretnénk írni, hogy interface X as X, akkor azt rövidíthetjük úgy, hogy interface X.
Az interfészeket paraméterezni tudjuk. Ez azt jelenti, hogy amennyiben megadunk egy paramétert az interfésznek akkor több példány keletkezik egy helyett. Az hogy mennyi pontosan az az interfész paraméter típusától függ. Annyi példány keletkezik, amennyi a paraméter típus „elemszáma”. Tehát például az interface SendMsg S[uint8 t id] deklarációval 256 SendMsg típusú interfész példányunk lesz. Az egyes példányokra hivatkozni pl. S[3]-mal tudunk. Ezek a paramétereknek integrális típusúnak kell lennie, de enum nem lehet. Ezeket az interfész példányokat paraméterezett interfész példányoknak nevezzük. Meg kell jegyezni, hogy paraméterezett csak egy interfész példány lehet, nem pedig egy interfész típus.
A parancsoknak és az eseményeknek is lehetnek interfész paramétereik. Itt is fontos megjegyezni, hogy csak komponensekben adhatjuk meg ezeket, interfészeken belül nem. Amennyiben egy parancs (esemény) egy paraméterezett interfész példány eleme, akkor maga is paraméterezett lesz. Ezeket és az előbb említett paraméterezett parancsokat (eseményeket) paraméterezett parancsoknak (eseményeknek) nevezzük a többit pedig egyszerű parancsoknak (eseményeknek).

Példa:
configuration GenericComm { provides { interface StdControl as Control; interface SendVarLenPacket; //a következő interfészek paraméterezettek interface SendMsg[uint8_t id]; interface ReceiveMsg[uint8_t id]; } uses { event result_t sendDone(); } } ...

Itt a GenericComm :
Fontos a következő definíció (mikor nevezünk egy parancsot/eseményt megvalósítottnak illetve felhasználtnak): Azokat a parancsokat és eseményeket, amelyeket a komponens specifikációs részében felhasználtként vagy megvalósított-ként deklaráltunk azokat természetesen a megfelelő komponens felhasznált illetve megvalósított parancsainak (eseményeknek) nevezzük.

Az interfész példányokban megadott parancsokkal (eseményekkel) kicsit bonyolultabb a helyzet: Az F parancsot a K komponens által megvalósított X interfész példányban a K által megvalósított parancsának nevezzük (jel X.F). Ha F az Y felhasznált interfészben van, akkor pedig a K által felhasznált parancsnak nevezzük (jel Y.F).

Események esetében fordított a helyzet: Az E eseményt egy a K által megvalósított X interfész példányban felhasznált eseménynek nevezzük (jel X.E), amennyiben pedig egy K által használt Y interfész példányban szerepel, akkor megvalósított eseménynek nevezzük(jel: Y.E) .

A modulok és a konfigurációk esetében az implementáció az, ami különbözik.

Modulok implementációja

Tekintsük először a modulok implementációját. A modul egy alap építőegységnek számít. Ezért van az, hogy itt a parancsok és események implementációja valódi kódírást (függvénydefiníciót) jelent.

module-implementation: implementation { translation-unit }

A függvénydefiníciók a translation-unit-ba kerülnek. Ezen kívül itt még más C deklarációk és definíciók is lehetnek (taszkok definíciója, deklarációja, változók definíciója). Az itt deklarált nevek a modul komponens-impementációs hatókörébe tartoznak. Az előző fejezet végén bevezetett definíció segítségével könnyen meg tudjuk fogalmazni, hogy mit kell itt implementálni. Itt kell megvalósítani azokat a parancsokat és eseményeket, amelyek a modul szempontjából megvalósított parancs vagy esemény. A megvalósításban természetesen támaszkodhatunk a felhasznált parancsokra (eseményekre), azaz szabadon meg tudjuk hívni őket. Egy egyszerű, nem paraméterezett parancsot (eseményt) majdnem ugyanúgy implementálunk, mint egy egyszerű C függvényt. A különbség, hogy itt természetesen minősítenünk kell azt megfelelően vagy a command vagy az event kulcsszóval, illetve a függvényneveket közvetlenül kell megadni (direct-declarator), azaz ponttal elválasztva meg kell adni azt a specifikációs elemet, amiben benne van.
direct-declarator: also identifier . identifier direct-declarator parameters ( parameter-type-list )

Nézzünk egy példát:
command result_t Send.send(uint16_t address, uint8_t length, TOS_MsgPtr msg) { ... return SUCCESS; }

Paraméterezett parancsok és események esetében még a név és a paraméterlista között meg kell adni az interfész paramétereket szögletes zárójelben. Például, ha a modul megvalósítja a SendMsg típusú Send[uint8_t id] interfész-pédányokat, akkor az implementációban szerepelnie kell valami hasonlónak (azaz az implementációnak):
command result_t Send.send[uint8_t id](uint16_t address, uint8_t length, TOS_MsgPtr msg) { ... return SUCCESS; }


Futásidejű hiba történik amennyiben a következő kettő közül valamelyik fennáll:
Parancsok és események hívása

A parancsokat a call az eseményeket az signal kulcsszóval tudjuk meghívni:
call-kind: one of call signal post

Például a SendMsg típusú Send interfész példányt használó komponensben meghívhatjuk az interfész send parancsát:
call Send.send(1,sizeof(Message), &msg1);

Paraméterezett parancsoknál az interfész-paraméterek helyén híváskor olyan kifejezéseknek kell lenniük, amelyek automatikus típuskonverzióval átalakíthatóak az interfész-paraméterek típusaira. Például, ha a modul felhasználja a SendMsg típusú Send[uint8_t id] interfész-példányokat akkor a következő hívás helyes lesz:
Az, hogy egy függvényhívásnál pontosan hány komponensben hány függvény fog meghívódni, a modulokat összekapcsoló (a konfigurációban leírt) huzalozástól is függ. Ez egy nagyobb méretű téma, ezért nem kapott helyet ebben a leírásban. Azt azonban meggondolhatjuk, hogy egy komponens által felhasznált interfészt összekapcsolhatunk több, ugyanazt az interfészt megvalósító komponenssel. Ilyenkor egyetlen parancs hívása több függvényt fog elindítani. Bizonyos értelemben nem csak a megvalósított parancsokhoz (eseményekhez) tudunk megadni implementációt. Egy felhasznált parancshoz (eseményhez) meg tudunk adni alapértelmezett implementációt (vigyázat: megvalósított parancsokhoz (eseményekhez) ugyanez fordítási idejű hibát okoz). Ezek az alapértelmezett parancsok (események) akkor hívódnak meg, ha nincs összekapcsolva az általunk hívott parancs (esemény) semmivel.
Alapértelmezett parancs (esemény) implementációját a default kulcsszó segítségével definiáljuk:
declaration-specifiers: also default declaration-specifiers

Például, ha az aktuális modul felhasználja a SendMsg interfészt, akkor
default command result_t Send.send(uint16_t address, uint8_t length, TOS_MsgPtr msg) { return SUCCESS; } /* akkor lehet meghívni, ha a Send interfész nincs semmihez sem kapcsolva */ ... call Send.send(1, sizeof(Message), &msg1) ...


Taszkok

A taszkok is lényegében függvények, bár inkább eljárásnak nevezhetők, hiszen nincs sem paramétere sem pedig visszatérési értéke(azaz void a visszatérési értéke). Arra találták ki, hogy azokat a feladatokat, amelyek aránylag számításigényesek valamint nem kell azonnal végrehajtani, azok kikerüljenek a parancsok és események által irányított fő végrehajtási szálból. Egy parancs illetve egy esemény híváskor azonnal végrehajtódik, szemben egy taszkkal, amely hívás után rögtön bekerül a Main komponens (ez minden alkalmazásban szereplő fő komponens) ütemezőjébe. Ez az ütemező egy FIFO-ként működik. Az ütemező akkor „dolgozza fel” ezeket a taszkokat, amikor éppen nincs semmi dolga (azaz nincs parancs vagy esemény amivel foglalkozni kellene). Ennek a szerkezetnek megfelelően egy taszk végrehajtását egy esemény megszakíthatja, de egy másik taszk indítása nem. Taszkot a következőképpen definiálhatunk: void myTask(){...},indítani pedig a post myTask(); utasítással lehet.
call-kind: one of ... post

Konfigurációk implementációja

Egy konfiguráció lényegében néhány előbb elkészített komponens összehuzalozott halmaza. Egy konfiguráció implementációja egy olyan komponenst épít fel, amely kívülről ugyanúgy viselkedik, mint egy modul, csak belül nincsenek implementálva a szükséges parancsok (események), hanem komponensek vannak összekapcsolva. Tehát egy konfiguráció implementációs részében először meg kell adni azokat a komponenseket, amelyekből a konfiguráció felépül (component-list), majd meg kell adni a köztük lévő huzalozást (connection-list):

configuration-implementation: implementation { component-listopt connection-list }

A továbbiakban külsőnek fogjuk nevezni a konfiguráció specifikációs elemeit, szemben a konfigurációt alkotó komponensek specifikációs elemeivel, amelyeket belső specifikációs elemeknek fogjuk nevezni.
A konfigurációt alkotó komponensek

Nézzük, hogyan adhatjuk meg a konfigurációt alkotó komponenseket:
component-list: components component-list components components: components component-line ; component-line: renamed-identifier component-line , renamed-identifier renamed-identifier: identifier identifier as identifier

Látható, hogy itt is megadhatunk álneveket a rész-komponenseknek ugyanúgy, ahogyan az interfész példányoknak adtunk a specifikációs részben. Érdemes megjegyezni, hogy egy adott komponensből egy programban csak egy van. Azaz csak egyszer fordul elő a memóriában, még ha több konfigurációban használtuk is.

A konfigurációt alkotó komponensek huzalozása

A huzalozást belső és külső specifikációs elemek összekapcsolásához használjuk. Ezek határozzák meg, hogy pontosan milyen konkrét függvények hajtódnak végre egy call vagy signal utasítás végrehajtásakor.
A következő szabályok vonatkoznak a huzalozásra:
connection-list: connection connection-list connection connection: endpoint = endpoint endpoint -> endpoint endpoint <- endpoint endpoint: identifier-path identifier-path [ argument-expression-list ] identifier-path: identifier identifier-path . identifier

Látható, hogy a huzalozó állítások két végpontot (endpoint) kötnek össze.
Háromféle huzalozási állítás van a nesC-ben (ezek kétféle kapcsolatot tudnak kifejezni):
Huzalozásnál interfészt csak interfésszel, parancsot csak paranccsal, eseményt pedig csak eseménnyel lehet összekapcsolni. Az utóbbi két esetben a függvény szignatúráknak meg kell egyezniük, az első esetben pedig a két interfész típusnak kell megegyeznie.
Nézzünk egy példát:
module M1 { provides interface StdControl; } ... module M2 { uses interface StdControl as SC; } ... configuration C { } implementation { components M1, M2; M2.SC -> M1.SdtControl; }


Implicit huzalozás

Az előző példában a C-t másképp is leírhattuk volna:
configuration C { } implementation { components M1, M2; M2.SC -> M1; }

Ennek az az oka, hogy amennyiben pontosan egy olyan elem van az M1-ben, amely megfelelhet az M2.SC-nek akkor nem kell kiírni pontosan az M1-beli specifikációs elemet, hiszen a fordító egyértelműen be tudja azonosítani azt(de csak ekkor).

A huzalozási szemantika

A szemantikát először paraméterezett interfészek nélkül mutatjuk be.
A bemutatáshoz a következő példát használjuk:

interface X { command int f(); event void g(int x); } module M { provides interface X as P; uses interface X as U; provides command void h(); } implementation { ... } configuration C { provides interface X; provides command void h2(); } implementation { components M; X = M.P; M.U -> M.P; h2 = M.h; }


Először vezessük be a közvetítő függvény fogalmát:
Minden egyes komponens minden egyes a parancsához és eseményéhez bevezetünk egy Ia közvetítő függvényt. Pl. a példánkban az M modulnak a következő közvetítő függvényei vannak: IM.P.f, IM.P.g, IM.U.f, IM.U.g, IM.h. A példákban az átmeneti függvényeket a tartalmazó konfiguráció nevével, és a függvény nevével jelöljük. Ha egy interfészben található a függvény, akkor azt is feltüntetjük (a példák többségében ez fordul elő). Egy közvetítő függvény vagy felhasznált, vagy megvalósított. Mindegyik közvetítő függvény ugyanazokkal a paraméterekkel rendelkezik, mint a hozzá tartozó parancs vagy esemény. Egy közvetítő függvény törzse más közvetítő függvények hívásából áll. Ezek azok a függvényekhez tartozó közvetítő függvények, amelyekkel össze van kapcsolva a huzalozási állításokban. Amit egy közvetítő függvény megkap argumentumként, azt változtatás nélkül továbbadja az általa hívott közvetítő függvényeknek. A közvetítő függvény visszatérési értéke egy lista. Ez a lista az általa hívott függvények visszatérési listájának konkatenációja. Nyílván ennek egyszer véget kel érnie: egy modulhoz tartozó függvény közvetítő függvénye egyszerűen csak egy hívást tartalmaz az aktuális függvényre. A függvény eredménye lesz a közvetítő függvény visszatérési értéke (pontosabban egy egy elemű lista). Az előzőek szerint a huzalozási állítások határozzák meg a közvetítő függvények törzsét. Jelöljük I1 <-> I2 -vel azt, ha az I1 –hez tartozó függvény össze van kapcsolva az I2–höz tartozó függvénnyel.(most eltekintünk a = és a -> huzalozási állítások közötti különbségtől.)
A példaprogramunkat tekintve a következőket írhatjuk fel:
   IC.X.f <-> IM.P.f IM.U.f <-> IM.P.f IC.h2 <-> IM.h    IC.X.g <-> IM.P.g IM.U.g <-> IM.P.g

Egy I1 <-> I2 kapcsolatban az egyik a hívó a másik a hívott. Egy ilyen kapcsolat tehát csak annyit ír le, hogy egy hívás a hívotthoz belekerül a hívó törzsébe.
I1 egy hívott, ha a következők közül valamelyik teljesül:
Ha egyik sem teljesül, akkor I1 egy hívó. A meghatározott huzalozási szabályok miatt egy I1 <-> I2 kapcsolat nem kapcsolhat össze két hívót vagy két hívottat. A példabeli C komponensben IC.X.f, IC.h2, IM.P.g, IM.U.f a hívók és IC.X.g, IM.P.f, IM.U.g, IM.h a hívottak. Így C komponens által meghatározott kapcsolatok szerint IM.P.f -re van egy hívás IC.X.f törzsében, az IC.X.g –re pedig IM.P.g törzsében, stb.
Egy call a(e1, . . . , en) kifejezés tehát a következőképpen értékelődik ki: A signal kifejezések kiértékelése ugyanígy történik.
Lássunk egy példát a közvetett függvényekre:
list of int IM.P.f() { return list(M.P.f()); } list of int IM.U.f() { return IM.P.f(); } list of int IC.X.f() { r eturn IM.P.f(); } list of void IC.h2() { return IM.h(); } list of void IM.P.g(int x) { list of int r1 = IC.X.g(x); list of int r1 = IM.U.g(x); return list concat(r1, r2); } list of void IM.U.g(int x) { return list(M.U.g(x)); } list of void IC.X.g(int x) { return empty_list; } list of void IM.h() { return list(M.h()); }


Megjegyzés: itt list(X) egy X –et tartalmazó egyelemű lista, concat(r1, r2) összekapcsolja az r1 és r2 listákat, empty_list pedig az üres lista.

A paraméterezett függvények és a huzalozás.

Ha a K komponens a eseménye vagy parancsa paraméterezett a t1, t2, … ,tn interfész paraméterekkel, akkor minden (v1:t1, … vn:tn) n-eshez létezik egy Ia,v1,...,vn közvetítő függvény . A modulok esetében ha az Iv1,...,vn közvetítő függvény egy paraméterezett , megvalósított parancshoz vagy eseményhez tartozik, akkor a függvény meghívása a v1, . . . , vn interfész paraméterekkel történik.
A call a[e’1, . . . , e’m](e1, . . . , en) hívás kiértékelése a következőképpen történik:
A signal kifejezések is ugyanígy értékelődnek ki.

Alkalmazás szintű követelmények

A következő két alkalmazás szintű követelménynek kell megfelelniük a huzalozási állításoknak, különben fordítási idejű hiba keletkezik: