четверг, 27 ноября 2008 г.

cl-libxml2-0.2.1

Выпустил небольшой релиз cl-libxml2-0.2.1, сделал страницу с примерами кода и все таки, наконец, завел отдельную страницу на cliki: http://www.cliki.net/cl-libxml2.

пятница, 21 ноября 2008 г.

cl-libxml2-0.2

Сегодня выпустил cl-libxml2 версии 0.2

Основное внимание при разработке этого релиза было уделено возможностям расширения, это:
  • Custom URL resolve

  • XPath extension functions

  • XSLT extension elements

Custom URL resolve

Позволяет подсовывать парсеру, когда он желает загрузить какие-то внешние данные, свои данные. Это может потребоваться для реализации какого-то нестандартного протокола, либо, например, для поддержки авторизации, ну да много для чего. Выглядит это примерно так:
CL-USER> (in-package #:libxml2.tree)
#<PACKAGE "LIBXML2.TREE">
TREE> (defun my-resolve-1 (url id ctxt)
(declare (ignore url id))
(resolve-string "<node />" ctxt))

MY-RESOLVE-1
TREE> (defun my-resolve-2 (url id ctxt)
(declare (ignore id))
(if (eql (puri:uri-scheme url) :my)
(resolve-string (format nil "<node>~A</node>" url) ctxt)))

MY-RESOLVE-2
TREE> (with-custom-resolvers (#'my-resolve-2 #'my-resolve-1)
(with-parse-document (doc "<root xmlns:xi=\"http://www.w3.org/2001/XInclude\">
<xi:include href=\"my:doc\" />
<xi:include href=\"my2:doc\" />
</root>")
(process-xinclude doc)
(serialize doc :to-string)))

"<?xml version=\"1.0\" encoding=\"utf-8\"?>
<root xmlns:xi=\"http://www.w3.org/2001/XInclude\">
<node>my://doc</node>
<node/>
</root>
"

XPath extension functions

XPath мощный инструмент, но часто встроенной функциональности не хватает. В таком случае можно добавить пару своих функций:

TREE> (in-package #:libxml2.xpath)
#<PACKAGE "LIBXML2.XPATH">
XPATH> (define-xpath-function hello-world ()
"Hello world!")

HELLO-WORLD
XPATH> (define-xpath-function echo (msg)
msg)

ECHO
XPATH> (define-xpath-function join (delimiter &rest strs)
(iter (for str in strs)
(reducing str
by (lambda (s x) (concatenate 'string s delimiter x)))))

JOIN
XPATH> (with-xpath-functions ((hello-world "hello-world")
(echo "echo")
(join "join"))
(with-parse-document (doc "<root />")
(find-string doc "join('//', hello-world(), '---', echo('Buy!'))")))

"Hello world!//---//Buy!"

XSLT extension elements

Я очень люблю писать на XSLT, совершенно удивительный инструмент, однако и здесь набор имеющихся средств далеко не полон. Не беда, дописать необходимые элементы можно и самому. Например, встроенную функцию copy-of можно реализовать так:

XPATH> (in-package #:libxml2.xslt)
#<PACKAGE "LIBXML2.XSLT">
XSLT> (define-xslt-element copy-of (self input output)
(iter (for node in-xpath-result (attribute-value self "select") on input)
(append-child output (copy node))))

COPY-OF
XSLT> (with-xslt-elements ((copy-of "copy-of" "www.sample.org"))
(with-stylesheet (style "<?xml version=\"1.0\"?>
<xsl:stylesheet xmlns:xsl=\"http://www.w3.org/1999/XSL/Transform\" xmlns:my=\"www.sample.org\" extension-element-prefixes=\"my\" version=\"1.0\">
<xsl:template match=\"/root\">
<result><my:copy-of select=\"node()[@attr]\" /></result>
</xsl:template>
</xsl:stylesheet>")
(with-parse-document (doc "<root><a attr=\"1\"/><b /><c attr=\"2\"/><d /></root>")
(with-transfom-result (res (style doc))
(serialize res :to-string)))))

"<?xml version=\"1.0\" encoding=\"utf-8\"?>
<result>
<a attr="1"/>
<c attr="2"/>
</result>

Резюме

Кстати, "custom URL resolving" и "XPath extension functions" работают и внутри XSLT.
Когда-то меня впечатлили соответствующие возможности библиотеки lxml: оказалось что реализовать сопоставимый функционал не так уж и сложно :-)

среда, 12 ноября 2008 г.

Управление ресурсами в Common Lisp

Вот за что люблю C++, так это за деструкторы (ну, конечно, не только за это). Последовательное следование принципу RAII привело к тому, что в течении последних нескольких лет активного использования C++ я не имел ни одной проблемы, связанной с утечкой ресурсов, даже когда писал с использование ObjectARX (Autocad), а это жуткий монстр, настоящее минное поле: шаг в сторону (ну в документации то про это ни слова) и все - лезут проблемы, но в каком-нибудь совершенно другом месте... Поэтому все байки о том, что при программировании на C++ есть какие-то большие проблемы с управлением ресурсами никогда всерьез не принимал: так было когда-то давно (ну когда компьютеры были большими, а программы маленькими), сейчас же достаточно просто последовательно использовать современным концепциям (RAII, интеллектуальные указатели, безопасная обработка исключений) и все эти рассказы о трехсуточных отладках можно смело отнести к области программистского фольклора былых дней.

Ну да ладно, что-то ударился в воспоминания: уже больше года на C++ не пишу (и рад бы, да не знаю куда его можно применить, не над теми задачами сейчас работаю). В Lisp это всё якобы не имеет большого значения, ибо есть сборка мусора. Нет, ну может оно так и есть для Lisp-машин, но я с ними не работал. А реальность состоит в том, что в основе значительной части современной IT-инфраструктуры лежит язык "C". А значит, сокеты, каналы, соединения с базой и т.п. сборщик мусора никак обработать не может. Но даже если бы мог, то вряд ли бы это улучшило ситуацию, часто эти ресурсы необходимо освобождать в четко определенные моменты и не ждать когда же запуститься сборка мусора (вот не закроешь канал и останется висеть дочерний процесс в памяти, и последствия мало предсказуемы: вдруг он ждет завершения ввода для начала выполнения основной работы).

Вывод из этого только один: сборка мусора может и хорошо, да только её мало, необходимо иметь более общий (и явно контролируемый) механизм для управления ресурсами.

"Lisp way" для решения этих проблем заключается в использовании макросов with-... Часто такой способ и удобен и эффективен. Но не всегда. Возьмем такой надуманный (и довольно глупый) пример:
(defun myprint (text stream)
(write text :stream stream))
Что будет, если в качестве stream будет передан nil? Ну, текст будет выведен в *standard-output*. А если необходимо другое? Скажем, как нибудь так:
(defun myprint (text stream)
(if stream
(write text :stream stream)
(with-open-file (out (print (sb-posix:mktemp "/tmp/XXXXXX")) :direction :output)
(write text :stream out))))
Т.е. мы получили дублирование кода. Благо это пока только одна строка (write text :stream stream). А если там не одна строка? А много-много строк? Ну хорошо, можно сделать так:
(defun myprint (text stream)
(flet ((myprint-impl (text stream)
(write text :stream stream)))
(if stream
(myprint-impl text stream)
(with-open-file (out (sb-posix:mktemp "/tmp/XXXXXX") :direction :output)
(myprint-impl text out)))))
Вроде всё хорошо? Ну а если в функцию передается несколько потоков и каждый из них может иметь значение nil и необходимо для каждого такого параметра создавать поток самостоятельно. Вот тут начинаются проблемы, ибо with-open-file нам больше не поможет (разве что только через совершенно жуткий код, пример которого я даже приводить боюсь). Нет, ну по настоящему мужественные люди возьмут в руки unwind-protect, что фактически эквивалентно переходу на ручное управление ресурсами, и без особых проблем напишут необходимый код. Только я так делать боюсь. Ибо это требует аккуратного кодирования, а славное прошлое C++-программиста научило меня избегать подобных вещей.

Подобной ситуации с потоками у меня пока ещё не было (да и вряд ли будет, пример то надуман), но схожие ситуации при работе, например, с cffi встречают регулярно, и выглядит это примерно так:
(defun myfun (str1 str2 str3 str4)
(cffi:with-foreign-strings ((%str1 str1) (%str2 str2) (%str3 str3) (%str4 str4))
(c-фунция %str1 %str2 %str3 %str4)))
Всё отлично, вот только с-функция может принимать, как это часто бывает, в том числе NULL-ы, а значит в myfun можно передать несколько nil. Для передачи NULL в с-функцию необходимо использовать (cffi:null-pointer), но вот беда - cffi:with-foreign-strings ломается, если туда передавать nil. Приходиться брать в руки unwind-protect и заниматься ручным управлением памятью, т.е. почувствовать себя "многострадальным С-программистом", со всеми вытекающими.

Однако, "многострадальные С-программисты" не так просты и их страдания малость преувеличены :-) В частности, разработчики Apache, для которых "ресурсы" святы (к чему приведут утечки ресурсов за несколько месяцев работы?) уже давно придумали эффективный и удобный механизм, который можно для краткости назвать APR Pools. Не вдаваясь в подробности, суть в том, что при выделении ресурса его необходимо немедленно зарегистрировать в пуле (их есть несколько, для запроса, для соединения и т.п.) и система автоматически освободит его (ресурс) при разрушении этого пула. Всё, имеющийся API делает проблему управления ресурсами тривиальной.

Надо же, С-хакеры оказались в этом куда более продвинуты :-) Ситуация является неприемлемой. Эта мысль привела меня к созданию garbage-pools: реализации подобия APR Pools на Common Lisp. С помощь этого несложного инструмента предыдущий пример можно написать так:
(defun myfun (str1 str2 str3 str4)
(flet ((foreign-string (str)
(if str
(gp:cleanup-register (cffi:foreign-string-alloc str)
#'cffi:foreign-string-free)
(cffi:null-pointer))))
(gp:with-garbare-pool ()
(с-функция (foreign-string str1)
(foreign-string str2)
(foreign-string str3)
(foreign-string str4)))))
garbage-pools имеет совсем небольшой API:
  • pool - класс пула ресурсов
  • with-garbare-pool - макрос создающий и разрушающий пул. Возможно использование как именованных, так и безымянных пулов, например:
    (with-garbare-pool ()
    (cleanup-register myobj clenup-fun))
    (with-garbare-pool (mypool)
    (cleanup-register myobj clenup-fun mypool))
  • cleanup-register - регистрирует в пуле объект и "уничтожающую"-функцию
  • cleanup-pool - вызывает разрушение всех зарегистрированных объектов
  • cleanup-object - вызывает "преждевременное" разрушение зарегистрированного объекта
  • object-register - "generic"-метод для регистрации объектов в пуле без указания функции "разрушителя". Это имеет смысл, когда функцию-разрушитель можно определить по типу объекта (из коробки поддерживаются только stream и pool)
  • defcleanup - макрос, позволяющий сопоставить указанному классу функцию-разрушитель (после чего объекты данного классы можно будет регистрировать с помощью object-register). Пример:
    (defcleanup stream #'close)


Lisp Logo

Довольно случайно наткнулся на забавный Lisp Logo. Выглядит это так (на самом деле там несколько вариантов):



Такая картинка показалась мне гораздо интереснее, чем унылый



Посему, оное "страшное" предупреждение и было добавлено на главную страницу cl-libxml2 :-)