Típusbiztos szkriptnyelvek generálása funkcionális beágyazott nyelvekből Horváth Gábor, Kozár Gábor, Szűgyi Zalán 2014. február 7. 1. Bevezetés Ez a cikk az Informatics 2013 konferencián publikált Generating typesafe script languages from functional APIs cikkünk átdolgozása és lefordítása magyarra. A fordított nyelvek számos előnnyel rendelkeznek az értelmezett nyelvekkel szemben. Többek között általában az ilyen nyelven megírt programok hatékonyabbak és a fordító fordítási időben több potenciális hibát talál meg. Ugyanakkor a forráskód lefordítása gyakran jelentős időt vehet el a fejlesztőktől, ezáltal hátráltatva a munkát. További problémát jelent, hogy ahhoz, hogy egy módosítást le lehessen tesztelni, újra kell indítani magát a programot is. Ez a folyamat gyakorta még több időt igényel, mint a fordítás. Egy lehetséges megoldás a dinamikus könyvtárak használata lenne, ugyanakkor a dinamikus könyvtárak kicserélése sem triviális feladat, miközben fut a program. Ráadásul ez a megoldás nagyban függne az adott platformtól is. Az értelmezett nyelvek esetében az értelmező gyakran a típusellenőrzést csak közvetlen egy adott kódrészlet végrehajtása előtt végzi el. Ez a programozók számára nagyobb flexibilitást nyújt, azonban ez azt jelenti, hogy a futó programban lehetnek rosszul típusozott részek is. Ugyanakkor, mivel nincs szükség a forráskód gyakori újrafordítására, ezért a fejlesztők gyors 1
fejlesztés-tesztelés iterációkat tudnak végezni. Viszont éppen azért, mert kevesebb garanciát nyújt az értelmező a kód helyességével kapcsolatban, mint a statikusan típusozott nyelvek esetén, ezért a fejlesztőknek még nagyobb hangsúlyt kell fektetniük a tesztelésre. Sajnos az értelmezett nyelvek nem elég hatékonyak ahhoz, hogy rendszerprogramozási nyelveknek használjuk őket. Bár léteznek hibrid megoldások, mint például a szkript előfordítása byte kódra, vagy a Just In Time fordítási modell, de ez a cikk nem ezekről a megoldásokról szól. Igen gyakori, hogy egy alkalmazás esetén a teljesítmény szempontjából kritikus részeket egy fordított és statikusan típusozott nyelvben írják, majd egy API-t készítenek egy szkriptnyelv számára, amivel egyszerűen és gyorsan lehet a kevésbé teljesítmény kritikus részeket lefejleszteni. Sajnos ennek az API-nak az elérhetővé tétele a szkriptnyelv értelmezője számára gyakran nagyon sok munkát jelent. Emellett az elkészült szkriptek tesztelésére is rengeteg időt kell fordítani. Ez a cikk egy olyan megoldásról szól, ami segítségével jelentősen csökkenthető az előbb említett tevékenységekre fordított idő. 1.1. Motiváció A Clang [4] egy modern C/C++/Obj-C fordító, ami moduláris felépítésű. Úgy tervezték meg, hogy könnyen lehessen különböző eszközöket fejleszteni ezekhez a programnyelvekhez a fordító könyvtárainak a felhasználása segítségével. Az egyik ilyen könyvtár egy beágyazott domain specifikus nyelvet kínál a felhasználó számára [2][3][5]. Ezt a nyelvet AST- Matcher nek hívják. Ennek a nyelvnek a segítségével mintákat lehet definiálni, amiket később a programkód szintaxis fájára lehet illeszteni. Ez a nyelv funkcionális megközelítést alkalmaz. A C++ az egyik legösszetettebb gyakorlatban is használt programnyelv. Ezt a bonyolultságot a C++ programok szintaxis fája természetes módon tükrözi. Ebből kifolyólag, ha egy bizonyos mintát akarunk megtalálni ezeken a szintaxis fákon, akkor elég kicsi a valószínűsége, hogy első próbálkozásra a megfelelő mintát fogjuk leírni. Ebből kifolyólag egy tipikus munkafolya- 2
mat, hogy egy a mintán apró változtatásokat végzünk, majd kipróbáljuk azt. Sajnos a C++ kódok elemzése elég sokáig tart, ráadásul az alkalmazást ami az illesztést végzi újra kell fordítani a minta minden egyes módosítása után. Ebből kifolyólag a folyamat, ameddig sikerül a megfelelő mintát meghatározni, szükségtelenül sokáig tart. Ezt a problémát megoldandó egy olyan értelmezett környezetre lenne szükségünk, ahol egyből megkapjuk a visszajelzést, hogy a minta mennyire felelt meg a követelményeknek [9]. Az értelmező felelős azért, hogy elemezze a mintát, amit kap, majd lefordítsa ezt ASTMatcherek formájában megfogalmazott mintára. Ugyanakkor fontos, hogy az illegális lekérdezések ne kerülhessenek végrehajtásra, mivel ez esetben a környezet leállhat, és az újraindításnál a fejlesztő kénytelen újra kivárni a kódok elemzésével eltöltött időt. Emellett a Clang fordító gyorsan változik, ezért nem ritka, hogy a ASTMatcher könyvtár elemei jönnek és mennek. Ezért az értelmező által elfogadott nyelv nyelvtanját is folyton változtatni kellene. A cikkben bemutatott megoldás célja az, hogy az értelező által elfogadott szkriptnyelv nyelvtanát generáljuk le automatikusan a típusinformációkból, ezáltal nem szükséges a programozóknak lekövetnie a szkript nyelvben az ASTMatcher könyvtárban bekövetkezett változásokat. Emellett a szkriptnyelvben ne legyen lehetséges illegális minták megfogalmazása. Számos alternatíva létezik ugyan, amivel szkriptnyelvek számára elérhetővé lehet tenni egy API-t. Ilyen például a Simplified Wrapper and Interface Generator (SWIG) [12] is. Azonban ezek az eszközök nem kezelik jól a C++ nyelvben megtalálható template-eket. A template-ek viszont gyakran alapjául szolgálnak a beágyazott nyelveknek, ezért a már létező megoldások nem alkalmasok erre a feladatra. 1.2. C++ és a Template Metaprogramozás A bemutatott megoldás C++-ban [7] került megvalósításra, és sok metaprogramot tartalmaz. A C++ template-ek egy Turing-teljes beágyazott nyelvet alkotnak a C++-ban. A template-ek segítségével megírt programok a C++ kód fordítása közben futnak le. Ez az alapja a nyelvben a 3
generatív programozásnak. Teljes szoftverkomponensek kigenerálása is lehetséges, például akár értelmezők generálása is. A generatív programozásra nem csak a C++ alkalmas, tehát minden itt bemutatott módszer átvihető bármely nyelvre, ahol ugyanezek az eszközök rendelkezésre állnak. Sőt, bizonyos részei a metaprogramoknak kiválthatóak reflexió segítségével is. Az alapötlet az, hogy minden típusinformáció a fordító rendelkezésére áll, ismeri a konverziós szabályokat. A template specializációkkal történő mintaillesztés és a Substitution Failure Is Not An Error (SFINAE) [8] technika segítségével lehetséges ezeknek a konverziós szabályoknak a megállapítása és eltárolása fordítási időben. Ezekből a konverziós szabályokból állítható elő az a nyelvtan, amit az értelmező el fog fogadni. Bár ebben a megoldásban nem generáljuk ki az értelmező teljes elemzőprogramját, az is lehetséges lenne [6]. 1.3. Architektúra áttekintése Az eszköz, amit kifejlesztettünk, egy értelmezett lekérdezőnyelven alapszik, amivel lekérdezéseket lehet megfogalmazni a C++ kódbázisra. A lekérdezések gyakorlatilag minták, amiket a C++ kód szintaxisfájára próbálunk illeszteni. Az architektúra nagy vonalakban az 1. ábrán megtekinthető. Az eszköz bemenete egy ASTMatcher kifejezés szövegként. Az értelmező elemzője elkészíti a szintaxis fáját a lekérdezésnek magának. A lexikális és szintaktikus hibák ebben a fázisban derülnek ki. Ezután a szemantikus analízis jön, ezt a részt végző kódot generáljuk ki metaprogramok segítségével. Itt derül ki, ha egy lekérdezés nem jól formált. Ezután kódgenerálás helyett a tényleges ASTMatcherek legenerálása történik, amit utána a végrehajtó egység fog végrehajtani (elvégezni a mintaillesztést). A végrehajtó egység egyik bemenete az ASTMatcher kifejezés, a másik pedig a C++ kód szintaxis fája. A szemantikus analízisért felelős modul típusbiztos automatikus kiegészítésre is rendkívül egyszerűen felhasználható. 4
1. ábra. Architektúra 2. Implementáció A megoldásunk függvényeket és funktorokat (függvényként viselkedő osztály) tud kezelni. A Clang [4] ASTMatcher könyvtárában a kifejezések [10] általánosított parancs mintát [1] megvalósító funktorokból épülnek fel. Ahhoz, hogy az értelmező végre tudja majd hajtani az értelmezett kifejezést, valahogy példányosítania kell ezeket a függvényobjektumokat. Ehhez a futási idejű polimorfizmust használja fel. Ehhez azonban a funktoroknak egy öröklődési hierarchiában kell lenniük. Amennyiben nincsenek, a metaprogramnak mesterségesen kell becsomagolnia a funktorokat egy ilyen hierarchikus szerkezetbe. Sok problémát okoznak a túlterhelt függvények, ugyanis név alapján nem lehet eldönteni ebben az esetben, hogy pontosan melyik függvényről van szó. Ezekben az esetekben egy konverzió segítségével lehet meghatározni a pontos függvényt, ugyanakkor ennek a szintaxisa nagyon sok gépelést igényel. 5
2.1. Karakterláncok a metaprogramokban Az első kihívás a karakterláncok kezelése. Ugyanis technikai okokból kifolyólag egy karakterlánc az nem legális template paraméter. A probléma az, hogy a szemantikus ellenőrző név alapján tudja csak azonosítani a függvényeket, ezért valahogy a metaprogramban mégis csak muszáj kezelni a karakterláncokat. Az erre kidolgozott módszerünk Sinkovics Ábel munkáján [11] alapszik. Az ő megoldását viszont módosítani kellett, mivel nálunk jelentős szempont volt, hogy minél egyszerűbben tudjunk fordítási idejű karakterláncból futási idejű objektumot generálni. Ábel megközelítésének a lényege az volt, hogy egy makró metaprogram segítségével feldarabolja a karakterláncot karakterek sorozatára, és a fel nem használt karakterek helyére nullákat rak. # define DO(z, n, s) at(s, n), # define _S(s) \ BOOST_PP_REPEAT ( STRING_MAX_LENGTH, \ DO, s) template <int N> constexpr char at( char const (&s)[n], int i) { return i >= N? \0 : s[i]; } Ő a Boost MPL metaprogramozáshoz gyakran használt könyvtárban lévő vektor típust használta fel a karakterek tárolására. Mi azonban ehelyett a C++11-ben megjelent variadikus (változó paraméterszámú) templateek paraméter csomagjaiban tároltuk a karaktereket. Ez azért előnyös, mert az új inicializációs szintaxis segítségével könnyen és hatékonyan tudunk futási idejű karakterláncokat létrehozni. A MetaStringImpl metaprogram eltávolítja a fölösleges záró nullákat is a karakterlánc végéről. A GetRuntimeStr metóduson látszik, hogy ebben a reprezentációban milyen egyszerű a futási idejű karakterlánc létrehozása. template <char... cs > struct Accumulator { static std :: string GetRuntimeStr () { return {cs...}; 6
} }; template < typename T, char... > struct MetaStringImpl ; template <char... cs > struct MetaString { typedef typename MetaStringImpl < Accumulator <>, cs... >:: result str ; static std :: string GetRuntimeStr () { return str :: GetRuntimeString (); } }; 2.2. Típusinformáció tárolása A kovetkező akadály, hogy hogyan tároljuk el a konverziós adatokat úgy, hogy azt későbbiekben a legkönnyebben fel tudjuk használni. Makrók segítségével egyszerűvé tettük a függvények és funktorok regisztrációját a szemantikus elemző generátorához. Eltárolásra kerül a függvény szignatúrája, a neve, valamint az objektum típusa, ami ha példányosításra kerül, akkor utána függvényobjektumként is használható. Ezeket az információk a standard könyvtárban is megtalálható std::tuple osztályokban tároljuk. template < typename T, T* ptr > struct FunctionPointer ; template < typename Ret, typename... Args, Ret (*f)( Args...) > struct FunctionPointer < Ret ( Args...), f> { Ret operator ()( Args... args ) { return f( args...) ; } }; # define FUNCTION (x) \ std :: tuple < decltype (x), \ 7
FunctionPointer < decltype ( x), &x>, \ MetaString <_S( #x ) >> # define FUNCTOR (x) \ std :: tuple < \ decltype (&x:: operator ()), x, \ MetaString <_S( #x ) >> A metaprogramok ki és bemenetei elsősorban típusok. Funktorok esetében egyszerű minden információt biztosítani, azonban függvények esetén ahhoz, hogy kapjunk példányosítható objektumot is, a függvény mutatójából készítünk egy egyedi típust. Ezt szolgálja a FunctionPointer template. Ezek a rendezett n-esek egy lista szerű fordítási idejű adatszerkezetbe kerülnek. 2.3. A típusellenőrző kigenerálása A metaprogram ezután generálni fog számos mátrixot a lista tartalma alapján. A mátrixok száma az meg fog egyezni a legnagyobb aritású függvény aritásával. Mindegyik mátrix a függvények illetve funktorok neveivel van indexelve. Valójában a szintaxisfa csúcsai mind függvénykompozíciót jelölnek, például: A(..., B(...),...), ahol B függvény eredményét tovább adjuk A-nak az i-edik paramétereként. Legyen az i-edik mátrix neve M i. Az előbbi szintaxisfabeli csúcs akkor típushelyes, hogyha az M i [A, B] értéke igaz. Tehát a M i [A, B] hordozza azt az információt, miszerint a B visszatérési értéke implicit módon konvertálódik-e A-nak az i-edik formális paraméterének a típusára. A metaprogram ezeket a mátrixokat fordítási időben generálja ki. Egy példán keresztül szemléltetve lehet a legkönnyebben látni, hogy mit is csinál ez a metaprogram. Tekintsük a következő egyszerű API-t: struct A {}; struct B : A {}; struct C {}; struct E : B {}; A* func1 (A*) { return 0; } B* func2 (A*, B*) { return 0; } C* func3 (A*) { return 0; } 8
struct D { E* operator () (A*) { return 0; } }; A szemantikus ellenőrzést végző automata generálásához elég beregisztrálnunk a függvényeinket és funktorainkat: typedef typename Automata < FUNCTION ( func1 ), FUNCTION ( func2 ), FUNCTION ( func3 ), FUNCTOR (D) >:: result GA; std :: set < std :: string > expected {" func1 ", " func2 ", "D"}; auto tmp = GA :: GetComposables (" func1 ", 0); std :: set < std :: string > result ( tmp. begin (), tmp. end ()); EXPECT_EQ ( expected, result ); Sajnálatos módon a felsorolás nem feltétlen triviális. Amennyiben túl van terhelve egy függvény, akkor minden túlterhelt változatát máshogy kell elnevezni a beregisztráláskor. Valójában a fentebb leírt makrók minden esetben a név mezőt is kitöltik a tuple-ban, tehát túlterhelt függvények esetén nincs lehetőség ezen makrók használatára. Ezek után, ha meg akarjuk határozni, hogy a func1 nevű függvény első paramétereként mely függvényhívások fordulhatnak elő, egy GetComposables hívással megkaphatjuk a neveiket. Gyakorlatilag ebben az esetben minden olyan függvény megfelel, aminek a visszatérési értéke az A, vagy A egy leszármazottjára mutató mutatóval tér vissza. A fenti példához hasonló módon dönti el a szemantikus elemző is, hogy az adott csúcs a szintaxisfában sérti-e a típuskonverziós szabályokat. 9
3. Összefoglalás A generatív programozás egy nagyon népszerű paradigma számos feladat megoldására. Sok automatizált eszköz létezik bizonyos feladatok elvégzésére. Ugyanakkor, mint kiderült, a C++ alkalmas a saját API-jának a kivezetésére típus biztos nyelvek számára mindenféle külső eszköz nélkül is. Ez is mutatja a statikus típusozás gazdagságát. A majdan megjelenő C++17-es szabványnak már része lesz a fordítási idejű statikus reflexió is. Ennek a segítségével jelentősen hatékonyabbá tudjuk majd tenni a megoldásunkat, valamint ki fogjuk tudni terjeszteni imperatív beágyazott nyelvekre és API-kra is. Összességébe véve egy olyan módszert és eszközt alkottunk meg, amelynek hála néhány esetben nagyban növelhető a fejlesztők hatékonysága. Hivatkozások [1] Alexandrescu, A.: Modern C++ Design: Generic Programming and Design Patterns Applied, Addison Wesley (2001) [2] Fowler, M.: Domain-Specific Languages, Addison-Wesley, 2010. [3] Hudak, P.: Building domain-specific embedded languages, ACM Comput. Surv. 28, 4 (Dec), 1996. [4] Lattner, C.: LLVM and Clang: Next Generation Compiler Technology, The BSD Conference, 2008. [5] Mernik, M., Heering, J., Sloan A.: When and how to develop domainspecific languages, ACM Computing Surveys, 37(4) pp. 316 344, 2005. [6] Porkoláb, Z., Sinkovics, Á.: Domain-specific Language Integration with Compile-time Parser Generator Library, In Proc. 9th international conference on Generative programming and component engineering (GPCE 2010). ACM, October 2010, pp. 137-146. 10
[7] Stroustrup, B.: The C++ Programming Language Addison-Wesley Publishing Company, fourth edition, 2013. [8] Vandervoorde, D., Josuttis, N. M.: C++ Templates: The Complete Guide, Addison-Wesley Professional, 2002 [9] Varanda, M. J., Mernik. M., Da Cruz, D. Henriques, P. R.: Program comprehension for domain-specific languages, Comput. Sci. Inf. Syst., Dec. 2008, vol. 5, no. 2, pp. 1 17 [10] Clang AST matcher library http://clang.llvm.org/docs/libastmatchers.html (11. June 2013.) [11] Sinkovics, Á.,Abrahams, D.: Using strings in C++ template metaprograms http://cpp-next.com/archive/2012/10/using-strings -in-c-template-metaprograms/ (02. June 2013.) [12] Beazley, David M.: SWIG: an easy to use tool for integrating scripting languages with C and C++ In Proc. 4th conference on USENIX Tcl/Tk Workshop, 1996 - Volume 4, pp. 15-15. 11