M A S A R Y K O V A UNIVERZITA FAKULTA INFORMATIKY Integrace skriptovacích jazyků do C++ kódu D I P L O M O V Á PRÁCE Michal Pochobradský Brno, jaro 2016 Místo tohoto listu vložte kopie oficiálního podepsaného zadání práce a prohlášení autora školního díla. Prohlášení Prohlašuji, že tato diplomová práce je mým původním autorským dílem, které jsem vypracoval samostatně. Všechny zdroje, prameny a literaturu, které jsem při vypracování používal nebo z nich čerpal, v práci řádně cituji s uvedením úplného odkazu na příslušný zdroj. Michal Pochobradský Vedoucí práce: RNDr. Dalibor Klusáček, Ph.D. i Poděkování Chtěl bych zde poděkovat svému vedoucímu práce RNDr. Daliboru Klusáčkovi, Ph.D. za čas, vstřícnost a mnoho rad jež mi dal. Dále bych chtěl poděkovat Mgr. Václavu Chlumskému a RNDr. Simonu Tóthovi za mnoho rad a technickou pomoc s plánovačem systému TORQUE. Nakonec bych chtěl též poděkovat svým rodičům za nezměrnou podporu při mém studiu. iii Shrnutí Cílem této práce je popsat a porovnat způsoby integrace různých skriptovacích jazyků do C/C++ aplikace. K porovnání byly zvoleny jazyky ChaiScript, Python, Lua a JavaScript. Pro každý z nich je popsán přesun hodnot mezi skriptovacím jazykem a C/C++ kódem, volání funkcí a export datových typů z C/C++. Součástí práce je též porovnání výkonu jednotlivých jazyků při volání funkcí a exportu dat. Získané znalosti jsou pak použity k praktické ukázce možného využití skriptovacího jazyka v plánovači dávkového systému TORQUE. iv Klíčová slova skriptovací jazyky, integrace, C/C++, ChaiScript, Python, Lua, JavaScript, V8, SpiderMonkey v Obsah 1 Úvod 1 2 Testované jazyky 3 2.1 ChaiScript 4 2.1.1 Instalace a inicializace 4 2.1.2 Správa paměti 5 2.1.3 Export jednoduchých hodnot do scriptu 5 2.1.4 Export jednoduchých hodnot ze scriptu 5 2.1.5 Export C++ funkcí do scriptu 5 2.1.6 Volání funkcí z C++ 6 2.1.7 Export C komplexních typů 6 2.2 Python 7 2.2.1 Instalace a inicializace 7 2.2.2 Správa paměti 8 2.2.3 Export jednoduchých hodnot do scriptu 9 2.2.4 Export jednoduchých hodnot ze scriptu 9 2.2.5 Export C++ funkcí do scriptu 10 2.2.6 Volání funkcí z C++ 11 2.2.7 Export C komplexních typů 11 2.3 Lua 14 2.3.1 Instalace a inicializace 14 2.3.2 Správa paměti 15 2.3.3 Export jednoduchých hodnot do scriptu 15 2.3.4 Export jednoduchých hodnot ze scriptu 16 2.3.5 Export C++ funkcí do scriptu 16 2.3.6 Volání funkcí z C++ 17 2.3.7 Export C komplexních typů 18 2.4 JavaScript V8 20 2.4.1 Instalace a inicializace 20 2.4.2 Správa paměti 21 2.4.3 Export jednoduchých hodnot do scriptu 22 2.4.4 Export jednoduchých hodnot ze scriptu 22 2.4.5 Export C++ funkcí do scriptu 22 2.4.6 Volání funkcí z C++ 23 2.4.7 Export C komplexních typů 24 2.5 JavaScript SpiderMonkey 27 vii 2.5.1 Instalace a inicializace 27 2.5.2 Správa paměti 28 2.5.3 Export jednoduchých hodnot do scriptu 29 2.5.4 Export jednoduchých hodnot ze scriptu 29 2.5.5 Export C++ funkcí do scriptu 29 2.5.6 Volání funkcí z C++ 30 2.5.7 Export C komplexních typů 31 2.6 Správa paměti obecně 33 2.7 Podpůrné nástroje 34 2.7.1 SWIG 35 2.7.2 BOOST.PYTHON 36 2.7.3 LuaWrapper 37 2.7.4 Cython 38 3 Měření výkonu 41 3.1 Vyvolání exportované funkce 41 3.2 Rychlost paměťového přenosu 43 3.3 Rychlostfunkce definované ve skriptu 44 4 Osobní zkušenosti a celkové vyhodnocení 47 4.1 ChaiScript 47 4.2 Python 48 4.3 Lua 48 4.4 JavaScript V8 49 4.5 JavaScript SpiderMonkey 49 5 Plánovač 51 5.1 Rozšíření konfigurace plánovače 51 5.1.1 Dynamické načítání konfigurace 51 5.1.2 Rozšíření konfigurace 51 5.1.3 Získané zkušenosti 52 5.2 Integrace skriptovacího jazyka 53 6 Závěr 55 A Obsah přiloženého DVD 59 A . l Výkonostní testy 59 A . l . l práce_s_pametí_a_volání_skriptu.ods 59 A. 1.2 volání_exportovných_funkcí.ods 59 A.2 Úpravy plánovače 59 A.2.1 troque_fast_sched-git.txt 59 A.2.2 js_v8.cpp 59 A.2.3 js_v8.hpp 59 viii A.2.4 plan_eval.js 60 ix 1 Úvod Cílem této práce je prozkoumat možnosti spolupráce skriptovacích jazyků a C/C++ kódu s ohledem na možnost rozšíření konfigurace plánovače systému TORQUE (dále jen plánovač), který je využíván pro plánování výpočetních úloh na strojích zapojených do distribuované výpočetní infrastruktury centra CERIT-CS [3]. Dávkový systém TORQUE propojuje zapojené zdroje (počítače, datová úložiště apod.) a umožňuje jejich sdílení, takže uživateli stačí do systému zadat úlohu a už se nemusí starat o to, kde se úloha vykoná. TORQUE má tři části. První je server, na nějž uživatelé posílají úlohy, které server rozesílá klientům (viz dále), a od něj uživatelé dostávají výsledky. Druhou jsou klienti, kteří jsou nainstalováni na strojích počítajících úlohy. Třetí částí je plánovač, který dostává od serveru informace o úlohách a nazpět posílá pokyny, kdy a kde se mají jednotlivé úlohy vykonat. Právě konfigurace plánovače je motivací této práce. Plánovač používá pro uložení konfigurace textové soubory s jednoduchým formátem umožňujícím nastavit hodnoty konfiguračních voleb. Takováto konfigurace se ovšem ukazuje jako nedostatečná, neboť aby se dosáhlo požadovaných možností konfigurace, bylo by občas nutno přidat zcela neúměrně mnoho konfiguračních voleb pro kontrolu malé části kódu. Lepší by bylo přímo upravit kód daného algoritmu, ovšem to v současnosti znamená znovu zkompilovat celý plánovač. Možným řešením by byla integrace interpretu nějakého skriptovacího jazyka do plánovače a přesunutí často upravovaného algoritmu do skriptu. Použití skriptu by mělo umožnit požadovanou volnost při konfiguraci, protože by díky tomu by bylo možné za běhu plánovače měnit celý takto exportovaný algoritmus, a ne jen jeho vybrané parametry. Integrovaný interpret by umožnil vykonat vždy aktuální skript s daným algoritmem bez nutnosti jakékoliv kompilace. Integrace skriptovacího jazyka do existující C/C++ aplikace však není triviální. V této práci popíšu a porovnám postupy, jak ve vybraných skriptovacích jazycích umožnit spolupráci s C/C++ kódem, a poukážu na možné problémy s tímto související. Možnost integrace skriptovacího jazyka do plánovače ukážu exportem vybraného algoritmu do zvoleného skriptovacího jazyka. K tomu 1 i . ÚVOD a ke zohlednění potřeb plánovače se budu muset napřed seznámit s plánovačem a jeho konfigurací. Proto rozšířím možnosti jeho současné konfigurace. Existuje mnoho potenciálně použitelných jazyků a bylo by nemožné otestovat všechny, proto jsme se v diskusi s vedoucím práce a vývojáři plánovače dohodli na následujících čtyřech. Prvním je ChaiScript, který by měl být snadno integrovatelný do C/C++ kódu. Druhým je široce používaný Python. Třetím je Lua, která je také často používaná pro integraci do C/C++ aplikací. Čtvrtým je JavaScript, jehož interprety se stali staly důležitou součástí internetových prohlížečů, a dočkal se tak závodů v rychlosti interpretů. Pro JavaScript jsme zvolili dvě implementace interpretu ze dvou nejrozšířenějších prohlížečů s otevřenými zdrojovými kódy. Prvním je V8 engine od firmy Google, který je použit v prohlížeči Chromé. Druhým je pak SpiderMonkey použitý v prohlížeči Firefox. V následujícím textu nejprve představím C/C++ rozhraní jednotlivých jazyků. Dále uvedu porovnání výkonnosti těchto jazyků ve vybraných oblastech. Poté uvedu provedu celkové zhodnocení jednotlivých jazyků. Nakonec popíšu změny, jež jsem provedl v plánovači, abych se s ním seznámil, a ukázku integrace zvoleného skriptovacího jazyka. 2 2 Testované jazyky V této kapitole se zaměřím na prostředky spolupráce zvolených skriptovacích jazyků s C/C++. Při vytváření rozhraní mezi C/C++ a moderním skriptovacím jazykem vyvstávají zejména dva problémy Zaprvé skriptovací jazyky jsou dynamicky typované, zatímco C/C++ je staticky typovaný jazyk. Tento rozdíl lze zpravidla řešit a skriptovací jazyky (přinejmenším ty jimiž se zabývám v této práci) poskytují C/C++ rozhraní umožňující toho dosáhnout. Zpřístupnit data z C/C++ pro použití ve skriptovacím jazyku a naopak je už zpravidla relativně jednoduchý, ačkoliv často rozsáhlý úkol. Druhým problémem při exportu je alokace a dealokace paměti, která je v C/C++ zcela v rukou programátora, ale skriptovací jazyky používají tzv. Garbage Collector (dále jen GC), tedy otázkou je, kdo má uvolnit paměť exportovaných hodnot. Tento problém je typicky složitější, proto se jím v následujícím textu nebudu zaobírat a budu mu věnovat samostatnou podkapitolu později (viz kapitola 2.6). V následujících podkapitolách každý jazyk krátce představím a pro každý ukážu: • Jak daný jazyk připravit pro použití. • Jak pracovat s pamětí drženou daným jazykem. • Jak převést jednoduchou hodnotu (např. číslo) z daného skriptovacího jazyka do C/C++ a naopak. • Jak z daného skriptovacího jazyka volat C/C++ funkce a nao­ pak. • Jak exportovat složité C / C++ pole a třídy. Jak uvádím v kapitole 5.1, v plánovači jsou často používány C/C++ struktury1 , budu se jim proto věnovat více. Ještě upozorním, že cílem dalšího textu je pouze ukázat, že výše uvedené cíle jsou dosažitelné, a popsat postup, jak jich dosáhnout, ale samozřejmě mohou existovat i jiné postupy. 1. v C++ představují datové typy, struktura struct a třída class prakticky totéž 3 2. TESTOVANÉ JAZYKY 2.1 ChaiScript ChaiScript [11] je relativně mladý jazyk (stabilní verze od roku 2009), jeho asi nejvýraznější vlastností je velký důraz na snadnou integraci s C/C++ kódem. To se projevuje jak v jeho C/C++ rozhraní, tak i v syntaxi samotného skriptu, která se zjevně inspiruje syntaxí jazyka C. 2 //inicializace 3 chaiscript::ChaiScript chai; 4 5 //export proměnné 6 int a = 2; 7 chai.add(chaiscript::var(a),"A"); 8 //ziskáni hodnoty z ChaiScriptu 9 int a = chai.eval("A"); 10 //stejně jednoduše lze do ChaiScriptu exportovat funkce 11 double foo (double i){ 12 return 42; 13 > 14 chai.add(chaiscript::fun(foo), "foo"); Ukázka 2.1: ChaiScript jednoduchá ukázka: inicializace, export proměnné a funkce do ChaiScriptu a vrácení hodnoty z ChaiScriptu 2.1.1 Instalace a inicializace ChaiScript není potřeba instalovat ani zvlášť kompilovat, stačí do kódu přidat include jeho hlavičkových souborů (viz [11]). V této práci používám ChaiScript verze 5.7. Použití jazyka ChaiScript z C++ je velmi přímočaré, k základní inicializaci stačí vytvořit instanci třídy ChaiScript (viz Ukázka 2.1 ř. 3, konstruktoru třídy lze předat jako parametr modul s knihovnou, případně cesty, kde hledat skripty), od tohoto okamžiku je ChaiScript připraven k použití. Skript k vykonání lze ChaiScriptu předat buď jako textový řetězec pomocí metody evalQ (viz Ukázka 2.1 ř. 9), nebo jako soubor pomocí metody eval_file(). 4 2. TESTOVANÉ JAZYKY 2.1.2 Správa paměti ChaiScript interně používá kopie, pouze pokud je cokoliv exportováno jako ukazatel nebo reference (ať už z C do ChaiScriptu, tak z ChaiScriptu do C/C++), tak je na programátorovi, aby zajistil, že odkazovaná paměť nebude předčasně uvolněna, toto může zjednodušit použití sdílených ukazatelů ze standardu C++11, které ChaiScript podporuje stejně jako ukazatele běžné. 2.1.3 Export jednoduchých hodnot do scriptu K exportu proměnných do ChaiScriptu slouží dvouparametrická verze metody add(), které se předá exportovaná proměnná zabalená pomocí funkce var{) a textový řetězec obsahující jméno, pod nímž bude proměnná vystupovat ve skriptu (viz Ukázka 2.1 ř. 7). Stojí za povšimnutí, že není třeba zadávat datový typ proměnné, ten se určí automaticky. Zadávaná proměnná může být také ukazatel nebo reference. 2.1.4 Export jednoduchých hodnot ze scriptu K získání hodnot ze skriptu slouží metoda eval(), která vykoná předaný script a vrátí výsledek. Datový typ očekávaného výsledku lze zadat jako parametr šablony (viz Ukázka 2.1 ř. 9), pokud žádný typ není zadán, vrátí funkce výsledek jako třídu BoxedValue, což je C++ datový typ, který ChaiScript interně používá pro všechny hodnoty Pro získání hodnoty konkrétní proměnné ze skriptu stačí metodě evalQ jako parametr předat název této proměnné. 2.1.5 Export C++ funkcí do scriptu Export C++ funkce do skriptu se v zásadě neliší od exportu proměnné, pouze je třeba exportovanou funkci zabalit pomocí funkce fun() místo var() (viz Ukázka 2.1 ř. 14), parametry ani návratovou hodnotu není třeba nijak specifikovat. 5 2. TESTOVANÉ JAZYKY 2.1.6 Volání funkcí z C++ Úplně stejně jako proměnné lze z ChaiScriptu získat i funkce, návratovou hodnotou metody eval() je v tomto případě C++11 šablona stá :: function, konverze jejích argumentů proběhne automaticky. 2.1.7 Export C komplexních typů Zde popíšu, jak lze exportovat C/C++ pole a třídy. Jen pro úplnost uvedu, že přesná definice třídy TestClass používané v ukázkách jako exportovaná C++ třída je součástí přiložených testů (viz přílohy A.l). Při exportu C/C++ třídy je třeba si uvědomit, že ChaiScript stejně jako jakýkoliv jiný jazyk nezná konkrétní exportovanou třídu, proto je třeba ji napřed zaregistrovat. Poněkud překvapivější by mohlo být, že jednoduše nelze exportovat ani C/C++ pole. Jenže běžně používaná pole ve většině skriptovacích jazyků, včetně ChaiScriptu, nejsou zcela ekvivalentní C/C++ poli, například jsou dynamická a typicky mohou obsahovat hodnoty různého typu. 1 //registrace pole 3 hodnot typu int jako IntArray3 2 chai.add(bootstrap::array("IntArray3")); 3 //registrace vektoru hodnot typu int jako Veclnt 4 chai.add( bootstrap::standard_library::vector_type >("VecInt")); 5 //vytvořeni modulu 6 chaiscript::ModulePtr m = chaiscript::ModulePtr(new chaiscript::Module()); 7 //registrace třidy TestClass H chaiscript::utility::add_class(*m, 9 "TestClass",//jméno v ChaiScriptu 10 {//seznam konstruktorů k zaregistrováni chaiscript::constructor(), 12 chaiscript::constructor() 13 },//seznam metod k zaregistrováni {{chaiscript::fun(&TestClass::metod), "metod"}}); 15 //načteni modulu 16 chai. add(m); Ukázka 2.2: ChaiScriptu, registrace komplexních typů 6 2. TESTOVANÉ JAZYKY Pole Pro export polí je třeba v ChaiScriptu nejprve zaregistrovat typ exportovaného pole, to se provede pomocí pomocné funkce array{) a výsledek této metody se do ChaiScriptu přidá pomocí jednoparametrické verze metody add() (viz Ukázka 2.2 ř. 2). Po zaregistrování se již pole exportují stejně jako jakákoliv jiná hodnota. Ještě upozorním, že pole se registruje včetně své velikosti, různě velká pole stejného typu je tak potřeba registrovat zvlášť. Obdobně se exportují i standardní C++ kontejnery (viz Ukázka 2.2 ř. 4). Třídy Pro export vlastních tříd je nejjednodušší vytvořit si nový modul a použít pomocnou funkci add_class(), která zaregistruje do daného modulu třídu, její konstruktory a metody (viz Ukázka 2.2 ř. 6-16), alternativně lze totéž udělat tak, že se zaregistruje typ obdobně jako proměnná2 a poté se zaregistrují jednotlivé metody a konstruktory (metody i atributy třídy se registrují jako funkce). 2.2 Python Python patří k nejznámějším a nejpoužívanějším skriptovacím jazykům. Díky jeho již dlouhodobému vývoji (vyvíjen od roku 1990) má odladěnou C/C++ implementaci zvanou CPython se stabilním C/C++ rozhraním. 2.2.1 Instalace a inicializace V této práci používám Python (verze 3.4), který je dostupný z webu[6]. Alternativně je možné na Ubuntu a zřejmě i jiných linuxových distribucích nainstalovat potřebnou knihovnu a hlavičkové soubory z repositářů. Pro jeho použití z C/C++ kódu je třeba přidat hlavičkový soubor Python.h a přilinkovat jeho knihovnu. Inicializace proběhne opět velmi jednoduše, tentokrát zavoláním funkce Py_Initialize(). Pro ukončení a uvolnění zdrojů slouží naopak funkce Py_Finalize(). Po Py_Initialize() je již Python připraven k použití, ale pro plnohodnotnou práci je často třeba importovat moduly s dodatečnou funk- 2. pomocí šablony user_type, např. chai.add(user_type < TestClass > (), "TestClass"); 7 2. TESTOVANÉ JAZYKY cionalitou, toto lze provést pomocí funkce PyItnport_ItnportModule (viz ř. 6 z Ukázky 2.3), funkce je ekvivalentní příkazu import v jazyce Python a lze tedy zároveň použít k načtení skriptu z předdefinovaných cest, ale pro načtení a provedení skriptu poskytuje poskytuje rozhraní Pythonu též několik vhodnějších funkcí, například PyRun_SimpleString() (načtení skriptu z textového řetězce) a PyRun_ SimpleFile (načtení skriptu ze souboru). 1 //inicializace Pythonu 2 Py_Initialize(); 3 //zde již je inicializace hotová a s Pythonem lze pracovat 4 5 //import dodatečných modulů 6 PyObject *m = PyImport_ImportModule("NazevModulu"); 7 //export proměnné s int a 9 //konverze C hodnoty na hodnotu jazyka Python 10 PyObject *value = PyLong_FromLong(a); n 12 //nastaveni konvertované hodnoty jako globálni proměnná modulu m 13 PyObject_SetAttrString(m, ,value) 14 //ziskáni hodnoty z Pythonu 15 PyObject *value2 = PyObject_GetAttrString(m,"A"); u, i f (PyLong_Check(value2)) { 17 a = PyLong_AsLong(value2); 18 } 19 //sniženi referenci již nepoužívaných hodnot 20 Py_DECREF(value); 21 Py_DECREF(value2); 22 //ukončeni práce s Pythonem 23 Py_Finalize(); Ukázka 2.3: Python jednoduchá ukázka: inicializace, export proměnné do Pythonu a zpět a ukončení práce 2.2.2 Správa paměti Při používání C rozhraní Pythonu je třeba mít na paměti, že Python používá pro detekci nepoužívaných proměnných počítání referencí, 8 2. TESTOVANÉ JAZYKY proto je třeba při používání funkcí manipulujících s nativními hodnotami jazyka Python, tj. těch, které mají v C typ PyObject, dávat pozor, jak se chovají k jejich referencím (toto je vždy popsáno v dokumentaci [7] konkrétní funkce), proto je třeba v případě potřeby počet referencí snížit, nebo zvýšit (funkcí Py_DECREF() a Py_INCREF(), pro detaily odkážu na dokumentaci [7]). Zejména je třeba vždy po získání hodnoty s novou referencí opět snížit počet referencí (viz Ukázka 2.3 ř. 20) poté, co již není potřeba, jinak by jimi držená paměť nemohla být uvolněna. 2.2.3 Export jednoduchých hodnot do scriptu Python má vlastní typy (pojmenovány jako Py[Typ], např. PyLong), které často přibližně odpovídají typům z C, hodnoty těchto typů jsou C struktury reprezentovatelné jako PyObject (mají definovanou hlavičku viz kapitola 2.2.7). C rozhraní Python nabízí funkce pro konverzi jednoduchých C typů na odpovídající PyObject, tyto funkce mají typicky název tvaru [python typ]_from[C typ] () (viz Ukázka 2.3 ř. 10), jinou možností je funkce Py_BuildValue{), která bere na vstupu formátovací řetězec a proměnný počet C hodnot (musí odpovídat formátovacímu řetězci) a vrátí odpovídající PyObject, takto lze vytvořit i proměnné složitějších základních typů jazyka Python jako pole nebo seznamy (pro detaily odkážu na [7]). Hodnoty vrácené těmito funkcemi mají vždy novou referenci. Do skriptu je možné tyto hodnoty předat buď jako parametry funkce, nebo nastavit jako atributy nějakého objektu (např. modulu, pak takovéto proměnné vystupují jako globální proměnné modulu, viz Ukázka2.3 ř. 13). 2.2.4 Export jednoduchých hodnot ze scriptu Ze skriptu lze získat hodnoty jako PyObject jednak jako návratové hodnoty po volání pythonové funkce (viz 2.2.6), nebo jako atribut jiného PyObjektu (např. modulu) pomocí PyObject_GetAttrString (viz Ukázka 2.3 ř. 15)) Ke konverzi z hodnot typu PyObject na C hodnoty slouží funkce s názvy tvaru [python typ]_as[C typ](), daný PyObject samozřejmě 9 2. TESTOVANÉ JAZYKY musí být daného typu, toto lze ověřit funkcemi s názvem tvaru [python typ]_Check() (Ukázka 2.3 ř. 16). 2.2.5 Export C++ funkcí do scriptu 1 //Přímo exportovateIná funkce 2 PyObject *python_function(PyObject *self, PyObject *args){ 3 //Získaní argumentu PyArg_ParseTuple(args, "formátovací řetězec", &argl, . . . ) ; 5 ... 6 } 7 //definice exportu funkcí s PyMethodDef module_methods[] = { 9 //jméno v Pythonu, funkce, očekávané argumenty, popis 10 {"foo", python_function, METH_VARARGS, "Popis funkce"}, {NULL} n > ; i3 static PyModuleDef module = { PyModuleDef_HEAD_INIT,//nezbytná hlavička modulu 15 " JmenoModulu", 16 "Popis modulu.", -1, i8 module_methods « >; 20 PyModule_Create (femodule); Ukázka 2.4: Python export C/C++ funkce Do Pythonu lze přidat funkce např. pomocí modulů, ale nelze přidat libovolnou funkci, pro export běžné C/C++ funkce je třeba vytvořit novou funkci splňující níže uvedené, která konvertuje parametry a zavolá exportovanou funkci. Nový modul lze vytvořit voláním funkce PyModule_Create() (viz Ukázka 2.4 ř. 20), pro kterou je třeba vytvořit definici modulu, do nějž se předá pole definic metod ukončené NULL(viz Ukázka 2.4 ř. 8 až ř. 19, jen poznamenám, že —1 v definici modulu je zde zcela nepodstatná, pro detaily odkážu na dokumentaci[7]). Přidávaná funkce musí mít 1 (PyNoArgsFunction), 2 (PyCFunction) nebo 3 (PyCFunctionWithKeywords) argumenty typu PyObject* (první 10 2. TESTOVANÉ JAZYKY argument je objekt, na kterém je funkce volána, druhý pozičně zadané argumenty funkce a třetí jsou argumenty zadané klíčovými slovy) a vracet PyObject* (viz Ukázka 2.4 ř. 2). Argumenty jsou funkci předány jako n-tice (Tuple) a k jejich konverzi na C hodnoty slouží funkce PyArg_ParseTuple(), která konvertuje jednotlivé hodnoty z n-tice na odpovídající C hodnoty podle formátovacího řetězce ([7, Parsing arguments and building values]). 2.2.6 Volání funkcí z C++ K vyvolání funkce je třeba nejprve získat funkci jako volatelný PyObject (viz kapitola 2.2.4), pak stačí použít např. funkci PyObject _CallObject() (viz. Ukázka 2.5) jejímž prvním argumentem je právě volatelný objekt, druhým argumenty jako n-tice (Tuple)(pro detaily viz dokumentace [7]). Opět vrací novou referenci. i PyObject *result = PyObject_CallObject(py_func, py_args); Ukázka 2.5: Volání funkce jazyka Python 2.2.7 Export C komplexních typů Oproti ChaiScriptu Python nemá možnosti, jak jednoduše zadefinovat typ odpovídající C/C++ poli nebo třídě, a tak je celý proces kompliko­ vanější. Pole Python běžně používá pro reprezentaci pole objekt typu PyList. Nejjednodušším způsobem konverze pole je tak vytvořit nový list pomocí PyList_New(), jejímž argumentem je počet prvků nového listu (slouží k dopředně alokaci paměti), exportované hodnoty postupně konvertovat na PyObject (viz 2.2.3) a ty přidávat do pole funkcí PyList_SetItem (viz Ukázka 2.6, případ na ř. 8 odpovídá přiřazení list[i] = source[í\). Druhým řešením je exportovat pole jako novou třídu jazyka Python a definovat pro ni metody getitem a setitem , případně další (stejné jako při definici vlastního pole ve skriptu, viz např. [15, Speciál Method Names]). 11 2. TESTOVANÉ JAZYKY //„source" je exportované pole int source[count]; //vytvoření nového Listu velikosti „count" PyObject * l i s t = PyList_New(count); //hodnoty pole se nakopíruji do Listu for (unsigned long i=0;i }; luaL_setfuncs(L, functions, 0); Ukázka 2.9: Lua export funkce 16 2. TESTOVANÉ JAZYKY Alternativně lze funkce přiřadit hromadně tabulce na vrcholu zásobníku pomocí funkce luaL_setfuncs (viz Ukázka 2.9 ř. 14). V každém případě musí být takto přidávaná funkce speciální funkce, která má jediný C argument ukazatel na lua_State, argumenty čte ze zásobníku a výsledky ukládá na zásobník a jako C návratovou hodnotu vrací počet skutečně vrácených (na zásobník uložených) hodnot jako int (viz Ukázka 2.9 ř. 2). Lua garantuje, že po zavolání zásobník obsahuje pouze její argumenty (pro volání je vytvořen nový zásobník). Pro konverzi argumentuje vhodné použít funkce luaL_check[typ] (), které jsou stejné jako lua_to[typ] (), ale přeruší výpočet funkce v případě, že hodnota není daného typu, a vrátí chybu. K exportu libovolné C/C++ funkce je třeba, stejně jako u Pythonu, vytvořit novou funkci, která získá ze zásobníku argumenty, vyvolá exportovanou funkci a uloží výsledek na zásobník (viz Ukázka 2.9). 2.3.6 Volání funkcí z C++ Nejprve je třeba umístit volanou funkci na vrchol zásobníku (viz Ukázka 2.10 ř. 1), poté přidat na zásobník argumenty funkce a poté zavolat funkci pomocí pcallQ, jehož druhým argumentem je počet argumentů volané funkce (ten také určuje pozici volané funkce na zásobníku), třetím je očekávaný počet vrácených hodnot (nemusí být shodný s počtem hodnot skutečně vraceným funkcí, Lua přebytečné hodnoty zahodí nebo chybějící doplní hodnotami nil), třetí argument je pozice na zásobníku s funkcí pro zpracování chyb. Po volání je volaná funkce i s argumenty odstraněna ze zásobníku a na vrchol jsou doplněny vrácené výsledky. lua_getglobal(L, "foo"); //argumenty funkce lua_pushinteger(L, argl); lua_pushinteger(L, argN); //zavolání funkce (očekává N argumentů a vrací jeden) lua_pcall(L, N, 1, 0); int result = lua_tointeger(L, -1); Ukázka 2.10: Lua volání funkce 17 2. TESTOVANÉ JAZYKY 2.3.7 Export C komplexních typů Komplexní struktury jsou v jazyce Lua zpravidla implementovány pomocí Lua tabulek a metatabulek. Poměrně unikátní oproti ostatním testovaným jazykům je, že Lua umožňuje přímo alokovat libovolně velkou paměť zpravovanou G C jazyka Lua. Pole Nativní pole je v jazyce Lua implementováno jako Lua tabulka (stejně jsou implementovány např. i třídy vytvořené ve skriptu). Novou tabulku je možné vytvořit funkcí lua_createtable(), která jako druhý argument bere očekávanou velikost pole (počet prvků, jedná se jen o nápovědu pro funkci, pole se automaticky zvětší, pokud je to třeba), nastavit hodnotu v poli lze pomocí funkce lua_rawseti()(viz ř. 6 z Ukázka 2.11), nastavená hodnota musí být na vrcholu zásobníku a je z něj funkcí lua_rawseti{) odebrána. Naopak pro získání hodnoty z pole slouží funkce lua_rawgeti(), která uloží hodnotu z pole na vrchol zásobníku (viz ř. 9 z Ukázka 2.11). Alternativně lze pole též exportovat jako třídu (pro detaily viz např. [10, Array Access]). //vytvoření nové tabulky očekávající „velikost" prvků lua_createtable(L,velikost,0); //umístění vkládané hodnoty na zásobník lua_pushinteger(L,10); //na i-tý index nově vytvořené tabulky (index -2) vloží 10 lua_rawseti(L, -2, i ) ; //z tabulky na vrcholu získá i-tou hodnotu lua_rawgeti(L, -1, i ) ; value = lua_tointeger(L, -1); Ukázka 2.11: Lua export pole Třídy Lua umožňuje na zásobník uložit libovolnou C/C++ hodnotu prostřednictvím hodnoty typu userdata, to je prakticky jen na Lua zásobníku alokovaná paměť libovolné velikosti, do ní je pak množné uložit např. kopii instance exportované třídy nebo ukazatel na originální instanci. Ale z pohledu jazyka Lua je to pořád jen paměť na zásobníku, pro export vlastní třídy je třeba pomocí funkce luaL_newmetatable() 18 2. TESTOVANÉ JAZYKY vytvořit „metatabulku", která se přiřadí později vytvořeným „userdatům", „metatabulka" jednak funguje jako definice „typu" třídy ale hlavně umožňuje přiřadit třídě metody a atributy. Přiřazení „metatabulky" konkrétním „userdatům" zachycuje Ukázka 2.13. //vytvoření nové metatabulky na vrcholu zásobníku luaL_newmetatable(L, "ClassWrapper"); //a uložení její kopie na vrchol zásobníku lua_pushvalue(L, -1); //nastavení atributu „ index" nové tabulky na sebe samu lua_setfield(L, -2, "__index"); //přiřazení funkce jako metody objektu luaL_setfuncs(L, wrapper_methods, 0); Ukázka 2.12: Lua export třídy definice Aby se metody mohly volat jako metody je třeba do atributu index matatabulky přiřadit nějakou metatabulku (třeba i samu sebe) obsahující tyto funkce. Příkladem, jak toho dosáhnout, je Ukázka 2.12, pokud by se tabulka nepřiřadila do index, tak by se metody nedaly volat jako metody (pomocí':'), ale pouze jako atributy typu funkce, tedy by se automaticky nedosadila třída, na níž byla funkce volaná jako první argument. //alokace paměti na Lua zásobníku, jako userdata TestClass *userdata = (TestClass *)lua_newuserdata(L, sizeof(TestClass)); //získání dříve vytvořené metatabulky luaL_getmetatable(L, "ClassWrapper"); //přiřazení metatabulky nově vytvořeným userdatům lua_setmetatable(L, -2); //do alokované paměti můžu uložit exportovanou třídu *userdata = ... Ukázka 2.13: Lua export třídy, vytvoření instance Metody se exportují stejně jako funkce, jen s tím rozdílem, že jako první parametr očekávají exportovanou třídu, to si mohou ověřit pomocí funkce luaL_checkudata(), která zkontroluje, že userdata na zásobníku mají přiřazenou konkrétní „metatabulku" a vrátí na ně ukazatel. 19 2. TESTOVANÉ JAZYKY Takto lze definovat nejen běžné metody, ale i operátory, např. aritmetické operátory (viz dokumentace [14]). 2.4 JavaScript V8 JavaScript je skriptovací jazyk hojně využívaný ve w w w stránkách. V8 engine je implementace JavaScriptu, přesněji implementuje JavaScript(dále jen JS) dle specifikace ECMAScript4 , vyvinutá pro prohlížeč Google Chromé. V8 je dostupný také samostatně jako C++ knihovna s otevřeným zdrojovým kódem, kterou zde budu prezento­ vat. 2.4.1 Instalace a inicializace Postup instalace V8 a kompilace aplikace používající V8 je komplikovanější a je popsán na webu [8] v kapitolách „Download and build" a „Getting Started". V této práci používám V8 engine verze 4.3, který byl použit v prohlížeči Chromé verze 43. Stejně tak inicializace V8 engine je oproti předchozím jazykům komplikovanější. Nejprve je třeba inicializovat platformu, poté jednu nebo více instancí Isolate (proměnná isolate v ukázkách), které představují něco jako izolované virtuální stroje, a pro ně je třeba inicializovat kontext (proměnná context v ukázkách), který definuje standardní funkce a objekty skriptu, jenž je v něm spuštěn, při vytvoření kontextu vznikne také globální objekt (proměnná context v ukázkách), který lze z kontextu získat metodou Global (), a je mu možné přiřazovat globální proměnné a funkce. Pro přesné podrobnosti odkážu na oficiální dokumentaci ([8, Getting Started]). Po základní inicializaci je možné začít provádět skripty. V8 neposkytuje podporu pro načtení skriptu ze souboru, ale pouze z textového řetězce, ten je třeba konvertovat na JS textový řetězec (viz kapitola 2.4.3), následně zkompilovat pomocí statické metody Compile() třídy Script, která vrátí instanci této třídy, zkompilovaný skript je pak možné provést pomocí jeho metody Run() (viz 2.14 ř. 6 a 7). 4. http://www.ecma-internátional.org/publications/standards/Ecma-262. htm 20 2. TESTOVANÉ JAZYKY 1 HandleScope handle_scope(isolate); 2 //načteni a provedeni skriptu 3 char* c_str_script = ... 4 //konverze textového řetězce 5 Local js_string_script = v8::String::NewFromUtf8(isolate, c_str_script); 6 Local