Стрелочные часы на CMake



Когда я устроился на новую работу, пришлось в ускоренном темпе осваивать новые для меня технологии, которые используются в данной компании. Одной из таких технологий стала система сборки cmake, с которой мне раньше не приходилось сталкиваться.

Эта система имеет свой встроенный язык для написания сборочных скриптов. Этот самый язык меня и заинтересовал. Вскоре я выяснил, что в нем есть возможность вычисления математических выражений, запись и чтение из файлов, запуск внешних процессов и другие интересные возможности, что навело меня на мысль воспользоваться этим языком в качестве основного ЯП и написать на нем что-нибудь осязаемое. Речь пойдет о том, как я писал стрелочные часы на языке cmake 2.8.

Честно говоря, сначала мне в голову пришла идея проверить cmake на возможность ввода-вывода из стандартных потоков. Хотелось научиться считывать нажатые клавиши, ну или, на худой конец, события мыши, что позволило бы сделать какую-нибудь интерактивную программу, написать, к примеру, тетрис. С выводом оказалось все довольно просто:
file(WRITE /dev/stdout "blabla")
А вот считывать стандартный поток cmake напрочь отказывался, читать напрямую из ивентов (/dev/input/event4 или /dev/input/mice) также не удавалось. Так что идея сделать тетрис была отброшена и я решил поиграться с выводом, всё-таки способность выводить напрямую в stdout меня привлекала больше, чем стандартная команда message().

Я решил, раз уж могу писать напрямую в stdout, то надо попробовать писать туда escape-последовательности. Это бы дало богатые возможности: цветной вывод, перемещения курсора, очистка экрана и другие. К счастью, в cmake оказалась возможность вывода непечатаемых символов — это операция ASCII функции string, так я написал функцию очистки экрана:
string(ASCII 27 ESCAPE)
function (clrscr)
    file(WRITE /dev/stdout "${ESCAPE}[2J")
endfunction(clrscr)

Раз уж escape-коды заработали, то я решил следующим шагом научиться выводить текст в произвольных координатах:
function(textXY X Y MSG)
    file(WRITE /dev/stdout "${ESCAPE}[${Y};${X}H${MSG}")
endfunction(textXY)

Ну и следующим логичным продолжением этого родилась мысль написать функцию рисования линии. Здесь уже пришлось столкнуться с первыми трудностями:
  1. cmake вычисляет выражение записанное в строку и результат его целочисленный;
  2. функции в cmake не возвращают значение;
  3. хочется иметь универсальный алгоритм рисования линии, не зависящий от расположения концов относительно друг друга;

Решать эти трудности я начал с конца. Во-первых, был придуман алгоритм рисования линии:
  1. Найти разницу координат концов Dx=x2-x1 и Dy=y2-y1 с учетом минуса (он будет нужен для направления);
  2. Найти максимальную по модулю дельту Dmax = max(abs(Dx), abs(Dy));
  3. Пробежать циклом i = 0..Dmax, на каждом шаге вычисляя текущие координаты по формулам:
    	x = x1 + i * Dx / Dmax
    	y = y1 + i * Dy / Dmax

Во-вторых, нужны были функции поиска максимума и абсолютного значения. Так как функции в cmake значения не возвращают, то пришлось воспользоваться макросами. В макросы можно подставлять как переменные, так и значения. Мне показалось, что переменные везде подставлять красивее, но макрос получается слишком «волосатым», так что в дальнейшем я стал использовать подстановку переменной только для результата.

Код макросов
macro(max a b m)
	if(${a} LESS ${${b}})
		set(${m} ${${b}})
	else(${a} LESS ${${b}})
		set(${m} ${${a}})
	endif(${a} LESS ${${b}})
endmacro(max)

macro(abs a res)
	if(${a} LESS 0)
		string(LENGTH ${a} len)
		math(EXPR l1 "${len} - 1")
		string(SUBSTRING ${a} 1 ${l1} ${res})
	else(${a} LESS 0)
		set(${res} ${a})
	endif(${a} LESS 0)
endmacro(abs)


Для поиска абсолютного значения используется тот факт, что cmake оперирует строками и просто «откусывается» минус, если он есть.

Когда макросы были готовы, то при попытке вычислять выражения для координат, используя команду
math(EXPR <result> <expression>)
мною были осознаны интересные нюансы, связанные с тем, что cmake оперирует строками, поэтому, например, выражение "${a} + ${b}", в случае, когда b отрицательное — вычисляться не будет (т.к. может получится что-то вроде 5 + -6, а такое выражение не валидно). Этот нюанс удалось обойти хитрым правилом — везде, где в формуле может встретиться отрицательное значение переменной, добавлять к ней ведущий 0 и брать все это в скобки: "${a} + (0${b})". Итоговая функция рисования линии получилась такой:

Код функции line(x1 y1 x2 y2 chr)
function(line x1 y1 x2 y2 chr)
	math(EXPR Dx "${x2} - ${x1}")
	abs(${Dx} aDx)
	math(EXPR Dy "${y2} - ${y1}")
	abs(${Dy} aDy)
	max(aDx aDy Dmax)
	set(i 0)
	while(i LESS ${Dmax})
		math(EXPR cx "${x1} + ${i} * (0${Dx}) / ${Dmax}")
		math(EXPR cy "${y1} + ${i} * (0${Dy}) / ${Dmax}")
		textXY(${cx} ${cy} ${chr})
		math(EXPR i "${i} + 1")
	endwhile(i LESS ${Dmax})
endfunction(line)


После тестирования функции рисования линии и появилась идея куда-то ее применить (например, «запилить» стрелочные часы). До этого я вообще не знал, что интересного можно со всем этим сделать. Оказалось, практически все готово, осталось нарисовать циферблат, получить время из системы, вычислить необходимые углы, нарисовать 3 линии под нужными углами (часовая, минутная и секундная стрелки) и часы будут готовы. Не хватало еще 2-х функций: синуса и косинуса, для рисования окружности и рисования линии под заданным углом.

Дело осложнилось тем, что синус и косинус имеют значения в интервале [0;1], а cmake оперирует только целочисленными значениями, так что решено было использовать коэффициент 1000: находить синус и косинус умноженный на 1000, а в выражении, где они применяются делить все на этот коэффициент.

Для реализации тригонометрических функций применяется их разложение в ряд Маклорена. И снова трудности:
  1. Не хочется использовать слишком высокие степени и факториалы в ряде Маклорена;
  2. При использовании 2-3 первых членов ряда хорошие приближения получаются только в интервале [-pi/2; pi/2].

Мне же хотелось иметь ОДЗ хотя бы в интервале [-pi; 2*pi], для этого было решено угол в радианах переводить в правую полуплоскость, делая поправку на знак функции. Технически тут геометрический смысл и формулы приведения, поэтому сильно не «разжевываю». Итоговый код тригонометрических функций получился довольно «страшненьким»:

Код синуса и косинуса
set(PI1000 3142)
set(PI500 1571)
set(_PI500 -1571)
set(_2PI1000 6283)

macro(m_rad1000_4sin x res)
	math(EXPR rad1000 "(0${x}) * ${PI1000} / 180")
	if(rad1000 GREATER ${PI1000})
		math(EXPR rad1000_ "${PI1000} - ${rad1000}")
	else(rad1000 GREATER ${PI1000})
		set(rad1000_ ${rad1000})
	endif(rad1000 GREATER ${PI1000})
	
	if(rad1000_ GREATER ${PI500})
		math(EXPR rad1000__ "${PI1000} - ${rad1000_}")
	else(rad1000_ GREATER ${PI500})
		if(rad1000_ LESS ${_PI500})
			abs(${rad1000_} abs_rad1000_)
			math(EXPR rad1000__ "${abs_rad1000_} - ${PI1000}")
		else(rad1000_ LESS ${_PI500})
			set(rad1000__ ${rad1000_})
		endif(rad1000_ LESS ${_PI500})
	endif(rad1000_ GREATER ${PI500})
	
	set(${res} ${rad1000__})
endmacro(m_rad1000_4sin)

macro(m_rad1000_4cos x res)
	math(EXPR rad1000 "(0${x}) * ${PI1000} / 180")
	if(rad1000 GREATER ${PI1000})
		math(EXPR rad1000_ "${rad1000} - ${_2PI1000}")
	else(rad1000 GREATER ${PI1000})
		set(rad1000_ ${rad1000})
	endif(rad1000 GREATER ${PI1000})
	
	set(${res} ${rad1000_})
endmacro(m_rad1000_4cos)

macro(sin1000 x res)
	m_rad1000_4sin(${x} r1000)
	math(EXPR ${res} "0${r1000} - (0${r1000}) * (0${r1000}) / 1000 * (0${r1000}) / 1000 / 6 + (0${r1000}) * (0${r1000}) / 1000 * (0${r1000}) / 1000 * (0${r1000}) / 1000 * (0${r1000}) / 1000 / 120")
endmacro(sin1000)

macro(cos1000 x res)
	m_rad1000_4cos(${x} r1000)
	unset(sign)
	if(r1000 GREATER ${PI500})
		math(EXPR r1000_ "${PI1000} - ${r1000}")
		set(r1000 ${r1000_})
		set(sign "0-")
	endif(r1000 GREATER ${PI500})
	
	if(r1000 LESS ${_PI500})
		math(EXPR r1000_ "${PI1000} + (0${r1000})")
		set(r1000 ${r1000_})
		set(sign "0-")
	endif(r1000 LESS ${_PI500})
	
	math(EXPR ${res} "${sign}(1000 - (0${r1000}) * (0${r1000}) / 1000 / 2 + (0${r1000}) * (0${r1000}) / 1000 * (0${r1000}) / 1000 * (0${r1000}) / 1000 / 24 - (0${r1000}) * (0${r1000}) / 1000 * (0${r1000}) / 1000 * (0${r1000}) / 1000 * (0${r1000}) / 1000 * (0${r1000}) / 1000 / 720)")
endmacro(cos1000)


После этого остальное уже было делом техники — нарисовать 12 чисел по кругу, крутиться в цикле и спрашивать у системы время; когда оно изменилось, стирать старые стрелки и рисовать новые под нужными углами. Время получаем через запуск внешнего процесса:
 execute_process(COMMAND "date" "+%H%M%S" OUTPUT_VARIABLE time)
выделить подстроки из time и вычислить углы — в рамках школьной математики.

Полный код можно посмотреть на гитхабе.
Тестировалось на cmake version 2.8.12.2, Ubuntu 12.04, 14.04.
Ads
AdBlock has stolen the banner, but banners are not teeth — they will be back

More

Comments 22

    –13
    похвально, но есть же clockywock.

    кстати буду благодарен если кто подскажет где его взять под ubuntu
      +4
      А при чем тут это? Автор просто показал возможности cmake, как я понял. Просто позабавился немного.
      +1
      Круто:) Но всё равно они(kitware) гады, что запилили свой язык, вместо чего нибудь популярного и легко разбираемого сторонними утилитами — того же js.
        +2
        Почему вы думаете что свой язык чем-то хуже любого существующего и/или что существующий язык способен эффективно решать все возможные проблемы так что его нужно использовать и в системе сборки? Системы сборки с существующими языками (m4, python) мы уже видели, они отвратительны.
          0
          еще Fake
            +2
            Было бы хуже, если вместо

            function (clrscr)
            file(WRITE /dev/stdout "${ESCAPE}[2J")
            endfunction(clrscr)

            Было бы

            function clrscr() { stdout.write(escape + «2J») }?

            В чем преимущество своего непохожего языка сдесь?
              0
              Этот пример я комментировать не буду, потому что это ненормальное программирование. А вот примеров из реальной жизни можно привести много:

              — Нормальное раскрытие переменных а-ля make (message(«Building ${FOO} with ${BAR}» вместо плясок с кавычками и плюсами)
              — В том числе рекурсивное (${FOO_${BAR}}) которое иначе потребовало бы запаковку значений в словари и обратно
              — Отсутствие необходимости писать кавычки почти везде
              — Нормальный foreach по содержимому массива
              — Форсирование повторения условия в endif/endfor/end* что сильно увеличивает читабельность
              — Именованные аргументы на мой взгляд сделаны очень удобно
              — Правильная передача значений в дочерние CMakeLists.txt и обратно (PARENT_SCOPE)
              — Кэшированные переменные и все их атрибуты
              — Макросы
                0
                То есть нужен язык со string interpolation, foreach, именованными аргументами, подобием модульности и без кавычек?

                Вот эти фичи мне кажутся сомнительныими:

                — В том числе рекурсивное (${FOO_${BAR}}) которое иначе потребовало бы запаковку значений в словари и обратно

                Если использовать чем это лучше чем ${foo.bar}

                — Форсирование повторения условия в endif/endfor/end* что сильно увеличивает читабельность

                Нет ли способа так увеличивать читабельность, чтобы это не понадобилось?

                — Макросы

                Мне кажется, в динамических языках это не так сильно нужно.

                А вот это я не понял:
                — Кэшированные переменные и все их атрибуты

                В любом случае нельзя ли взять наиболее подходящий ЯП общего назначения и внести необходимые дополнения туда?
                  0
                  >А вот это я не понял:
                  — Кэшированные переменные и все их атрибуты

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

                  В любом случае, такое можно сделать на чём угодно, так что относить сиё к поинтам языка CMake — слегка странно, это просто фича.
                    0
                    > То есть нужен язык со string interpolation, foreach, именованными аргументами, подобием модульности и без кавычек?

                    Нет, нужен DSL на котором удобно писать скрипты сборки. Собственно, такой и получился.
                      0
                      Ваша реплика выглядит странно. Мы как раз и выясняем в чем отличие «DSL на котором удобно писать скрипты сборки» от языка общего назначения (вернее, если обойтись eDSL нельзя ли скомпенсировать недостаток заточенности под задачу выгодами от лучшей поддержки).

                      Я взял подмножество ваших требований а вы его отрицаете словом нет, не добавляя ясности.

                      К тому же я задал вопрос «В любом случае нельзя ли взять наиболее подходящий ЯП общего назначения и внести необходимые дополнения туда?»

                      Грубо говоря, если нужно сделать свой велосипед, почему бы не использовать хотя бы стандартные гайки.

                        0
                        > Ваша реплика выглядит странно

                        Нет, странно выглядит то что вы несколько примеров нужных фич языка оформили как полное ТЗ. Я вас поправил.

                        > Грубо говоря, если нужно сделать свой велосипед, почему бы не использовать хотя бы стандартные гайки

                        Если уж опускаться до аналогий, то зачем если нужен велосипед, выпиливать его из самолёта? А стандартных гаек там достаточно — в плане синтаксиса ничего нового не изобретено.
                          0
                          Ну вот я и интересуюсь, какие фичи языка вы считаете нужными и почему? Еще пришла идея, что можно использовать в качестве хост языка какой-нибудь готовый шелл. Вроде там примерно те же требования

                          image

                          зачем если нужен велосипед, выпиливать его из самолёта

                          Поюзать самолетную инфраструктуру.

                          в плане синтаксиса ничего нового не изобретено

                          А вот это function (name) откуда взято?
                        0
                        Сделать ограниченый DSL на js, или том же python, не является особой проблеммой, оба интерпретатора имеют кучу открытых и легко кастомизируемых реализаций. При этом не надо будет учить новый язык, и писать новый парсер.

                        Вот тот же OpenSCAD, у них опять же свой язык, но зачем — непонятно, ограничения без какой либо необходимости. Проект опенсурсный и бесплатный, может автор просто хотел свой интерпретатор написать, чисто как упражнение? Единственное предположение — нижележащий язык это ассемблер, и это представление наиболее эффективно для данной предметной области. Но опять же, вряд ли оно сильно выигрывает у asm.js.

                        Заточить js интерпретатор под свои нужды — крайне не сложно. Что характерно, находятся товарищи которые делают например так :)
                  0
                  Системы сборки которые отвратительны, таковы не из-за языка. В CMake мощная концепция метасборщика, удачно сочетается с модульной системой поиска всего и вся. Однако, имеется определённый порог вхождения, отсутсвие или слабая выразительность многих привычных языковых средств.

                  Сильно сомневаюсь что можно найти, хоть какой то код на CMake, который будет выглядеть проще и лаконичнее чем аналог на любой распостранёной скриптоте общего назначения. Отсюда — не ясен профит создания и поддержки нового языка (если только это не vendor lock).

                  Я уже молчу про то, что если бы это был например js, то встроить поддержку CMake в ваше любимое IDE заняло бы пару дней у плохо обученного школьника (и без костылей типа парсинга cbp файла).
                    0
                    > Однако, имеется определённый порог вхождения, отсутсвие или слабая выразительность многих привычных языковых средств.

                    Это «отсутсвие или слабая выразительность» для системы сборки — как мана небесная, поскольку на cmake всё-таки собирают проекты, а не часы рисуют. Для этого выразительности не только хватает — её столько, что многие вещи сильно упрощаются, а писать хитровыгнутые конструкции желание пропадает. Я достаточно насмотрелся на SCons'овские скрипты от питонистов, которые во всю используют «привычные языковые средства» — разбираться в этом невозможно. Как результат, порог вхождения как раз намного ниже, чем если бы использовался язык общего назначения.

                    > Сильно сомневаюсь что можно найти, хоть какой то код на CMake, который будет выглядеть проще и лаконичнее чем аналог на любой распостранёной скриптоте общего назначения.

                    Как-бы DSL всегда быть проще и лаконичнее языков общего назначения. Несколько примеров я привёл выше.

                    > то встроить поддержку CMake в ваше любимое IDE

                    Да, давайте выбирать язык для системы сборки на основании лёгкости встраивания его в моё любимое IDE. Да и про лёгкость я бы поспорил — если и понадобится глубокий разбор cmake, то пожалуй проще будет разобрать его синтаксис, чем js, где нужное определение цели возможно придётся вытаскивать через несколько замыканий.
                      0
                      Однако, никто, пока что, так и не встроил CMake в ide, без костылестроения :) Если только JetBrains (надо бы потыкать).

                      Вот тот же qbs, можно без проблем встроить куда душа попросит, и я не сказал бы что там какой то адский синтаксис, жалко вот он сам собирает (как и упомянутый вами SCons), то бишь относится слегка к другому классу инструментов.

                      > Это «отсутсвие или слабая выразительность» для системы сборки — как мана небесная, поскольку на cmake всё-таки собирают проекты, а не часы рисуют.

                      По мимо сборки, на средних и больших проектах встречается ещё ряд задач, как то покрытие кода разного уровня тестами, различные виды анализа кодовой базы (часто специфичные для проекта), кодогенерация и прочие плюшки. В таком случае одна фигня приходится делать find_package(Python REQUIRED) и забавляться с кастомными целями, либо обрастать платформозависимым шелом.
                    0
                    Кто это «мы» видели?
                    И причем тут языки программирования?
                    Новый язык нафик не нужен, потому что есть C++, Java, их и так 100500. что мешает хотя бы C использовать вместо непонятного и ущербного cmake 2.8? Нахрена изучать новую матчасть, нахрена заставлять людей мучиться и себя тоже?
                    Лень арифметику указателей имплементировать?
                    Возьми стандарт ECMA Script, например готовый QtScript из того же Qt, там уже все реализовано, вставь в свой CMake и нечего какие то непонятные языки-велосипеды изобретать.
                    Если я бы был инвестором — послал бы нахрен этих изобретателей cmake 2.8, но Cmake ведь бесплатный.
                      0
                      QBS. QML + js. Недостатки определённо у нее есть, но я еще не слышал, чтобы хоть кто-то назвал её «отвратительной».
                        0
                        Я пока не видел ни одного проекта её использующего.
                    0
                    А зачем управляющие последовательности терминала-то хардкодить? Вызовите tput — ровно так же, как вы делаете для date. Что-то типа:

                    execute_process(COMMAND "tput" "clear" OUTPUT_VARIABLE terminal_clrscr)
                    function (clrscr)
                        file(WRITE /dev/stdout "${terminal_clrscr}")
                    endfunction(clrscr)
                    
                      0
                      Тут 2 причины:
                      1) В идеале хотелось максимально решить все средствами cmake без запуска внешних процессов (жаль время узнать не удалось)
                      2) Я не так давно являюсь пользователем linux и просто не знал про tput

                      А так да, спасибо за замечание :)

                    Only users with full accounts can post comments. Log in, please.