Введение
Хочу рассказать про использование макросов в InterSystems Caché. Макрос — это символьное имя, заменяемое при компиляции исходного кода на последовательность программных инструкций. Макрос может «разворачиваться» в различные последовательности инструкций при каждом вызове, в зависимости от сработавших разветвлений внутри макроса и переданных ему аргументов. Это может быть как статический код, так и результат выполнения COS. Рассмотрим, как их можно использовать в вашем приложении.
Компиляция
Для начала, чтобы понять где используются макросы, несколько слов о том, как происходит компиляция ObjectScript кода:
- Компилятор классов использует определение класса для генерации MAC кода
- В некоторых случаях, компилятор использует классы в качестве основы для генерации дополнительных классов. Можно посмотреть на эти классы в студии, но не надо их изменять. Это происходит, например, при компиляции классов, которые определяют веб сервисы и веб клиенты
- Компилятор классов также генерирует дескриптор класса. Caché использует его во время выполнения кода
- Препроцессор (иногда называемый макро-препроцессор, MPP) использует INC файлы и заменяет макросы. Кроме того, он обрабатывает встроенный SQL в рутинах ObjectScript
- Все эти изменения происходят в памяти, сам пользовательский код не изменяется
- Далее компилятор создаёт INT код для рутин ObjectScript. Этот слой известен как промежуточный код. Весь доступ к данным на этом уровне осуществляется через глобалы
- INT код компактен и человекочитаем. Для его просмотра нажмите в студии Ctrl+Shift+V или кнопку
- INT код используется для генерации OBJ кода
- Виртуальная машина Caché использует этот код. После того, как он сгенерирован 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 Caché"
#ElseIf $SYSTEM.Version.GetNumber()="2015.2.0"
Write "You are using the latest beta version of Caché"
#Else
Write "Please consider an upgrade"
#EndIf
}
В Caché версии 2015.1.1.505 скомпилируется в следующий INT код:
zTest5() public {
Write "You are using the latest released version of Caché"
}
И в терминале выведет:
You are using the latest released version of Caché
В Caché скачанной с бета-портала скомпилируется уже в другой INT код:
zTest5() public {
Write "You are using the latest beta version of Caché"
}
И в терминале выведет:
You are using the latest beta version of Caché
А прошлые версии Caché скомпилируют следующий INT код с предложением обновиться:
zTest5() public {
Write "Please consider an upgrade"
}
И в терминале выведут:
Please consider an upgrade
Эта возможность может использоваться, например, для сохранения совместимости клиентского приложения между старыми версиями и новыми, где может быть использована новая функциональность СУБД Caché. Этой цели также служат директивы препроцессора #IfDef, #IfNDef которые проверяют существование или отсутствие макроса соответственно.
Выводы
Макросы могут как просто сделать ваш код понятней, упрощая часто повторяющиеся в коде конструкции, так и реализовывать на этапе компиляции часть логики приложения, уменьшая таким образом нагрузку в рантайме.
Что дальше?
В следующей статье расскажу о более прикладном примере использования макросов — системе логирования.
Ссылки
О компиляции
Список директив препроцессора
Список системных макросов
Класс с примерами
Часть II. Система логирования
Автор выражает благодарность хабраюзерам Daimor, Greyder и еще одному очень компетентному инженеру, пожелавшему остаться неназванным, за помощь в написании кода.