За прошедшую неделю в думах над моим проектом в голове накопилось несколько мыслей, о которых мне хотелось бы сейчас написать:
стоит ли стремиться к строгой работе с GDB/MI?
окоченевшие строки, или в поисках Лиспа;
как заблудиться в исходниках GNU Emacs;
Special: Java-ад расширения Eclipse CDT.
Много думал, много расстраивался, но в итоге что-то, кажется, понял. Закрепляю мысли сей заметкой.
В прошлой заметке я говорил про то, что решил отображать ответы GDB/MI в рекурсивные списковые структуры Emacs Lisp, состоящие из вложенных друг в друга ассоциативных и не очень списков. Такого вида, например:
Например, если (условная) команда MI -basket-info выводит в терминал
что-то вроде:
{basket=[{color="green",taste="delicious"},{color="red",taste="disgusting"}]}
то этот ответ будет переведён в структуру basket следующего вида:
(defvar basket '((apples . (((color . "green")
(taste . "delicious"))
((color . "red")
(taste . "disgusting"))))))
В настоящий момент я использую входящий в состав Emacs парсер JSON для выполнения этого преобразования.
Обращение к членам структуры я сначала делал с помощью простеньких самописных процедур навроде такой:
(fadr-member basket ".apples[0].color")
=>
"green"
Проблемы, на которые указали разработчики Emacs, побудили
меня ещё раз подумать над возможностью применения вместо этого других
функций, например defstruct и Ко из пакета cl.
Для структурирования данных под Emacs Lisp есть средства более
высокого уровня, например EIEIO или макросы defstruct из пакета
cl. Их плюсом является то, что при работе со структурами мы
бесплатно получаем простеньку проверку типа структуры (то есть не
удастся попросить поле X у структуры, которая обладает только полем
Y). Это придаст большую строгость общению Emacs и GDB. А с EIEIO ещё
и всякое наследование, ну почти все CLOS-овые дела в общем.
Сейчас я просто перевожу всю структуру из ответа MI в списки. Когда я извлекаю из этих списков значения полей, я делаю предположение о том, что там эти поля есть, на основе того, что написано в мануале по GDB/MI и того, что MI выплёвывает на самом деле. Код устроен так, что если в MI поменяется состав выходных записей, моё предположение может стать ошибочным, и код молча сломается. Но зная характер разработки MI, я этого не боюсь :)
С другой стороны, как бы я поступил при использовании defstruct или
EIEIO?
С defstruct/defclass мне потребовалось бы сначала описать схемы
ответов (список наличествующих в них полей). Использумых Емаксом
команд MI много, так что таких описаний (и произведённых от них
конструкторов/селекторов) пришлось бы использовать тоже много.
В то же время, в MI состав ответа для одной и той же команды может
быть неоднородным: например, в выводе команды -break-info для
разных точек останова информация о файле/строке может присутствовать
или нет, в зависимости от того, куда была эта точка останова
поставлена. Вот кусочек:
[{number="1",
type="breakpoint", disp="keep", enabled="y", addr="0x08048458",
at="<printf@plt>",
times="0",original-location="printf"},
{number="2",
type="breakpoint", disp="keep", enabled="y", addr="0x0804859d",
func="main", file="test.c",
fullname="/home/sphinx/.emacs.d/elpa/gdb-mi-0.5/test.c", line="14",
times="0", original-location="test.c:14"}]
Получается, на каждую команду понадобится даже не одна схема, а
несколько, а определять необходимую к использованию придётся во время
исполнения согласно тому, какие поля на самом деле есть в выводе (если
описать в схеме все возможные поля, уменьшится строгость, к которой
мы стремимся при использовании defstruct или defclass)!
Получается, что мы не требуем от вывода MI удовлетворения какому-то
контракту, а выбираем контракт на основе того, что у нас в выводе!
Дополнительные затраты будут связаны с тем, что в MI ответы на самом деле имеют не единичную глубину вложенности, так что потребуется описывать схему для каждого уровня.
Раз возможность повышения строгости кода под вопросом, просто ради
селекторов смысла использовать defstruct или EIEIO я не вижу.
Было бы хорошо, если бы MI был грамотно спроектирован (а не как сейчас) и имелось бы формальное описание всего протокола, по которому бы я мог сгенерировать нужный код на Лиспе. Но всего этого нет, хотя мне, возможно, хотелось бы в будущем сделать что-то на эту тему.
Прежде всего, цитата Алана Перлиса:
The string is a stark data structure and everywhere it is passed there is much duplication of process. It is a perfect vehicle for hiding information.
Действительно плохой идеей было использование С-подобной нотации для доступа к моим структурам. Было так:
(fadr-member basket ".apples[0].taste")
=>
"delicious"
При написании этого кода я стремился к компактности и (с привычной для меня точки зрения) наглядности нотации. Когда-то любимые точечки с квадратными скобками придавали стройным рядам Лисп-смайликов пикантность.
Но так делать плохо.
Был однажды тред на тему такой нотации на ЛОР. Использование строк для реализации какой-либо нотации показалось мне «неспортивным» ещё тогда :)
Один из Emacs-хакеров сказал, что такая нотация чужеродна, уродливая и совсем-совсем не круглоскобовская.
Тут же оперативно начатый тред в c.l.l помог мне взглянуть на вещи трезво.
Выбранная мной нотация имеет следующие проблемы:
для обработки приходилось использовать операции на строках и регулярные выражения, которые чужеродны Лиспу, в котором принято оперировать символами и списками (отсюда дублирование («duplication of process»));
мои строки находятся на некотором удалении от обычных Лисповских кододанных (они как будто прячут что-то под одеждой («vehicle for hiding information»)).
Паскаль Б. указал на неожиданную (не думал о таком раньше) для меня проблему с инъекцией вредоносного кода в макросах.
Забавно, но в Emacs есть пакет bindat, который, казалось бы,
предназначен для работы с какими-то там упакованными бинарными
данными. Но в его составе нашлась функция bindat-get-field, которая
делает то же самое, что fadr-member, только с нормальным синтаксисом
(никаких строк!):
(bindat-get-field basket 'apples 0 'taste)
=>
"delicious"
Эта нотация не длиннее использовавшейся ранее ни на символ, но при этом хорошо вписывается в окружающую Лисп-среду. Пока что я решил использовать её.
Заглянул в код Eclipse CDT, связанный с MI.
Дерево исходников с глубиной вложенности в более чем десять директорий (ну это, похоже, так везде, где Java). Пиздец.
Четырнадцать тысяч строк мусора на Java. Пиздец.
На каждую MI-команду свой класс, который при этом парсит почти совершенно по факту, без всяких строгих схем (как в моём коде сейчас).
Этот шокирующе многословный и громоздкий пример позволяет мне думать, что в моём коде, наверное, не всё уж так и плохо, а Emacs Lisp не так уж и страшен, как кажется на первый взгляд.
Может я чего ещё и не понимаю в составлении программ, но следовать примеру CDT мне что-то сейчас совсем не хочется. Как и иметь дело с Java в каком-либо виде.