Введение
Хочу рассказать про использование макросов в InterSystems Cache. Макрос — это символьное имя, заменяемое при компиляции исходного кода на последовательность программных инструкций. Макрос может «разворачиваться» в различные последовательности инструкций при каждом вызове, в зависимости от сработавших разветвлений внутри макроса и переданных ему аргументов. Это может быть как статический код, так и результат выполнения COS. Рассмотрим, как их можно использовать в вашем приложении.
Компиляция
Для начала, чтобы понять где используются макросы, несколько слов о том, как происходит компиляция ObjectScript кода:
- Компилятор классов использует определение класса для генерации MAC кода
- В некоторых случаях, компилятор использует классы в качестве основы для генерации дополнительных классов. Можно посмотреть на эти классы в студии, но не надо их изменять. Это происходит, например, при компиляции классов, которые определяют веб сервисы и веб клиенты
- Компилятор классов также генерирует дескриптор класса. Cache использует его во время выполнения кода
- Препроцессор (иногда называемый макро-препроцессор, MPP) использует INC файлы и заменяет макросы. Кроме того, он обрабатывает встроенный SQL в рутинах ObjectScript
- Все эти изменения происходят в памяти, сам пользовательский код не изменяется
- Далее компилятор создаёт INT код для рутин ObjectScript. Этот слой известен как промежуточный код. Весь доступ к данным на этом уровне осуществляется через глобалы
- INT код компактен и человекочитаем. Для его просмотра нажмите в студии Ctrl+Shift+V или кнопку
- INT код используется для генерации OBJ кода
- Виртуальная машина Cache использует этот код. После того, как он сгенерирован CLS/MAC/INT код больше не требуется и может быть удален (например, если мы хотим поставлять решения без исходного кода)
- Если класс — хранимый, то компилятор SQL создаст соответствующие SQL таблицы
Макросы
Как уже было сказано, макрос — это символьное имя, заменяемое при обработке препроцессором на последовательность программных инструкций. Определяется он с помощью команды #Define за которой следует сначала название макроса (возможно — со списком аргументов) а затем значение макроса:
#Define Macro[(Args)] [Value]
Где могут определятся макросы? Либо непосредственно в коде, либо в отдельных INC файлах, содержащих только макросы. Подключают необходимые файлы к классам командой Include MacroFileName в самом начале определения класса — это основной и предпочтительный метод подключения макросов к классу, присоединив таким образом макросы их можно использовать в любой части определения класса. К MAC рутинам или коду отдельных методов класса можно присоединить INC файл макросов командой #Include MacroFileName.
Примеры
Пример 1
Перейдём к примерам использования, начнём с вывода строки «Hello World». COS код: Write "Hello, World!"
Теперь напишем макрос HW, выводящий эту строку: #define HW Write "Hello, World!"
Достаточно написать в коде $$$HW ($$$ для вызова макроса, затем следует имя макроса):
ClassMethod Test()
{
#define HW Write "Hello, World!"
$$$HW
}
И при компиляции он преобразуется в следующий INT код:
zTest1() public {
Write "Hello, World!" }
В терминале при запуске этого метода будет выведено:
Hello, World!
Пример 2
В следующем примере используем переменные:
ClassMethod Test2()
{
#define WriteLn(%str,%cnt) For ##Unique(new)=1:1:%cnt { ##Continue
Write %str,! ##Continue
}
$$$WriteLn("Hello, World!",5)
}
Тут строка %str выводится %cnt раз. Названия переменных должны начинаться с %. Команда ##Unique(new) создает новую уникальную переменную в генерируемом коде, а команда ##Continue позволяет продолжить определение макроса на следующей строке. Данный код преобразуется в следующий INT код:
zTest2() public {
For %mmmu1=1:1:5 {
Write "Hello, World!",!
} }
В терминале при запуске этого метода будет выведено:
Hello, World!
Hello, World!
Hello, World!
Hello, World!
Hello, World!
Пример 3
Перейдём к более сложным примерам. Оператор ForEach бывает очень полезен при итерации по глобалам, добавим его:
ClassMethod Test3()
{
#define ForEach(%key,%gn) Set ##Unique(new)=$name(%gn) ##Continue
Set %key="" ##Continue
For { ##Continue
Set %key=$o(@##Unique(old)@(%key)) ##Continue
Quit:%key=""
#define EndFor }
Set ^test(1)=111
Set ^test(2)=222
Set ^test(3)=333
$$$ForEach(key,^test)
Write "key: ",key,!
Write "value: ",^test(key),!
$$$EndFor
}
Вот как это выглядит в INT коде:
zTest3() public {
Set ^test(1)=111
Set ^test(2)=222
Set ^test(3)=333
Set %mmmu1=$name(^test)
Set key=""
For {
Set key=$o(@%mmmu1@(key))
Quit:key=""
Write "key: ",key,!
Write "value: ",^test(key),!
} }
Что происходит в этих макросах?
- На вход принимается переменная %key в которую будет записываться текущий ключ (subscript) итерируемого глобала %gn
- В новую переменную записываем имя глобала (функция $name)
- Ключ принимает первоначальное, пустое значение
- Начинаем цикл итерации
- С помощью индирекции и функции $order присваиваем ключу следующее значение
- С помощью постусловия проверяем не принял ли ключ значение "", если да, то итерация завершена, выходим из цикла
- Выполняется произвольный пользовательский код, в данном случае вывод ключа и значения
- Цикл закрывается
В терминале при запуске этого метода будет выведено:
key: 1
value: 111
key: 2
value: 222
key: 3
value: 333
Если вы используете списки и массивы — наследников класса %Collection.AbstractIterator то можно написать аналогичный итератор уже для него.
Пример 4
Ещё одной возможностью макросов является выполнение произвольного COS кода на этапе компиляции и подстановка результата выполнения вместо макроса. Создадим макрос со временем компиляции:
ClassMethod Test4()
{
#Define CompTS ##Expression("""Compiled: " _ $ZDATETIME($HOROLOG) _ """,!")
Write $$$CompTS
}
Который преобразуется в следующий INT код:
zTest4() public {
Write "Compiled: 05/19/2015 15:28:45",! }
В терминале при запуске этого метода будет выведено:
Compiled: 05/19/2015 15:28:45
Выражение ##Expression выполняет код и подставляет результат, на входе могут быть следующие элементы языка COS:
- Строки: "abc"
- Рутины: $$Label^Routine
- Методы классов: ##class(App.Test).GetString()
- Функции COS: $name(var)
- Любая комбинация вышеперечисленных элементов
Пример 5
Директивы препроцессора #If, #ElseIf, #Else, #EndIf используются для выбора исходного кода при компиляции в зависимости от значения выражения после директивы, например этот метод:
ClassMethod Test5()
{
#If $SYSTEM.Version.GetNumber()="2015.1.1" && $SYSTEM.Version.GetBuildNumber()="505"
Write "You are using the latest released version of Cache"
#ElseIf $SYSTEM.Version.GetNumber()="2015.2.0"
Write "You are using the latest beta version of Cache"
#Else
Write "Please consider an upgrade"
#EndIf
}
В Cache версии 2015.1.1.505 скомпилируется в следующий INT код:
zTest5() public {
Write "You are using the latest released version of Cache"
}
И в терминале выведет:
You are using the latest released version of Cache
В Cache скачанной с бета-портала скомпилируется уже в другой INT код:
zTest5() public {
Write "You are using the latest beta version of Cache"
}
И в терминале выведет:
You are using the latest beta version of Cache
А прошлые версии Cache скомпилируют следующий INT код с предложением обновиться:
zTest5() public {
Write "Please consider an upgrade"
}
И в терминале выведут:
Please consider an upgrade
Эта возможность может использоваться, например, для сохранения совместимости клиентского приложения между старыми версиями и новыми, где может быть использована новая функциональность СУБД Cache. Этой цели также служат директивы препроцессора #IfDef, #IfNDef которые проверяют существование или отсутствие макроса соответственно.
Выводы
Макросы могут как просто сделать ваш код понятней, упрощая часто повторяющиеся в коде конструкции, так и реализовывать на этапе компиляции часть логики приложения, уменьшая таким образом нагрузку в рантайме.
Что дальше?
В следующей статье расскажу о более прикладном примере использования макросов — системе логирования.
Ссылки
О компиляции
Список директив препроцессора
Список системных макросов
Класс с примерами
Часть II. Система логирования
Автор выражает благодарность хабраюзерам Daimor, Greyder и еще одному очень компетентному инженеру, пожелавшему остаться неназванным, за помощь в написании кода.
AlexeyMaslov
Стоило бы упомянуть и о том, что команда $$$Многострочного_Макроса должна быть единственной в строке.
В остальном — неплохое введение в макросы, спасибо!
eduard93 Автор
>Стоило бы упомянуть и о том, что команда $$$Многострочного_Макроса должна быть единственной в строке.
ClassMethod Test6()
{
#define HW Write "Hello, World!"
#define Five 5
#define WriteLn(%str,%cnt) for ##unique(new)=1:1:%cnt { ##Continue
$$$HW,! ##Continue
Write %str,! ##Continue
}
$$$HW $$$WriteLn("Hello, World!",$$$Five) $$$HW
}
Компилируется в:
zTest6() public {
Write "Hello, World!" for %mmmu1=1:1:5 {
Write "Hello, World!",!
Write "Hello, World!",!
} Write "Hello, World!" }
И в терминале выводит:
AlexeyMaslov
Так можно, потому что фигурные скобки инвариантны по отношению к концам строк.
Если же в макроопределении (или при вызове макроса) используются традиционные блоки do с точками, то нельзя. Вот
#define WR(%arg) Write «Result is „_%arg,!
#define FindStr(%ref,%ind,%str,%result) s %ind=“» for { ##Continue
set %ind=$o(@%ref@(%ind)) ##Continue
if %ind="" s %result=0 quit ##Continue
if $f(@%ref@(%ind),%str) s %result=1 quit ##Continue
} ##Continue
#define FindStr1(%ref,%ind,%str,%result) s %ind="",%result=-1 for do quit:%result'<0 ##Continue
. set %ind=$o(@%ref@(%ind)) ##Continue
. if %ind="" s %result=0 quit ##Continue
. if $f(@%ref@(%ind),%str) s %result=1 quit ##Continue
if 1 { $$$FindStr1(«a»,i,«jo»,result) $$$WR(result) } else { w «nop»,! } // так нормально
#if 0 // если поменять 0 на 1, будет ошибка компиляции
if 1 do // some condition
. set a(1)=«11»,a(2)=«joker»
. $$$FindStr(«a»,i,«jo»,result)
. $$$WR(result)
#else // ошибки компиляции нет, однако имеем бесконечный цикл
if 1 do // some condition
. set a(1)=«11»,a(2)=«joker»
. $$$FindStr1(«a»,i,«jo»,result)
. $$$WR(result)
#endif
morisson
Генерировать в Cache блоки «традиционные» блоки do с точками — не могу представить ни одного случая, зачем это может понадобиться. Синтаксис с фигурными скобками как раз и пришел на смену точек, чтобы устранить не писать такие потенциально ошибочные, затрудняющие чтение и наполненные лишними точками места.
AlexeyMaslov
Как видно, вы не можете представить себе проекта с тоннами унаследованного кода, куда, тем не менее, продолжает добавляться новый функционал )))
Если уж на то пошло, гораздо труднее представить себе ситуацию, когда многострочные макросы привносят что-то полезное, недостижимое при использовании обычных вызовов функций (методов класса), А ограничений и дополнительных затруднений немало. Попробуйте, например, вернуть значение из многострочного макроса (как его может вернуть однострочный макрос-выражение).
eduard93 Автор
>Если уж на то пошло, гораздо труднее представить себе ситуацию, когда многострочные макросы привносят что-то полезное, >недостижимое при использовании обычных вызовов функций (методов класса),
Они упрощают чтение INT кода.
> А ограничений и дополнительных затруднений немало.
Какие, кроме проблем с точками?
>Попробуйте, например, вернуть значение из многострочного макроса (как его может вернуть однострочный макрос-выражение).
Пример 3 оперирует со значением, возвращаемым многострочным макросом.
AlexeyMaslov
Понятно, что через переменную вернуть можно (нарушая все правила приличия ))). Не увидел в вашем примере конструкций вида: set result=$$$SomeMacro(...)
eduard93 Автор
Я являюсь сторонником макросов, так как после однократного выверения макроса с использованием INT кода, CLS код становится понятней, и на INT больше смотреть не надо. Кроме того вызов функций при компиляции может ощутимо уменьшить нагрузку в рантайме. Подробнее об этом я расскажу в следующей статье.
>Не увидел в вашем примере конструкций вида: set result=$$$SomeMacro(...)
Например:
ClassMethod Test8()
{
#define SomeMacro(%name) "Hello "_ ##Continue
%name
set result=$$$SomeMacro("World")
}
AlexeyMaslov
Думаю, спорить дальше не стоит, у каждого свой опыт и свои предпочтения. Мне когда-то приходилось кодить на ассемблере для компов нескольких архитектур, на этом уровне макросы действительно большое подспорье, а в ЯВУ, в большинстве случаев, скорее анахронизм и источник ненужных проблем. Мы читаем не тот код, который выполняется — вот главная из них.
morisson
Представить не сложно ) Непонятно только, зачем в новую функциональность вносить точечный синтаксис? Читабельность от этого точно не повышается, а вероятность ошибок увеличивается.
AlexeyMaslov
Это уже вопрос философский. Представьте, что целый комплекс программ уже написан «в точках», а тут кто-то продвинутый решит развивать его (без полного переписывания), используя скобки. Какой будет результат? Улучшится читаемость?
К тому же, корпоративный стандарт может предписывать программирование на классическом M (даже в Cache').
eduard93 Автор
{
#define HW Write "Hello, World!"
#define Five 5
#define WriteLn(%str,%cnt) for ##unique(new)=1:1:%cnt { ##Continue
$$$HW,! ##Continue
Write %str,! ##Continue
}
$$$HW $$$WriteLn("Hello, World!",$$$Five) $$$HW $$$WriteLn("Hello, World!",$$$Five)
}
}
Компилируется в:
zTest6() public {
Write "Hello, World!" for %mmmu1=1:1:5 {
Write "Hello, World!",!
Write "Hello, World!",!
} Write "Hello, World!" for %mmmu2=1:1:5 {
Write "Hello, World!",!
Write "Hello, World!",!
} }
Hello, World!
Hello, World!
Hello, World!
Hello, World!
Hello, World!
Hello, World!
Hello, World!
Hello, World!
Hello, World!
Hello, World!Hello, World!
Hello, World!
Hello, World!
Hello, World!
Hello, World!
Hello, World!
Hello, World!
Hello, World!
Hello, World!
Hello, World!
DAiMor
AlexeyMaslov
Это твои скрытые знания ))) По тексту статьи можно понять, что сначала генерится .MAC, а потом из него .INT. Что неправильно, ибо тот .MAC, который в objectgenerator-е, имеет отношение к генерации кода, а не к его выполнению. В общем случае .MAC не порождается, лишь в частном случае objectgenerator-а. Стоило бы это уточнить, статья ведь рассчитана на начинающих.
morisson
Тоже думал так, но похоже что сначала mac? Источник.
AlexeyMaslov
А вы проверьте на практике…
eduard93 Автор
> На самом деле, компилятор классов генерирует INT-код.
Поговорил с разработчиком компилятора классов COS. Класс преобразуется в MAC-подобный набор методов, на основе которого генерируется INT код.