Keď sa navrhuje aplikácia, často sa kladie doraz na to, aby bola modulárna, ľahko rozšíriteľná, aby sa dala ľahko upravovať podla potrieb. Pri takýchto myšlienkach si programátor nie raz povie, žeby nebolo zle ak by jeho aplikácia mala skriptovací jazyk.
Vyvinúť vlastný skriptovací jazyk v C nieje malé sústo a taktiež prináša viac nevýhod ako výhod. To však nemusí platiť ak sa človek rozhodne implementovať už existujúci interpreter. Práve v tomto ja osobne vidím jednu z najväčších výhod Perlu a Pythona. Totižto ich interpreter sa dá ľahko implementovať do C aplikácie a sú doslova predurčene na takéto využitie. Jedným z mnoho príkladov je napríklad 3D modelovacia aplikácia Blender, ktorej funkcionalitu môže užívateľ vylepšiť práve modulmi napísanými v Pythone. V tomto článku vám ukážem, ako môžete vykonávať vo vašej C aplikácii Python skript, ako môžete volať funkcie Pythonu, taktiež ako môžete volať C funkcie z Python skriptu. Tieto poznatky by vám mali umožniť obojstrannú interakciu medzi skriptom a C aplikáciou.
Počiatočné strasti a priepasti
Asi najproblematickejší krok je, že ako začať. Na úplnom začiatku bude nutne nastaviť náš kompilátor, naše vývojové prostredie na prácu s Python API. K tomu sú potrebne 2 základné kroky. Nastaviť cestu k python.h i ostatným hlavičkovým súborom, a pre linker nastaviť Python knižnicu. Tieto kroky sa líšia od toho aké vývojové prostredie a aký OS používate. Ak mame všetko správne nastavené, môžeme prejsť k napísaniu prvého príkladu.main.c:
#include <Python.h>
int main()
{
Py_Initialize();
PyRun_SimpleString("print 'Hello World'");
Py_Finalize();
return 0;
}
Príklad je momentálne dosť strohý a veľa toho nedokáže. Na začiatku sa vola funkcia Py_Initialize(), ktorá vytvorí interpreter. Za ňou nasleduje PyRun_SimpleString(). Táto funkcia vykoná Python skript, ktorý jej predáme ako reťazec. Konkrétne ide o vypísanie textu 'Hello World' na výstup. Následne je volaná Py_Finalize(). Ta interpreter zruší a uprace za sebou pamäť. To nám na začiatok úplne stačí aby sme zistili, či máme všetko správne nastavené. Náš mini-program skompilujeme a spustíme.
sn3d@laptop$ g++ -I/cesta_k_python_h -L/cesta_k_python2.4_so -lpython2.4 -o out main.c
sn3d@laptop$ ./out
Hello World
Ak používate C++ prekladač a includujete iostream pred includovaním Python.h tak vtedy môže nastať nasledujúci warning:
warning: "_POSIX_C_SOURCE" redefined
Je to tak trošku bug Python headrov. Zbaviť sa ho môžete tým, že includovanie Python.h umiestnite pred includovanie iostream headru. Taktiež môžete naraziť na problém, že program sa skompilujte bez chyby, no po spustení nás program ovalí error správou, ktorá hovorí, že nieje možne nájsť libpython2.4.so knižnicu. V tomto prípade doporučujem ešte doplniť do LD_LIBRARY_PATH premennej prostredia cestu k tejto knižnici, alebo v prípade UBUNTU je potrebne tuto cestu vložiť do súboru /etc/ld.so.conf.
Pár slov ohľadom PyObject
V momente keď začnete používať API funkcie Pythonu alebo sa len letmo prejdete zrakom po dokumentácii, tak takmer všade natrafíte na typ PyObject. Je to zakladaný prvok všetkých API. PyObject reprezentuje akúkoľvek súčasť Python interpretera od globálnych premenných, všetkých možných funkcii v skripte, cez moduly a hodnoty, až po samotné prostredie interpretéra. PyObject obsahuje 2 dôležité informácie. Prvou je ukazovateľ na akýkoľvek objekt pythonu, a druha dôležitá informácia je počítadlo referencii. Python ma v sebe vstavaný mechanizmus garbage collectora, ktorý sa stará o správne uvoľnenie pamäti. Tento garbage collector uvoľňuje všetky tie objekty, u ktorých je práve toto počítadlo nulové. Keďže mi pristupujeme k Python interpreteru cez C kód, ktorý takýto pamäťový manažment nemá, musíme toto počítadlo pri inicializovaní Python objektu zvýšiť a naopak, pri ukončení prace znížiť. Na tuto činnosť slúži dvojica makro funkcii Py_INCREF(), ktorá zvýši počítadlo objektu o jeden a Py_DECREF(), ktorá naopak zníži počítadlo objektu o jeden.Volanie Python funkcie
Keďže už mame základné korky za sebou, pustíme sa do niečoho zmysluplnejšieho. Zavoláme Python funkciu compute zo súboru scrip.py. Tato funkcia obdrží 2 číselné parametre, ktoré spočíta a jeden textový parameter, ktorý vypíše na výstup. Funkcia bude vracať číselný výsledok spočítania.script.py:
def compute(a, b, text):
print 'message is:', text
c = a + b
return c
print 'script.py loaded \n'
Teraz príde ta zaujímavejšia časť. Naprogramujeme aplikáciu, ktorá inicializuje prostredie interpretera. Ďalej načíta a vykoná súbor script.py. Ziska ukazovateľ na PyObject, ktorý reprezentuje v prostredí interpretera funkciu compute. Pripraví premenné, ktoré sa budú predávať tejto funkcii a zavolá funkciu. Nakoniec spracuje výsledok volania funkcie.
main.c:
#include <stdio.h>
#include <Python.h>
int main()
{
//Deklaracia premenných.
PyObject* pMain;
PyObject* pMainDict;
PyObject* pFunc;
PyObject* pArgs;
PyObject* pArgA;
PyObject* pArgB;
PyObject* pArgText;
PyObject* pReturn;
FILE* file;
long retuned;
//Inicializácia python interpretera.
Py_Initialize();
pMain = PyImport_AddModule("__main__");
pMainDict = PyModule_GetDict(pMain);
//Otvorenie súboru a jeho vykonanie v main module.
file = fopen("script.py", "r");
if (file) {
PyRun_SimpleFile(file, "script.py");
}
//Vyhladanie existencie funkcie.
pFunc = PyDict_GetItemString(pMainDict, "compute");
if (PyCallable_Check(pFunc)) {
//Prírava argumentov funkcie.
pArgA = PyInt_FromLong(1);
pArgB = PyInt_FromLong(3);
pArgText = PyString_FromString("Hello Python");
//Vytvorenie objektu argumentov.
pArgs = PyTuple_New(3);
PyTuple_SetItem(pArgs, 0, pArgA);
PyTuple_SetItem(pArgs, 1, pArgB);
PyTuple_SetItem(pArgs, 2, pArgText);
//Volanie funkcie a spracovanie result hodnoty.
pReturn = PyObject_CallObject(pFunc, pArgs);
Py_DECREF(pArgs);
//Spracovanie návratovej hodnoty.
if (pReturn) {
returned = PyInt_AsLong(pReturn);
Py_DECREF(pReturn);
printf("function return: %d\n", returned);
}
}
Py_Finalize();
return 0;
}
Na začiatku je deklarácia všetkých premenných použitých v main() funkcii. Všimnite si, že všetky premenné, ktoré budú ďalej použité v súvislosti z Pythonom, sú ukazovatele na PyObject o ktorom som sa už rozpisoval. Po deklarácii nasleduje inicializácia Python interpretera. Ta je však o niečo zložitejšia ako v prvom príklade. V Pythone je kód delený na moduly. Zakladaný modul sa vola __main__. Je to zakladaný modul, v ktorom sa vykoná script.py. Modul inicializujeme pomocou funkcie PyImport_AddModule(), ktorá vráti ukazovateľ na tento modul. To však nieje všetko. Na to aby sme mohli pristupovať k prvkom modulu, v našom prípade aby sme mohli zavolať funkciu, budeme potrebovať objekt Dictionary. Ide o akýsi priestor modulu, v ktorom sa nachádzajú globálne premenne, funkcie atď. Získame ho volaním funkcie PyModule_GetDict().
Python funkcia, ktorú chceme volať, sa nachádza v súbore script.py. Potrebujeme teda otvoriť tento súbor a nechať vykonať jeho kód, aby sa v __main__ module nachádzala tato funkcia. Súbor otvoríme pomocou bežnej funkcie fopen() a následne file descriptor predáme funkcii PyRun_SimpleFile(), ktorá skript vykoná.
Teraz nám už len stačí získať ukazovateľ na compute funkciu, predať jej argumenty a zavolať ju. Znie to celkom jednoducho. Ukazovateľ na funkciu získame nasledujúcim kódom.
...
pFunc = PyDict_GetItemString(pMainDict, "compute");
if (PyCallable_Check(pFunc)) {
...
Funkcia PyDict_GetItemString() vráti ukazovateľ na Python funkciu. PyDict_GetItemString() môže vrátiť akýkoľvek ukazovateľ na akýkoľvek prvok v module __main__, pci už je to spomínaná funkcia alebo napríklad globálna premenná. Pomocou funkcie PyCallable_Check() otestujeme, či funkcia existuje a či je možné túto funkciu volať. V prípade, že všetko je v poriadku, program pokračuje ďalej konvertovaním Céčkovských premenných na premenne ktorým rozumie Python.
pArgA = PyInt_FromLong(1);
pArgB = PyInt_FromLong(3);
pArgText = PyString_FromString("Hello Python");
...
Všetky hodnoty, ktoré chceme preniesť do Pythonu, je potrebne skonvertovať. Python totižto nerozumie presne hodnotám v Céčku, taktiež textové reťazce fungujú odlišnejšie ako v Céčku. Pre tento účel existujú funkcie PyInt_FromLong, PyString_FromString. Python funkcia compute prijíma 3 argumenty. Prvé dva sú číselné a posledný je text. Najprv si teda pre hodnoty vytvoríme Python objekty spomínanými konverznými funkciami. Po konverzii budeme musieť vytvoriť objekt argumentov. Tento objekt sa volá Tuple(násobok) a môžeme si ho predstaviť ako zásobník do ktorého vložíme všetky hodnoty, ktoré chceme predať funkcii compute().
...
//Vytvorenie objektu argumentov.
pArgs = PyTuple_New(3);
PyTuple_SetItem(pArgs, 0, pArgA);
PyTuple_SetItem(pArgs, 1, pArgB);
PyTuple_SetItem(pArgs, 2, pArgText);
...
Pomocou PyTuple_New() sa vytvorí Tuple objekt reprezentujúci argumentami. Funkcia potrebuje len jeden parameter, a tým je počet hodnôt-argumentov. Sled funkcii PyTuple_SetItem už len nastavuje hodnoty a ich pozíciu v Tuple objekte. Keď už máme ukazovateľ na funkciu a objekt reprezentujúci argumenty, s ktorými funkcia bude volána, tak môžeme prejsť k volaniu compute funkcie.
...
pReturn = PyObject_CallObject(pFunc, pArgs);
Py_DECREF(pArgs);
//Spracovanie návratovej hodnoty.
if (pReturn) {
returned = PyInt_AsLong(pReturn);
Py_DECREF(pReturn);
printf("function return: %d\n", returned);
}
Funkcia PyObject_CallObject() vykoná volanie Python funkcie s argumentami, vykoná kód funkcie a vráti ukazovateľ na návratovú hodnotu Python funkcie. V prípade, ak dôjde k zlyhaniu, funkcia vráti NULL. Nakoľko, už ďalej nebudeme potrebovať objekt pArgs, znížime počet referencii pomocou Py_DECREF(). Python funkcia compute nám vráti súčet hodnôt pArgA a pArgB. Ten je potrebne zase spätne konvertovať na hodnotu, ktorej rozumie Céčko. Na to nám poslúži funkcia PyInt_AsLong(), ktorá vráti číselnú hodnotu, ktorej už Céčko rozumie. Nakoniec zavoláme Py_Finalize(). Tá ukonči python interpreter v nasej aplikácii. Program by mal vypisovať nasledujúci text na výstup.
script.py loaded
message is:Hello Python
function return: 4
Volanie C funkcie
Predchádzajúci príklad je vcelku fajn, no na komplexnejšiu implementáciu pythona, ako skriptovacieho jazyka, nám to nestačí. Totižto, akosi nám začne chýbať spätná interakcia. Ako teda volať Céčkovskú funkciu s Pythona? Tak to si teraz hneď ukážeme. Zoberme si imaginárnu situáciu, že mame C funkciu computeEx(), ktorú chceme volať z Python skriptu. Túto funkciu budeme musieť obaliť ďalšou funkciou, ktorá sa postará o konvertovanie Python hodnôt na Céčkovské. Taktiež budeme musieť túto funkciu zaregistrovať, aby Python interpreter o nej vedel. Cely kód teda bude vyzerať nasledovne.main.c:
#include <stdio.h>
#include <Python.h>
#define SCRIPT_FILE "script.py"
int computeEx(int a, const char* text)
{
printf("C SAY: computeEx write:%s\n", text);
return (a+a);
}
PyObject* computeExWrap(PyObject* self, PyObject* args)
{
//Deklaracia premenných.
PyObject* pResult;
int result;
int arg_a;
char* arg_text;
//Konvertovanie python argumentov na C hodnoty.
PyArg_ParseTuple(args, "is", &arg_a, &arg_text);
//Zavolanie computeEx a spracovanie return hodnoty.
result = computeEx(arg_a, arg_text);
pResult = Py_BuildValue("i", result);
return pResult;
}
static PyMethodDef methods[] = {
{"computeEx", computeExWrap, METH_VARARGS, "my own C-function"},
{NULL, NULL, 0, NULL}
};
int main()
{
//Deklaracia premenných.
PyObject* pMain;
PyObject* pMainDict;
FILE* file;
//Inicializácia.
Py_Initialize();
Py_InitModule("c_module", methods);
//Otvorenie súboru a jeho vykonanie v main module.
file = fopen(SCRIPT_FILE, "r");
if (file) {
PyRun_SimpleFile(file, SCRIPT_FILE);
}
Py_Finalize();
return 0;
}
script.py:
import c_module
print 'PYTHON SAY: calling c_module.computeEx()'
ret = c_module.computeEx(2, "Hello Python")
print 'PYTHON SAY:c_module.computeEx() return is:', ret
Najprv si rozoberieme tú jednoduchšiu časť, ktorou je scrypt.py. Tento skript sa bude vykonávať vo vnútri našej aplikácie. Ako prvé bude treba importovať Python modul, v ktorom sa bude nachádzať funkcia computeEx(). Vytvorenie modulu popíšem neskôr. Na ďalších riadkoch už len zavoláme funkciu, predáme jej hodnoty a vypíšeme hodnotu, ktorú nám funkcia vráti. Teraz však prejdeme ku kódu, ktorý sa nachádza v main.c. Začnem vysvetľovať netradične, nie od main() funkcie, ale od prvej funkcie computeEx(). Ide o hypotetickú funkciu, ktorú chceme sprístupniť a umožniť jej volanie z Python skriptu. Funkcia je jednoduchá a nevykonáva nič podstatne. Ma 2 vstupne premenne. Prvou je číslo, ktoré sa počíta a druhou je text, ktorý sa vypíše na výstup. Naschvál som zvolil dvojicu číslo-text, aby ste videli ako na textové hodnoty a ako na číselne hodnoty. Teraz však príde omnoho dôležitejšia časť s pohľadu pythonu. Je ňou funkcia computeExWrap. Python pracuje práve s touto funkciou. Funkcia je akýmsi baleným volaním funkcie computeEx(). Jej význam spočíva v konverzii vstupných a výstupných hodnôt.
PyObject* computeExWrap(PyObject* self, PyObject* args)
{
//Deklaracia premenných.
PyObject* pResult;
int result;
int arg_a;
char* arg_text;
//Konvertovanie python argumentov na C hodnoty.
PyArg_ParseTuple(args, "is", &arg_a, &arg_text);
//Zavolanie computeEx a spracovanie return hodnoty.
result = computeEx(arg_a, arg_text);
pResult = Py_BuildValue("i", result);
return pResult;
}
Funkcia sa vykoná pri každom volaní computeEx() z pythonu. Do self sa uloží ukazovateľ na Python objekt funkcie a do args sa uloží ukazovateľ na Tuple objekt, ktorý by vám mal byt známy z predchádzajúcej časti. Ak ste zabudli, je možne si ho predstaviť ako objekt zásobníka funkcie. Volaním funkcie PyArg_ParseTuple() konvertujeme Python hodnoty na hodnoty, ktorým rozumie Céčko. Pozorne sa pozrite na druhy parameter funkcie. Ide o reťazec „is“. Ak viete, ako pracuje funkcia printf(), tak s touto funkciu by ste taktiež nemali mat problém. Reťazcom „is“ hovoríme, aké typy hodnôt sa nachádzajú v zásobníku a v akom poradí sú uložene. V tomto prípade ide o i – integer, a s – string. Hodnoty sa uložia následne no premenných arg_a a arg_text. Okrem 'i' a 's' môžu byt použite nasledujúce písmena určujúce typ:
znak | Python | C | komentár |
s | string | char* | konverzia zo string na char* |
s# | string | char*, int | konverzia zo string na char* a int, ktorý je dĺžka textu |
z | string | char* | konverzia zo string na char*, string môže byť None |
z# | string | char*, int | konverzia zo string na char* a int, ktorý je dĺžka textu kde string môže byt None |
b | integer | char | prevedie číslo na char hodnotu(rozsah 0-255) |
h | integer | short int | prevedie číslo na short int hodnotu |
i | integer | int | prevedie číslo na int hodnotu |
f | float | float | prevedie číslo na float hodnotu |
d | float | double | prevedie číslo na double hodnotu |
Akonáhle už mame skonvertované hodnoty do Céčka, môžeme zavolať funkciu computeEx(), ktorej tieto hodnoty predáme. Výsledok funkcie si uložíme a ďalšou funkciou Py_BuildValue() ju naopak konvertujeme z Céčka do Pythonu. Funkcia používa rovnaký konverzný reťazec, v tomto prípade len „i“, nakoľko výstup funkcie je len číslo. Návratovú hodnotu použijeme aj ako návratovú hodnotu funkcie computeExWrap(). Ďalej nasleduje akési prepojenie medzi pythonom a Céčkom. Ide o pole prvkov PyMethodDef.
static PyMethodDef methods[] = {
{"computeEx", computeExWrap, METH_VARARGS, "my own C-function"},
{NULL, NULL, 0, NULL}
};
Toto pole ma 2 dôležité vlastnosti. Prvou je to, že je deklarovaná ako statická. Dôvod je jednoduchý. Pri registrácii a volaní metód, používa Python práve toto pole, nie jeho kópiu. Platnosť pola teda nesmie vypršať skôr, ako neukončíme interpreter pomocou Py_Finalize(). V opačnom prípade by mohlo dojsť k pádu aplikácie. Ďalšou dôležitou vlastnosťou je posledný prvok pola. Všimnite si, že je prázdny. Hodnoty sú nastavene na NULL. Tento posledný prvok funguje podobne ako NULL znak pri reťazcoch. Označuje koniec pola. V prípade ak posledný prvok nebude nastavený na NULL, dôjde k pádu aplikácie. Prvky pola sú typu PyMethodDef. Ide o štruktúru, ktorá obsahuje všetko potrebne aby Python vedel zavolať Céčkovskú funkciu. Štruktúra obsahuje nasledujúce položky:
položka | typ | význam |
ml_name | char* | meno metódy |
ml_meth | PyCFunction | ukazovateľ na metódu, ktorá sa bude volať |
ml_flags | int | flagy určujúce ako bude konštruované volanie funkcie(v našom prípade METH_VARARGS, ktoré je najčastejšie používane. Okrem iného môže isť o METH_NOARGS, METH_CLASS atď.) |
ml_doc | char* | popisujúci komentár |
V tejto chvíli už mame všetko potrebne, aby sme zavolali computeEx() funkciu a dostávame sa pomaličky ku koncu a k funkcii main(). Funkcia nieje zložitá a už by vám mala byt jasná.
int main()
{
//Deklaracia premenných.
PyObject* pMain;
PyObject* pMainDict;
FILE* file;
//inicializácia
Py_Initialize();
Py_InitModule("c_module", methods);
//Otvorenie súboru a jeho vykonanie v main module.
file = fopen(SCRIPT_FILE, "r");
if (file) {
PyRun_SimpleFile(file, SCRIPT_FILE);
}
Py_Finalize();
return 0;
}
Najdôležitejšie je volanie funkcie Py_InitModule(). Funkcia zaregistruje pole metód pod modulom nazvaným „c_module“. Od tohto miesta je možné vykonať kód „script.py“, v ktorom sa modul importuje. Preklad programu by nemal byť problém a program by vám mal vypisovať na vystúp nasledujúci text:
PYTHON SAY: calling c_module.computeEx()
C SAY: computeEx write:Hello Python
PYTHON SAY:c_module.computeEx() return is: 4
Z textu môžete vidieť priebeh, ako je volaná Python funkcia computeEx(), ktorá spočíta 2 + 2, Ďalej vypíše text na výstup a následne vráti vypočítaný výsledok, ktorý je následne vypísaný na vystúp v Python skripte. Článok si dával za cieľ vysvetliť princíp implementácie Pythona do Céčkovského kódu. Dúfam, že sa mi podarilo vám tento mechanizmus priblížiť.