A Lua egy beágyazott nyelv, nem önálló alkalmazásként való futtatásra tervezték, hanem csatolható könyvtárként, egy kiszolgáló (host) alkalmazásba integrálásra.
Az eddigi leírásokban bemutatott lua interpreter is egy ilyen kiszolgáló alkalmazás. Maga az interpreter tulajdonképpen egy párszáz soros C alkalmazás, ami beolvassa a felhasználói bemeneteket, fájlokat és stringeket, és ezt továbbadja a Lua könyvtárnak.
A könyvtárként való használat az, ami kiegészítő nyelvvé teszi a Lua-t. Emellett a Lua-t használó programoknak lehetőségük nyílik új függvényeket hozzáadni a Lua környezethez. Ezeket a függvényeket C-ben implementálhatjuk, vagy akár más C kompatibilis nyelvben (például Pascalban), és segítségükkel olyan lehetőségeket adhatunk a nyelvhez amit Lua kódban nem lehetne megvalósítani. Ez a Lua nyelvet bővíthetővé teszi.
Ez a két nézete a nyelvnek (a kiegészítő és bővíthető nyelv) két különböző C - Lua interakciót kíván. Az első nézet során a C kód irányítja a Lua könyvtárat és hívja meg az egyes függvényeit. A C kódot ebben az interakcióban alkalmazás kódnak hívjuk. A második nézet során a Lua környezet irányítja a C kódot, ilyenkor a C kódot könyvtári kódnak hívjuk. Mind az alkalmazás, mind a könyvtári kód ugyanazt az API-t hívja a Lua környezettel való kommunikáláshoz. Ezt a Lua C API-nak hívjuk.
Ez a fejezet a Lua C API-t mutatja be, ami kulcsfontosságú szerepet töltött be a Lua sikerességében. A továbbiak megértéséhez feltételezzük, hogy az olvasó legalább alapszintű ismeretekkel rendelkezik a C nyelvről.
Elsőnek egy egyszerű Lua interpreteren keresztül mutatjuk be a Lua C API használatát.
A fenti C kód először include-olja megfelelő header fájlokat a Lua-ból. A lua.h definiálja az alapvető Lua függvényeket. Ezekkel a függvényekkel meghívhatunk Lua kódokat (lua_pcall), módosíthatunk globális változókat, regisztrálhatunk új függvényeket, stb. A lauxlib.h header fájl a kiegészítő könyvtár függvényeit definiálja. Ezek egy magasabb szintű absztrakciót adnak a lua.h függvényeit felhasználva. A kiegészítő könyvtár függvényei mindig luaL_ előtaggal kezdődnek, az alap könyvtár függvényei pedig lua_ előtaggal.
A harmadik, lualib.h header fájlban lévő függvények felelősek a Lua környezet könyvtárainak betöltéséért. Ezek regisztrálják a globális névtérből alapértelmezésben elérhető print függvényt, io táblát, stb. Amennyiben nem akarunk a futtatott Lua kódnak minden könyvtárhoz hozzáférést adni betölthetjük őket külön-külön is a luaopen_base, luaopen_table, stb. függvények segítségével.
A luaL_newstate() függvény segítségével létrehozunk egy Lua futtató környezetet, ami Lua futtatásához szükséges összes adatot tárolni fogja és a Lua C API minden függvényének át kell adni. Ezután betöltjük az alap Lua könyvtárakat, hogy egy teljes Lua környezetet tudjunk biztosítani.
A környezet inicializálása után felhasználói bemenetet várunk. Minden beírt sort betöltünk a Lua verembe a luaL_loadbuffer függvénnyel majd meghívjuk a Lua környezetet védett módban a lua_pcall függvénnyel. Amennyiben hibát jelezett valamelyik, akkor kiírjuk a hibaüzenetet és töröljük a veremből.
Vegyük észre, hogy a hibaüzenetet a program a standard error kimenetre írja. A hibakezelés elég összetett is lehet a C nyelvben. A Lua függvények nem írnak közvetlenül semmilyen kimenetre, helyette a hibákat visszatérési kódokkal/üzenetekkel jelzi. A használó alkalmazások ezeket a számukra legmegfelelőbb módon kezelhetik. Később kitérünk majd a hibakezelés megvalósítására az alkalmazás kódban.
A Lua-t fordíthatjuk C és C++ fordítóval is, de a lua.h nem tartalmazza a C könyvtárakban már megszokott makrót:
Ezért, ha C++ fordítóval fordíjuk a kódunkat, használjuk a lua.hpp header fájlt.
A Lua és a C nyelv közötti adatcserénél két nyelv eltérő típusrendszere és memória foglalási stratégiája miatt problémákba ütközünk. Ezeket az adatcsere problémákat a Lua C API egy absztrakt verem használatával oldja meg. A verem rekeszekre van osztva, minden rekesz egy tetszőleges Lua értéket tartalmazhat. Ha szükségünk van a Lua környezet valamely értékére, akkor a megfelelő hívással a Lua környezetet utasíthatjuk, hogy rakja a verembe. Hasonlóan, ha egy Lua kódnak akarunk átadni értékeket, akkor a megfelelő push függvény meghívásával a Lua ezt a verembe teszi. Szinte az összes API függvény a vermet használja.
A Lua környezet ezt a vermet LIFO eléréssel használja (Last In, First Out), amikor meghívjuk a Luat az mindig csak a verem tetejét változtatja. A C kódnak ennél nagyobb szabadsága van a veremkezelésben, tetszőleges helyen olvashat vagy módosíthat elemeket.
Az API minden típushoz biztosít lua_push* függvényeket, amikkel a verem tetejére helyezhetjük a megfelelő C típusú értékeket. A lua_pushnil függvény nil értéket, a lua_pushnumber egy double típusú számot, a lua_pushboolean egy logikai értéket, a lua_pushlstring tetszőleges stringet, a lua_pushstring pedig null terminált stringet rak a verem tetejére.
A verem kezelése során a C kód felelős a verem méretének kezeléséért. Lua indításakor, és bármikor amikor a Lua a C nyelvi függvényeket hívja garantálja, hogy legalább 20 szabad rekesz elérhető lesz a verem tetején. Amennyiben ez nem elég a C kód számára a következő függvénnyel beállíthatjuk a kívánt stack méretet.
A Lua verem egyes rekeszeinek eléréshez az API indexeket használ. A verem első eleme (amelyik legelőször került a verembe) az 1 -es indexen található, a következő elem a 2 -es indexen, és így tovább. A verem tetején lévő, utoljára berakott elemeket negatív indexekkel érhetjük el egyszerűen. Negatív indexek esetén a -1 -es index a verem legtetején lévő (legutoljára bekerült) elemet éri el, a -2 az előtte bekerült elemet, és így tovább. Például a lua_tonumber(L, -1) a verem tetején lévő számot adja vissza.
A verem adott rekeszének típusát a lua_is* függvényekkel kérdezhetjük le:
A verem egy rekeszének értékét a lua_to* függvényekkel kérdezhetjük le:
int lua_gettop (lua_State *L); | A verem tetején lévő elem indexét adja vissza |
void lua_settop (lua_State *L, int index); | Beállítja a verem tetejét a megfelelő indexre |
void lua_pushvalue (lua_State *L, int index); | Az adott indexen lévő elemet a verem tetejére másolja |
void lua_remove (lua_State *L, int index); | Az adott indexű elemet kiveszi a veremből és a többi elemet lejjebb csúsztatja |
void lua_insert (lua_State *L, int index); | Az adott indexre áthelyezi a legfelső elemet és a többi elemet feljebb csúsztatja |
void lua_replace (lua_State *L, int index); | A felső elemet megcseréli az adott indexen lévővel, csúsztatás nélkül |
A lua_gettop függvény a veremben található elemek számával tér vissza, ami egyben a legfelső elem indexe is. A lua_settop fügvénnyel tudjuk a verem tetejére mutató indexet beállítani. Ha az előző index nagyobb volt mint az új, akkor a fölösleges elemeket eldobjuk, ha viszont kisebb volt, a függvény nil-ekkel tölti fel az üres helyeket. Ha például ki akarjuk üríteni a vermet, használhatjuk a lua_settop(L,0) függvényt. Negatív számokat is megadhatunk paraméternek. Például az API által nyújtott lua_pop függvény megvalósítása:
A lua_pushvalue függvény a veremnek a paraméterben megadott indexű elemének a másolatát a verem tetejére másolja. A lua_remove eltávolítja a paraméterben megadott indexű elemet a veremből és ha az adott index felett találhatóak még elemek, akkor azokat lecsúsztatja eggyel. A lua_insert a verem tetején lévő elem másolatát a megadott indexre beszúrja, úgy hogy előtte elcsúsztatja a veremben az index felett lévő elemeket eggyel. A lua_replace a verem legfelső elemét az adott indexre másolja. Vegyük észre, hogy a következő műveleteknek semmilyen hatása nincs a virtuális vermen:
A C nyelvű alkalmazások kiegészítésekor a Lua-t az alkalmazás működését kiegészítő scriptek írására és konfigurációs fájlok nyelveként is használják.
Nézzünk egy példát egy szerver alkalmazás pár beállításának betöltésére:
Ezt a fájlt közvetlenül átadhatjuk a Lua-nak és a fájl végrehajtása után kiolvashatjuk a globális változók értékét:
A fenti kód létrehoz egy új Lua környezetet majd betölti a standard Lua könyvtárakat és lefuttatja a megadott fájlt a luaL_dofile függvénnyel. Miután a kódrészlet befejezte a futást lekéri a megfelelő globális változókat a verembe és ellenőrzi, hogy jó-e a típusuk. Ha megfelel a típus, akkor kiolvassa őket és kiírja.
A Lua táblák mezőit a C nyelvből a megfelelő tábla és kulcs verembe helyezésével tudjuk elérni. A következő kódrészlet betölti a person tábla age mezőjét, a lua_gettable függvényt használva:
A Lua függvények hívása a többi művelethez hasonlóan a vermen keresztül történik. Először a függvény objektum kerül a veremre, majd a paraméterei. A függvény hívást a lua_pcall fügvénnyel végezhetjük el, ez kiveszi a veremből a függvényt és a paramétereket, majd a verem tetejére rakja a visszatérési értékeket.
A Lua képes meghívni megfelelő prototípusú és működésű C függvényeket, amiket a alkalmazás kód regisztrált a Lua környezetbe. A meghívott C függvény a Lua vermen kapja meg a paramétereit és az eredményeket is vermen adja vissza. Minden hívott C függvény egy privát Lua vermet kap, ahol mindig az 1 -es indextől kezdődnek a paraméterek.
A lenti kódrészlet egy Luaból hívható C függvényt implementál, az egyszerűség kedvéért ez egy 1 paraméteres függvény lesz, ami a paraméter szinuszát adja vissza. Ezt a függvényt a lua_pushcfunction és lua_setglobal függvény segítségével regisztrálja.
A függvények regisztrálására egy elegánsabb módszer ha a standard könyvtárokhoz hasonló táblába töltjük be a függvényeinket. Ehhez egy a következő kódot használhatjuk:
Ellentétben a C++-al vagy a Java-val, a C nyelv nem nyújt támogatást a hibakezeléshez. Ezt kiküszöbölendő a Lua a C setjmp képességét használja, amivel a kivételkezeléshez hasonló működést valósít meg.
Minden struktúra Lua-ban dinamikus, azaz növekszik ha szükség van rá, vagy csökken ha lehetséges. Ez azt jelenti, hogy a memória foglalási hibák kiterjednek a Lua-ra is. Ahelyett hogy hibakódokat használna a Lua minden műveletnél, inkább kivételeket használ, hogy jelezze a hibákat. Ez azt jelent, hogy majdnem az összes API függvény dobhat hibát, ahelyett hogy visszatérne.
Amikor alkalmazás kódot írunk (ahol a C hívja a Lua-t), a mi felelősségünk hogy elkapjuk ezeket a hibákat.
Jellemzően az alkalmazás kód unprotected módon fut. Mivel a Lua az alkalmazás kódhoz nem fér hozzá, így elkapni sem tudja a kivételeket. Ilyenkor, ha például egy művelet memóriát akar foglalni és nincs elég hely a Lua nem tudja elkapni ezt a hibaüzenetet. Helyette egy pánik függvényt hív és ha ez visszatér az alkalmazás terminál. Megadhatunk saját pánik függvényt a lua_atpanic segítségével.
Nem minden API függvény dob kivételt. A luaL_newstate, lua_load, lua_pcall és lua_close függvények biztonságosak. A legtöbb függvény csak akkor dob kivételt, ha nincs elég hely a memóriában allokáció során például a luaL_loadfile kivételt dob ha nincs elég memória egy fájl nevének tárolására. A legtöbb program nem tud mit tenni, ha kifogy a memóriából, így figyelmen kívül hagyható ez a kivétel.
Ha azt szeretnénk, hogy ne termináljon a programunk memória allokációs hiba esetén sem két lehetőségünk van. Az első, hogy beállítunk egy pánik függvényt ami nem adja vissza a Lua-nak a vezérlést. A második, hogy védett(protected) módban futtatjuk a programunk.
A legtöbb alkalmazás a lua_pacall függvényhíváson keresztül futtat Lua kódot, ezért a Lua kód védett módban fut. Ha memória allokációs hiba keletkezik a lua_pcall egy hibakóddal tér vissza így az interpretert egy konzisztens állapotban hagyja. Ha szeretnénk az alkalmazás kódunkat is biztonságossá tenni, akkor a lua_cpcall függvényt hívjuk meg. Ez ugyanúgy működik mint a lua_pcall, de paraméterként kap egy C függvényt, ami akkor hívódik meg ha memória foglalási hiba történt miközben egy Lua függvényt akartunk a verembe tenni.
Egy fontos alkalmazási területe a Luának a konfigurációs nyelvként való használata. Ebben a fejezetben bemutatjuk hogyan használhatjuk a Lua-t arra, hogy egy programot konfiguráljunk. Először egy egyszerű példával kezdünk, majd ezt egészítjük ki sokkal komplexebb feladatok megoldására alkalmas módon.
Először is képzeljünk el, egy egszerű konfigurációs feladatot: a programunk egy ablakot jelenít meg és azt akarjuk, hogy a felhasználó tudja állítani a kezdeti méretét. Persze ilyen egyszerű feladatot egyszerűbben is meg lehet oldani, nem szükséges Lua-t használni hozzá. Mi most Lua-t fogunk használni, ekkor a konfigurációs fájlunk a következő.
Most a Lua-t arra kell utasítanunk, hogy olvassa be a fájlt és elemezze, majd a globális width és height változókat el kell kérnünk tőle. Ezt a load függvénnyel tudjuk megtenni. Ha létrehoztunk már egy lua state-et, akkor a luaL_loadfile függvénnyel tudunk egy fájlt betölteni a Lua-ba, majd hívhatjuk a lua_pcall függvényt. Ha hiba történt az elemzés során például szintaktikai hiba volt a fájlban, akkor az előbbi függvények egy hibakóddal térnek vissza amit a virtuális verembe tesznek. Ezután a programunkban a lua_tostring függvényt a -1 paraméterrel hívva megkaphatjuk a verem tetején lévő hibaüzenetet.
Ahhoz, hogy megkapjuk a globális változókat kétszer meghívjuk a lua_getglobal függvényt aminek paraméterként a változó nevét adjuk meg. Ezzel a verembe kerül a két változónk a -2 indexen a width a height pedig a -1-esen. Ezután megvizsgáljuk, hogy a veremben lévő érték az szám típusú-e lua_isnumber. Ha igen akkor meghívjuk a lua_tointeger függvényt, hogy átkonvertáljuk az értékeket, majd a megfelelő változókban eltároljuk őket.
Tényleg megéri Lua-t használni ilyen egyszerű feladatokra? Ahogyan az előzőekben már említettük, ilyen egyszerű feladatokra egyszerűbb lenne csak beolvasni két számot egy szöveges fájlból. Viszont a Lua használatának megvan az az előnye, hogy kezeli a szintaktikus hibákat helyettünk, a kommenteket, valamint sokkal összetettebb konfigurációkat is megadhatunk. Például bekérhet a felhasználótól adatokat, vagy rendszerváltozókból olvashat be értékeket:
Még ilyen egyszerű esetekben is nehéz mérlegelni, hogy mit akar a felhasználó, de amíg ez a két változó definiált a scriptben addig a C alkalmazásunk változtatás nélkül működni fog.
Végül nem elhanyagolható érv a Lua használata mellett az sem, hogy könnyű új konfigurációs fájlokat hozzáadni a meglévő programunkhoz, ami sokkal flexibilisebbé teszi a programunkat.
Tegyük fel, hogy most az alkalmazásunk háttérszínét szeretnénk konfigurációs fájlból beolvasni. A színeket három számmal reprezentáljuk, ahol minden szám egy szín komponenst jelent RGB-ben. C-ben ezeket a számokat integer-rel ábrázoljuk [0,255], mivel Lua-ban minden szám valós típusú ezért használjuk a [0,1] tartományt.
Egy naív megközelítése a feladatnak az, hogy elvárjuk hogy a konfigurációs fájlban a szín komponensek külön globális változóban legyenek megadva:
Ennek a megközelítésnek két hátránya is van, az egyik, hogy az alkalmazásunknak több színre lehet szüksége így rengeteg globális változóra lenne szükség, a másik, hogy így a felhasználó nem tud gyakran használt színeket előre definiálni (background=WHITE). Ahhoz hogy ezt elkerüljük, táblákat fogunk használni a színek reprezentálására:
A táblák használata strukturáltabbá teszi a scriptünket, így kényelmesebb lesz később használni az előre definiált színeket:
Hogy ezeket az értékeket kiolvassuk a C kódunkban a következőket kell tennünk:
Először lekérjük a background globális változót, majd megvizsgáljuk, hogy az egy tábla-e. Ezután lekérjük a táblában lévő színkomponenseket. Mivel a getfield függvény nem az API része ezért definiálnunk kell.
A Lua API egy függvényt lua_gettable biztosít az összes típushoz. Ez a függvény leveszi a verem tetejéről a kulcsot, majd ez alapján visszateszi a táblában található értéket a verembe. Az általunk definiált getfield feltételezi, hogy a tábla a virtuális verem tetején van, így miután letettük a kulcsot a verembe a lua_pushstring függvénnyel a tábla a -2-es indexen lesz a veremben. Mielőtt a függvény visszatérne, leveszi a verem tetején található értéket, így a vermet ugyanúgy hagyja ahogyan megkapta.
Mivel a táblák stringekkel való indexelése nagyon gyakori, ezért a Lua 5.1-ben bevezették a lua_gettable függvény egy speciális változatát erre az esetre: lua_getfield. Ezt a függvényt felhasználva lecserélhetjük ezt:
erre:
Tovább fogjuk bővíteni az alkalmazásunkat és bevezetjük a színek neveit. A felhasználók továbbra is használhatják a színtáblákat, de használhatnak előre definiált színeket is a gyakori színekhez. Ehhez a lehetőséghez implementálnunk kell egy színtáblát a C programunkban:
Ahhoz, hogy egy tábla mezőinek az értékeit beállítsuk definiálhatjuk a következő függvényt, ami leteszi a verembe az indexet és a mező értéket, majd meghívja a lua_settable függvényt:
Mint a többi API függvény a lua_settable is több típusra működik. Megkapja a tábla indexét majd a veremből kiolvasva a kulcsot és értéket módosítja a táblát. A setfield függvény feltételezi, hogy a tábla van a verem tetején amikor meghívták.
A Lua 5.1 bevezeti a lua_settable egy specializált változatát a lua_setfield-et. Ezt az új függvényt használva átírhatjuk az előző definícióját a setfield-nek a következőre:
A következő setcolor függvény egy színt definiál. Létrehoz egy táblát, beállítja a mezőit, majd ezt értékül adja a megfelelő globális változónak:
A lua_newtable függvény létrehoz egy új táblát és a verembe helyezi azt. A setfield hívások beállítják a tábla mezőit, végül a lua_setglobal kiveszi a táblát a virtuális veremből és értékül adja a megfelelő nevű globális változónak.
Az előzőekben definiált függvényekkel a következő ciklusban beállítjuk az összes színt a konfigurációs script számára:
Vegyük észre, hogy az alkalamazás először a ciklust hajtja végre mielőtt futtatná a scriptet.
Más lehetőségünk is van a színek reprezentálására, például a scriptekben szövegként background="BLUE". Így a background lehet tábla is vagy szöveg. Ezzel az implementációval a programnak nem kell semmit csinálnia mielőtt futtatja a lua scriptet. Cserébe nehezebb egy színt meghatározni. Meg kell vizsgálni hogy a background változó string típusú-e és ha igen, akkor a színtáblából kikeresni a stringhez tartozó színt.
Melyik a legjobb választás? C programokban stringekben tárolni ehhez hasonló értékeket nem ajánlott, mert a fordító nem veszi észre az elírásokat. Lua-ban a globális változóknak nincs szüksége deklarációra, így a Lua nem fog semmilyen hibát jelezni ha a felhasználó elírta egy szín nevét. Ha például a felhasználó WITE-ot ír WHITE helyett a background változó értéke nil lesz. Semmilyen információnk nincs arról, hogy mi a hiba. Másfelől ha elgépeljük a stringeket akkor ezt az alkalmazásunkban felismerhetjük és a hibaüzenetben kiírhatjuk. Az az előnyük is megvan, hogy a felhasználó írhatja bárhogy a színek neveit "white", "WHITE" vagy "White", ha az összehasonlítás során a kis- és nagybetű érzékenységet kikapcsoljuk. Továbbá ha a Lua script kicsi és sok színt tartalmaz eléggé zavaró lehet színek százait definiálni, csak azért hogy a felhasználó csak néhányat használjon. Stringek használatával ez elkerülhető.
Egy nagyszerű képessége a Lua-nak, hogy a konfigurációs fájlban definiálhatunk függvényeket, amiket aztán hívhatunk az alkalmazásukból. Például írhatunk egy alkalmazást ami kirajzolja egy függvény képét és ezt a függvényt definiálhatjuk Lua-ban.
Lua függvények hívása az API-n keresztül igen egyszerű, csak le kell raknunk a virtuális verembe a függvény nevét, majd a paramétereit, aztán meghívjuk a lua_pcall függvényt, végül kiolvassuk a függvény visszatérési értékét a veremből. Tegyük fel, hogy adott a következő konfigurációs script egy függvénnyel:
Ki akarjuk értékelni C-ben a következő kifejezést z=f(x,y) adott x és y-ra. Feltéve, hogy már van egy nyitott Lua példányunk a következő C függvénnyel tudjuk meghívni az f Lua függvényt:
Az f függvényt a lua_pcall függvényhívással hajtjuk végre a második paraméterben az f függvény paramétereinek számát adjuk meg, a harmadik paraméterben a visszatérési értékek számát, az utolsó paraméterben pedig a hibakezelő függvényt adhatjuk meg. Attól függóen hogy harmadik paraméternek mit adtunk meg a Lua az üres helyeket nil-el tölti fel a veremben, vagy ha a visszatérési értékek száma több mint amennyit argumentumban megadtunk, akkor azokat eldobjuk. Mielőtt a függvény eredményét a verembe tenné a lua_pcall kiveszi onnan a függvény paramétereit és a nevét. Ha egy függvény több értékkel tér vissza az első érték kerül először a verembe, így ha pl.: három értéket ad a függvény, akkor az első érték a -3-as indexen lesz az utolsó pedig a -1-esen.
Ha hiba történik a lua_pcall hívása során, akkor a verembe egy nullától különböző hibakódot tesz, a hibaüzenettel együtt (persze ekkor is kiveszi a veremből a hívott függvény argumentumait és nevét). Mielőtt a verembe tenné a hibaüzenetet meghívja a hibakezelő függvényt ha megadtuk a negyedik paraméterben. Az átlagos hibák jelzésére a LUA_ERRRUN szolgál.
Egy sokkal összetetteb példaként nézzük hogyan írhatunk egy wrappert a Lua függvényekhez a C változó argumentumlista mechanizmusát használva. A wrapper függvényünket nevezzük call_va-nak, ami megkapja a függvény nevét amit meg kell hívnia, egy stringet ami leírja az argumentumok és visszatérési értékek típusát, az argumentumokat, és végül pointerek listáját amik a visszatérési értékeket tároló változókra mutatnak. Ezzel a függvénnyel az előbbi példánk a következőre egyszerüsödik:
ahol a "dd>d" jelentése két double típusú paraméter és egy double típusú visszatérési érték. Ebben a formátum stringben a double típust a 'd', az int típust az 'i' és a string típust az 's'-el jelöljük. A paraméterek típusait a visszatérési értékek típusaitól a '>' választja el. Ha nincs visszatérési érték, akkor a '>'-t nem kötelező kiírni.