Автогенерация кода или улетные шаблоны в Си

Давным-давно в далекой-далекой галактике попались мне исходники не помню чего, у которых в шапке стояла пометка:
// Generated Automaticaly by xxx, Do not edit

И так мне эта идея понравилась, что начал копать на тему автогенераторов кода.
Ведь как было бы хорошо, если бы вместо убогого препроцессора были возможности нормального языка, исполняемого на этапе компиляции.

Но что-то все попавшиеся под руку средства были какие-то слишком заумные, так что отпустило довольно быстро.
И вот недавно, в процессе какого-то обсуждения, один хороший товарищ dxp подкинул наводку на очень интересный инструмент.

Cog — это инструмент для генерации исходных текстов программ. Он позволяет вам использовать небольшие фрагменты программ на языке Python в качестве генераторов в вашем исходном коде. Такие генераторы могут создавать любой код, который вам нужен.

Идея состоит в том, что прямо в тексте программы вставляется небольшой python-овский скрипт, заключенный в специальные теги. Затем файл обрабатывается другим (внешним) скриптом, после чего в результате работы внутреннего скрипта в исходном (или новом) файле появляется сгенерированный код. При этом можно использовать всю мощь питона, включая дополнительные библиотеки и модули.

В целом мне понравилось, но времени (да и особой надобности) не было, чтобы попробовать.
До вчерашнего дня.


А вчера я вплотную занялся новым проектом, в котором надо формировать ЦАПом синус. Обычно в таких случаях берется Excel, MathCAD или у кого чего есть (или кто в чем умеет), высчитывается таблица и переносится в текст программы.

Вот и я — открыл эксель (Libre Calc), заполнил несколько клеточек подготовительными расчетами… И что-то тоска напала. Некрасиво, неоптимально, да и вообще муторно. «Все не так, все тупо и плоско (C)». И тут вспомнил про отложенный до времени Cog.

Скажу сразу, что это был мой первый опыт писательства на питоне (до этого только читал). Поэтому пришлось привыкать к новому синтаксису. Но потом все пошло быстро и гладко. В конце концов родился такой текст:

/**
*   Таблица данных для синтеза синусоиды ЦАПом
*   Формируется автоматически генератором COG
*/
/*[[[cog
import cog
import math
col = 5
row = 10
N = col * row
Min = -2048
Max = 2047
Med = (Min + Max) / 2
Amp = (Max - Min) / 2

cog.out('#define SIN_TABLE		{')

for i in range(N):
	Phase = i * 2 * math.pi / N 
	if i % col == 0:
		cog.outl('	\\')
		cog.out('	')
	cog.out('	%5d' % math.floor(Med + Amp * math.sin(Phase)))
	if i < N-1:
		cog.out(',	')

cog.outl('}\n')

cog.outl('#define SIN_TABLE_SIZE          %d' %N)
]]]*/

//[[[end]]]


Думаю, не надо объяснять, что он должен делать. Но я все-таки прокомментирую.
Сначала задаются рабочие параметры — формат таблицы, границы кодов ЦАП и вспомогательные производные величины.
Затем в цикле рассчитываются значения и выводятся в файл через функции cog.out() и cog.outl(). Разница между этими функциями — в переносе строки. Вот, собственно и все. Вывод осуществляется непосредственно после скрипта, между тегами ]]] и [[[end]]].

Запускаем скрипт:
cog.py -r gen.h


Ключик -r нужен для того, чтобы записать выход скрипта в исходный файл, иначе вывод будет осуществляться в поток запускающего приложения (например, в консоль отладки). Также, можно с помощью разных ключей направлять вывод в другие файлы, передавать скрипту параметры, удалять из полученного файла текст скрипта и т.д.

Смотрим, что получилось:
]]]*/
#define SIN_TABLE		{	\
		   -1,		  255,		  508,		  752,		  985,		\
		 1202,		 1400,		 1576,		 1727,		 1851,		\
		 1945,		 2009,		 2041,		 2041,		 2009,		\
		 1945,		 1851,		 1727,		 1576,		 1400,		\
		 1202,		  985,		  752,		  508,		  255,		\
		   -1,		 -258,		 -511,		 -755,		 -988,		\
		-1205,		-1403,		-1579,		-1730,		-1854,		\
		-1948,		-2012,		-2044,		-2044,		-2012,		\
		-1948,		-1854,		-1730,		-1579,		-1403,		\
		-1205,		 -988,		 -755,		 -511,		 -258}

#define SIN_TABLE_SIZE          50
//[[[end]]]

Очень даже неплохо.
Теперь добавляем в gen.c строчку
static const int SinTableData[SIN_TABLE_SIZE] = SIN_TABLE;

— и вуаля, таблица размещена в программной флеш-памяти.

Допустим, я захочу поменять таблицу. Например, проредить. Все-таки частота выходного синуса 10кГц, и несчастный MSP430 может надорваться, даже с DMA.
Нет ничего проще:

col = 5
row = 4


Запускаем, готово:
]]]*/
#define SIN_TABLE		{	\
		   -1,		  631,		 1202,		 1655,		 1945,		\
		 2046,		 1945,		 1655,		 1202,		  631,		\
		   -1,		 -634,		-1205,		-1658,		-1948,		\
		-2048,		-1948,		-1658,		-1205,		 -634}

#define SIN_TABLE_SIZE          20
//[[[end]]]


В данной статье показан только один пример применения этого полезного инструмента, но при некоторой фантазии можно придумать множество других. Что я и собираюсь сделать в ближайшем будущем, при наличии интереса к теме, естественно.
  • +6
  • 20 апреля 2012, 14:49
  • MrYuran

Комментарии (38)

RSS свернуть / развернуть
Любопытно. Надо будет поглядеть.
0
  • avatar
  • evsi
  • 20 апреля 2012, 15:10
Я тоже пытаюсь использовать Питон для кодогенераци. Пробовал Cog, вобщем штука неплохая, но в большинстве случаев оказывается не очень полезен. Без него часто бывает проще и пример в статье это подтверждает: заменить cog.out на print в файл… и будет просто питоносвкий скрипт с подсветкой синтаксиса и пошаговой отладкой.
0
В принципе да, другой вопрос, что в плюсах подобными вещами должны, по идее, заниматься шаблоны. Вот только там решение подобных задач выглядит как black magic. Было бы куда удобнее, если бы можно было прямо сказать «при таких и таких условиях генери то, а при вот таких — это». И код был бы несравнимо читабельнее, и костыли вроде того же Cog-а были бы не нужны, не говоря уже о бесконечном количестве паттернов, которые применяются специально для написания шаблонов на плюсах. Александреску сотоварищи, конечно, зачет за фантазию, но, блин, читать это невозможно.
0
В принципе да, другой вопрос, что в плюсах подобными вещами должны, по идее, заниматься шаблоны.

Ну, в данном случае (заполнение таблицы значений синуса), С++ шаблоны не помогут. Разве что писать свою реализацию sin().
0
Не уверен на 100%, но, думаю, если задаться целью, можно попробовать сгенерить таблицу рекурсивным вызовом шаблонов или еще каким извращением. По поводу синусов, то генерация констант полностью отдана на откуп компилятору и, помнится, gcc знает и умеет инлайнить некоторые математические функции, так что вполне может быть, что он это дело построит во время компиляции. Могу ошибаться, конечно, давно не колупался в компиляторе так глубоко.
0
сгенерить таблицу рекурсивным вызовом шаблонов или еще каким извращением.
Вы правы. Теоретически можно заинлайнить sin(), например, чрез разложение в ряд Тейлора. Только, по сути, это и есть своя реализация sin()

gcc знает и умеет инлайнить некоторые математические функции

Теоретически возможно. Но, в любом случае, стандартом С/С++ «встроенной» реализации таких функции не предусмотрено. Соответственно, такой код не будет переносим.
0
Вы правы. Теоретически можно заинлайнить sin(), например, чрез разложение в ряд Тейлора. Только, по сути, это и есть своя реализация sin()
Я имел в виду генерацию таблицы как таковой, а не разложение самой функции. Впрочем, это вполне возможный вариант.
Соответственно, такой код не будет переносим.
Ну, вобщем, я бы забил, если честно. Для инетесных мне микроконтроллеров есть gcc, вот под него бы и точил.
0
Питон это вещь))) сам на нем уже несколько лет пишу )) рулез)
0
Подобные описенной в статье задачи (формирование массивов данных) выполняю:
— на том же Си, отдельным файлом/проектом. Что позволяет использовать один компилятор, а не качать с инета и всюду ставить десяток сред разработки.
— специально написанной программой конвертером. На случай когда данные надо менять. А данные это не только синусы, это и изображения и бинарники.
— скриптами на Lua. Всё же некоторые вещи легче реализовываются в скриптовых языках, нежели в чистом Си.

Вставлять в С/С++ (любой другой) файл кусок стороннего кода считаю несколько не правильным действием. Хотя вебпрограммеры со мной явно не согласятся.
0
А еще, может кому интересно, я написал на Питоне скриптик, который парсит каталог заголовочных файлов с описанием периферии МК, чтоб определить наличие модулей и возможностей.
Например, такой код напечатает дефайны всех контроллеров, у которых порт №1 имеет регистр управление подтяжкой:
devsHeaders = parse_main_header('D:\\Utils\\mspgcc-20110716-p20111105\\msp430\\include\\msp430.h')
pettern = "P1REN"
for dev in match_devs(devsHeaders, pettern):
   print dev

Вывод типа такого:
__MSP430F6636__
__MSP430X5502__
__MSP430_5327__
__MSP430G2413__
...
__MSP430X2471__
__MSP430X2533__
__MSP430F2617__
0
Симпатичное на первый взгляд решение. Спасибо.
0
Да, еще форматирование строк в стиле ' %5d' % foo у питонщиков считается анахронизмом. И считается оправданным, только если нужна совместимость с Сишным printf. Рекомендуется использовать форматирование вида «hello, {0}».format(«world»). Говорят, оно более кошерное :)
-1
Тут ведь, по сравнению с шаблонами и прочими заморочками, какое преимущество — все в наших руках. Можно написать небольшую библиотеку и переплюнуть Александреску. Замктить не фабрику, а концерн классов :)
Что первое приходит на ум:
Шаблоны новых функций с автоматической обвеской doxygen
Макросы Волкова с объединением пинов в виртуальные порты (ну, вы понимаете, кто в теме :) )
Что-нибудь еще. Питонопрототреды :)
0
Чрезмерно увлекаться автогенерацией кода тоже не стоит. В приведенном вами примере, использование «автогенератора» выглядит уместно. Но если увлечься автогенерацией — то в результате у вас будет жуткая помесь из шаблонов на питоне и кода на С. Поддерживать такой код будет очень сложно.
+1
По моему скромному мнению, использование автогенераторов оправдано только когда проект должен собираться автоматически, скажем, на специальной тестовой тачке, раз в сутки, с прогоном всех внешних тестов.
А для единождого встраивания в программу куска кода достаточно написать скрипт на ЛЮБОМ языке с выводом в stdout, после чего, например, направить его в файл и copy/paste в текст исходника. В особо извращённых случаях, подобные скрипты могут даже принимать внешние параметры и генерить не только куски исходников.
0
Вполне возможен и такой вариант: исходником является код/скрипт на ДРУГОМ языке (возможно с включением кусков кода на С/С++), а в процессе сборки он генерит код на С/С++. Весьма широко распространенный пример — lexx/flex/yacc/bison/etc.
0
Тут уже и сказали…
Вставлять в файл понятный лишь одному (причё левому) препроцессору код — неправильно.
Лучшим вариантом было бы вынесение Python-кода в отдельный файл и генерация им заголовочного файла с нужными данными/кодом.
Тогда тот, кто не имеет питона, смог бы использовать готовый сгенерированный файл. А кто имеет — смог бы внести нужные изменения сам.
Запускать это дело несложно из Makefule.
Так что имхуется мне, что всё это некоторое извращение (:
0
  • avatar
  • knkd
  • 22 апреля 2012, 20:10
И да, при этом в Python-коде нежелательно использовать что-то кроме стандартной библиотеки.
0
Тогда тот, кто не имеет питона, смог бы использовать готовый сгенерированный файл.
Так он и так может использовать.
Cog — скрипт находится в комментариях. А сгенерированный код располагается ниже.
Возможность генерить в отдельный файл есть. Также можно поставить ключик, который удаляет сам скрипт из полученного файла, так что никаких следов «левого препроцессора» не останется.
И, как по мне, так намного удобнее все иметь в одном месте. Чем разыскивать потом разнообразные экселевские, матлабовские и прочие файлы. Кстати говоря, питон свободно качается и устанавливается за полминуты, в отличие от всяческих других.

Кстати, насчет makefile…
В настоящее время колупаю scons (система сборки на основе питона), по результатам может тоже чего-нибудь выложу.
Те, кто переполз на scons, вспоминают make как страшный сон. И опять же, мощь змеюки, вплоть до графическтх интерфейсов.
0
Вообще мейкфайлы это, блин, такой каменный век… Все собираюсь попробовать собирать проекты Maven-ом или даже Gradle…
0
И то и другое выглядит (не имел дела с ними) как девятиногое чудище в сравнении с make. Оказывается есть люди готовые перейти на такое только из-за возраста make.
0
Я имел дело с make (причем много), Ant (который, по принципам работы тот же make) и Maven (который имеет совсем другие принципы в основе). Gradle игрался, но в работе пользоваться не доводилось. По принципам работы это тот же Maven, только синтаксис конфигурации существенно удобнее. Это, так сказать, предыстория, что бы было понятно, что мой выбор основан не на возрасте, а на опыте использования.
Так вот, если в двух словах, то make фтопку. Если более развернуто, то make крайне неудобен и многословен, простейшие задачи в нем решаются коряво, через одно место и с большими трудозатратами. Если сравнивать make и Maven с более привычными вещами, то разница между ними примерно как написание программ в шестнадцатеричных кодах в терминале без бекспейса и написание программ на жабе в IDE. Для Maven-а не нужно описывать зависимости между отдельными файлами, он оперирует такими понятиями как жизненный цикл проекта и его фазы. Минимальный файл конфигурации содержит, если мне правильно изменяет память, только указание на то, какого типа таргет собирается (например, библиотека или готовая программа), версия и название, при необходимости еще указываются библиотеки, от которых зависит программа. Все остальное он в состоянии сделать сам — скомпилировать, прогнать тесты, вытащить и разложить как нужно зависимости, упаковать таргет так, как положено именно для этого таргета. Поскольку он в курсе жизненного цикла программы, то нет необходимости создавать промежуточные таргеты как в мейке, только для того, что бы, например, только скомпилировать или скомпилировать и прогнать тесты, собрать отладочную или релизную версию или зачистить хвосты после сборки. Полноценный мейкфайл, который был бы в состоянии воспроизвести все известные мейвену фазы жизненного цикла проекта занял бы не один десяток, если не сотен килобайт. Например, мейвен в курсе того, что на определенных этапах может генерится код (например, с помощью приблуд обсуждаемых в топике). Кроме того, мейвен «искаропки» понимает многомодульные проекты и умеет их правильно собирать. В мейвене никому и в голову не взбредет описывать зависимости для отдельного исходника, добавление нового сорса в проект не требует каких-либо указаний мейвену, он сам с этим разберется. Замечу, что все описанное выше это только базовая функциональность, к мейвену существует бесчисленное множество плагинов, позволяющих решать специфические для конкретного проекта задачи, если вдруг возникает необходимость. Ах да, чуть не забыл, хотя это одна из центральных фишек мейвена — он знает о версиях всех участвующих в процессе сборки компонентов, как частей проекта, так и внешних зависимостей. Это значит, что программа которую вы собираете, в один прекрасный момент не начнет падать только потому, что накануне вы обновили что-то в системе и теперь под старым именем лежит совсем другая версия библиотеки идущей с системой.
Ну и еще одна особенность, которая выгодно отличает мейвен от мейка — сложность управления процессом сборки растет значительно медленнее роста сложности самого проекта. С мейком ситуация строго обратная, даже при очень грамотном подходе, сложность управления процессом сборки, в лучшем случае, растет пропорционально сложности проекта, а если этому не уделять должного внимания, то сложность сборки растет чуть ли не как квадрат сложности проекта. К тому же мейкфайлы имеют тенденцию быстро превращаться в черный ящик, в котором никто давно уже ничего не понимает и максимум может добавить новый исходник, а любая попытка что-то доделать или изменить означает переписать все мейкфайлы заново.
+2
А что ты скажешь про упоминавшийся где-то в сообществе CMake?
0
Я его, увы, не ковырял, что бы сложить о нем полноценное мнение. Из того, что я видел, предварительно у меня сложилось впечатление, что это мейк на стероидах. Если первое впечатление верно, то это тупиковый вариант развития.
0
А что ты думаешь о системах сборки в IDE, у который в основе менеджер проекта? Таких скажем, как в MSVS или Delphi.
0
Увы, не пользовался ни тем, ни другим. Точнее, с дельфи когда-то игрался (как и с турбопаскалем) но деталей относящихся к управлению проектом просто не помню.

P.S. к слову, мейвен, хоть и заточен был изначально под проекты на жабе, принципиально этими проектами не ограничен, под него есть так называемый native плагин, который добавляет поддержку типичных бинарных таргетов типа so, dll, exe, lib и так далее, что позволяет собирать как проекты с компонентами собирающимися в нативный код, так и проекты полностью из таких компонетов. Применительно к более близкой тематике это означает, его вполне можно приспособить для сборки проектов под микроконтроллеры.
0
У меня на работе сейчас Мавен используется, причем для сборки Си шарпа. Надо сказать, что сам по себе Мавен не умеет ровным счетом ничего, кроме разбора pom файлов, отслеживания зависимостей проектов, вызова плагинов работы с репозиторием. Всю работу делают плагины, коих понаписано великое множетво. Вот мы и писали плагины к нему для сборки C# и WIX.
Мавеновский репозиторий — это отдельный разговор. С одной стороны удобно, с другой — результат сборки зависит не только от исходников, но и от того, что есть в репозитории. Иногда это становится причиной некорректных сборок с трудно находимыми багами.
0
Насчет плагинов это верно. Я просто не считал нужным вдаваться в настолько глубокие подробности мейвена.

Репозитории это отдельная тема. Если нужно все абсолютно надежно, имеет смысл настроить локальный репозиторий и все сборки делать только через него. Впрочем, с проверенными репозиториями проблем я не встречал. Хотя, возможно, это специфика именно Java-репозиториев (конторы, ведущие основные публичные репозитории, проводят весьма жесткую политику в плане качества того, что лежит в репозиториях).
0
То есть так и есть, для всего этого, они должны быть очень сложны внутри.
0
Вовсе нет. Да и не с чего, на самом деле.
0
Если удалять скрипт из файла, то непонятно другое — зачем его туда вставлять? (:

Разыскивать файлы не надо, если они имеют строго оговоренные имена и лежат по своим директориям.
Наоборот — искать в одном файле исходники на разных языках, это не нормально)
0
Разыскивать файлы не надо, если они имеют строго оговоренные имена и лежат по своим директориям.
Дадад…
Все началось с того, что в исходнике прошлого проекта была прошлая таблица и комментарий:
// Таблица генератора SIN. Исходные данные в файле Расчеты.xls

В оговоренном месте искомого файла найдено не было, новый xls писать — тоска взяла, ну и собственно результат подробно описан выше.

Зачем стирать?
Ну, допустим, скрипт предполагается запускать одноразово для генерации, скажем, нового модуля со скелетами функций.
То есть, мы задаем имя модуля, имена функций, можно ещё чего-нибудь (например, базовые комментарии), а скрипт формирует новый модуль .c + .h, с заданными шапками, со скелетами функций и прочей требухой типа шапок и блоков doxygen. Плюс, самое главное, что все это будет однообразно и унифицированно, и не будет зависеть от настроения или фазы луны.
Точно так же можно вставлять отдельные функции или блоки текста или кода.
Грубо говоря, можно наделать кучу визардов, которые будут абсолютно инвариантны относительно среды разработки, т.к. python (или что-либо другое, не суть) везде работает одинаково.
0
Ну про то, что генерация в Эксиле, это извращения, я и не спорил.

Если генерировать костяк приложения, то генератор должен быть совершенно внешним. Зачем его вставлять в файл, чтобы потом оттуда же удалить? :)

Возражения вызывает не генерация кода как таковая, а использование для этого ненужного инструмента.
0
Что посоветуете почитать для изучения Python-а?
0
  • avatar
  • Aneg
  • 22 апреля 2012, 22:32
Dive into python
0
Мне, например, всегда хватало Excel. Только я не тупо делал таблицу, а писал рядом функции (на базе функции СЦЕПИТЬ() ), чтобы собрать таблицу в код. Простым копипастом потом можно вставлять.

Ниже пример (с учеником делали «умный-преумный» дом, точнее автозвонки в школе). Всё сделано Excel'ем, вставляется простым копипастом:


int Alarms[CountAlarms] = {510, 555, 565, 610, 620, 665, 675, 720, 
730, 775, 785, 830, 1950, 1995, 2005, 2050, 
2060, 2105, 2115, 2160, 2170, 2215, 2225, 2270, 
3390, 3435, 3445, 3490, 3500, 3545, 3555, 3600, 
3610, 3655, 3665, 3710, 4830, 4875, 4885, 4930, 
4940, 4985, 4995, 5040, 5050, 5095, 5105, 5150, 
6270, 6315, 6325, 6370, 6380, 6425, 6435, 6480, 
6490, 6535, 6545, 6590, 7710, 7755, 7765, 7810, 
7820, 7865, 7875, 7920, 7930, 7975, 7985, 8030, 
15000, 9149, 9150, 15000, 15000, 15000, 15000, 15000, 
15000, 15000, 15000, 15000, 15000, 15000, 15000, 15000, 
15000, 15000, 15000, 15000, 15000, 15000, 15000, 15000, 
15000, 15000, 15000, 15000, 15000, 15000, 15000, 15000, 
15000, 15000, 15000, 15000, 15000, 15000, 15000, 15000, 
15000, 15000, 15000, 15000, 15000, 15000, 15000, 15000, 
15000, 15000, 15000, 15000, 15000, 15000, 15000, 15000};   // Массив будильников. Время от 1 (00:01 пн) до 10079 (23:59 вс)

byte AlarmActions[CountAlarms] = {3, 3, 3, 3, 3, 3, 3, 3, 
3, 3, 3, 3, 3, 3, 3, 3, 
3, 3, 3, 3, 3, 3, 3, 3, 
3, 3, 3, 3, 3, 3, 3, 3, 
3, 3, 3, 3, 3, 3, 3, 3, 
3, 3, 3, 3, 3, 3, 3, 3, 
3, 3, 3, 3, 3, 3, 3, 3, 
3, 3, 3, 3, 3, 3, 3, 3, 
3, 3, 3, 3, 3, 3, 3, 3, 
3, 1, 0, 255, 255, 255, 255, 255, 
255, 255, 255, 255, 255, 255, 255, 255, 
255, 255, 255, 255, 255, 255, 255, 255, 
255, 255, 255, 255, 255, 255, 255, 255, 
255, 255, 255, 255, 255, 255, 255, 255, 
255, 255, 255, 255, 255, 255, 255, 255, 
255, 255, 255, 255, 255, 255, 255, 255};   // Массив действий. 0 - выключить; 1 - включить; 2 - переключить; 3 - включить, ждать 5 сек, выключить

byte
AlarmTargets[CountAlarms] = {12, 12, 12, 12, 12, 12, 12, 12, 
12, 12, 12, 12, 12, 12, 12, 12, 
12, 12, 12, 12, 12, 12, 12, 12, 
12, 12, 12, 12, 12, 12, 12, 12, 
12, 12, 12, 12, 12, 12, 12, 12, 
12, 12, 12, 12, 12, 12, 12, 12, 
12, 12, 12, 12, 12, 12, 12, 12, 
12, 12, 12, 12, 12, 12, 12, 12, 
12, 12, 12, 12, 12, 12, 12, 12, 
12, 13, 13, 12, 12, 12, 12, 12, 
12, 12, 12, 12, 12, 12, 12, 12, 
12, 12, 12, 12, 12, 12, 12, 12, 
12, 12, 12, 12, 12, 12, 12, 12, 
12, 12, 12, 12, 12, 12, 12, 12, 
12, 12, 12, 12, 12, 12, 12, 12, 
12, 12, 12, 12, 12, 12, 12, 12};   // Массив ножек, с которыми будут производит действия при срабатывании будильника. Значения от 0 до 13

0
Брр… А вот это уже по-настоящему страшно (:
0
Только зарегистрированные и авторизованные пользователи могут оставлять комментарии.