TinyScheme е един много малък Scheme интерпретатор, който през годините е използван за най-различни интересени проеки. Част от тях законни, други не особено. Той е написан на C и с помощта на макроси към препроцесора може да бъде компилиран с различна функционалност – според нуждите на потребителите. Естествено в самия интерпретатор е реализиран абсолютния минимум на езика. Очаква се потребителите да дописват каквото им потрябва. На мен ми потрябва база данни. Логичния избор за такава беше SQLite. Поради минималните си размери и простата организация – двете си пасват като дупе и гащи. Една хубава библиотека, която разширява TinyScheme възможностите е TSX. Тя дава възможност за работа със сокети и файлове. Написана е много хубаво и всичко вътре е изключително просто за разбиране. Вместо да правя отделна библиотека реших да допиша тази. За да можем да работим със SQLite са необходими поне следните функции – sqlite-open, sqlite-close, sqlite-prepare, sqlite-bind-text, sqlite-step, sqlite-column-text. Съответно трябва да можем да отваряме нова база и да я затваряме. Да правим sqlite3_stmt, да закачаме стойности към него, да го изпълняваме на отделни стъпки и да можем да вземаме стойностите от SELECT заявките. Естествено има още много полезни SQLite функции, които може да потрябват, но тези са един хубав минимум, от който можем да започнем. Разглеждайки TSX сорс кода, лесно можем да се ориентираме как се пишат TinyScheme функции. Всички функции връщат тип pointer, като параметри им се предават указател към интерпретатора и указател към аргументите на функцията. Цялата TinyScheme функционалност можем да ползваме през първия указател. Ето как изглежда sqlite-open.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | |
Най-напред проверяваме дали са ни подадени някакви аргументи изобщо, ако не са връщаме #f. След това вземаме първия аргумент. Понеже аргументите ни се подават, като списък (естествено) първия аргумент на функцията е car на списъка. Проверяваме дали той е низ, ако не е връщаме #f. Понеже повече аргументи не ни трябват – викаме sqlite3_open, като подаваме низ от първия аргумент. По този начин инициализираме sqlite3 указател, който след това връщаме. Понеже нямаме специален тип за sqlite3 в TinyScheme, използваме mk_integer(). Тоест връщаме стойността на указателя, като int или long стойност, в зависимост от архитектурата. Това най-вероятно не е най-добрия начин за вършене на тази работа, но със сигурност е най-простия. Ако искаме да постигнем дуракоустойчива система, в която да не разнасяме C указатели из Scheme интерпретатора, можем да му допишем базовите типове. Как става това ще опиша след малко. Вече можем да отворим нова база данни, но е добре да можем и да я затворим – sqlite-close.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | |
Тук си вземаме обратно C указателя, за целта проверяваме дали единствения аргумент е integer. Преди да затворим базата, затваряме и освобождаваме всички sqlite3_stmt отворени към нея. Това ни спестява писане на отделна функция за тази работа. Следваща функция, която ни трябва е sqlite-prepare, тя ще създава sqlite3_stmt обектите (prepared statements).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | |
Тук вече имаме два аргумента на функцията – указател към базата данни и низ – SQL заявка. За да вземем втория правим cdr на списъка, и после отново car. Викаме sqlite3_prepare и ако всичко е наред връщаме указател към sqlite3_stmt. Трябва да можем да вържем стойностите към заявката със sqlite-bind-text.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | |
Аргументите са три, вземаме ги с необходимото количество car и cdr. Това е малко досадно, но с достатъчно мотаене на разни списъци, човек става силен car, cdr caar, cadr, … нинджа. Лепим текста за заявката и ако всичко е наред – връщаме #t. Вече можем да я изпълним със sqlite-step.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | |
Вземаме указател към базата, изпълняваме стъпката и ако всичко е наред – връщаме #t. Остава да напишем sqlite-column-text.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | |
Тук подаваме sqlite3_stmt указател и номер на полето, което искаме да вземем от съответната заявка. Ако всичко е наред връщаме стойността на полето, като низ. Следващата стъпка е да опишем имената на всички функции, така че интерпретатора да може да ги намери, когато заредим библиотеката. С други думи – трябва да регистрираме имената на новите символи. Това става във функцията init_tsx() или съответна за модула init функция (init_sqlite(), ако пишем отделен модул).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | |
Накрая можем да компилираме TSX и да напишем една елементарна тестова програма sqlite.scm:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | |
Този пример тества всички написани до момента функции. Остана да напиша как се правят нови типове данни в интерпретатора. Описание за това има в документацията към TinyScheme, то е разбираемо, но не е много актуално. Дописването на нови типове си заслужава, ако се налага използване на динамична памет. Във всички написани до тук функции никъде не използвахме явно динамична памет. Такава памет се използва от самия SQLite. По тази причина ако използваме често sqlite-prepare ще направим memory leak, защото използваната памет се освобождава чак когато затворим базата. Този проблем може да се реши, ако напишем функция sqlite-finalize. sqlite-close и sqlite-finalize обаче не са в “духа” на езика Scheme. Много по-добре би било, ако самия интерпретатор се грижи за освобождаването на паметта, с помощта на garbage collector. TinyScheme използва Schorr-Deutsch-Waite link-inversion algorithm, от The Art of Computer Programming, том 1. Тип string от езика може да се използва, като пример за създаване на нови типове данни, които да използват динамична памет. На мен ми трябваше тип BLOB, в който да пазя разни парчета неформатирани двойчни данни. Този тип разбира се, може да се ползва и за записване на такива данни в SQLite BLOB полета. За целта можем да допишем функциите sqlite-bind-blob и sqlite-column-blob.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | |
За да работи тази функция, в интерпретатора трябва да имаме дефинирани функциите is_blob(), blob_value() и blob_size(). Човек лесно може да си ги напише, като гледа от съответните string функции. Няма да ги пиша тук, защото ще стане много дълго.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | |
Ето тук е интересната част. Използваме mk_blob() за да регистрираме новия тип в интерпретатора. Освобождаването на паметта е грижа на garbage collectior-а. Ако искаме да бъдем перфекционисти, освен BLOB можем да напишем sqlite3 и sqlite3_stmt, като типове в езика. Аз обаче не съм си играл да го правя. Дори в този си вид TinyScheme и SQLite представляват доста мощно средство за писане най-различни програми. В момента работя по една data mining система. Тъй като SQLite се използва масово в Mac OS X, iPhone OS и Series 60, написана на TinyScheme тя ще може да работи лесно на всички тези платформи.