Коли я влаштувався на нову роботу, довелося в прискореному темпі освоювати нові для мене технології, які використовуються в даній компанії. Однією з таких технологій стала система складання 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)
Ну і наступним логічним продовженням цього народилася думка написати функцію малювання лінії. Тут вже довелося зіткнутися з першими труднощами:
- cmake обчислює вираз, записаний у рядок і результат його цілочисельний;
- функції cmake не повертають значення;
- хочеться мати універсальний алгоритм малювання лінії, що не залежить від розташування кінців відносно один одного;
Вирішувати ці труднощі я почав з кінця. По-перше, був придуманий алгоритм малювання лінії:
- Знайти різницю координат кінців Dx = x2-x1 і Dy = y2-y1 з урахуванням мінуса (він буде потрібен для напрямку);
- Знайти максимальну за додатком дельта Dmax = max (abs (Dx), abs (Dy));
- Пробігти циклом 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, а у виразі, де вони застосовуються ділити все на цей коефіцієнт.
Для реалізації тригонометричних функцій застосовується їх розкладання в ряд Маклорена. І знову труднощі:
- Не хочеться використовувати занадто високі ступені і факторіали в ряді Маклорена;
- При використанні 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.