Макросы в InterSystems Caché

image

Введение

Хочу рассказать про использование макросов в InterSystems Caché. Макрос — это символьное имя, заменяемое при компиляции исходного кода на последовательность программных инструкций. Макрос может «разворачиваться» в различные последовательности инструкций при каждом вызове, в зависимости от сработавших разветвлений внутри макроса и переданных ему аргументов. Это может быть как статический код, так и результат выполнения COS. Рассмотрим, как их можно использовать в вашем приложении.

Компиляция

image
Для начала, чтобы понять где используются макросы, несколько слов о том, как происходит компиляция ObjectScript кода:

  • Компилятор классов использует определение класса для генерации MAC кода
  • В некоторых случаях, компилятор использует классы в качестве основы для генерации дополнительных классов. Можно посмотреть на эти классы в студии, но не надо их изменять. Это происходит, например, при компиляции классов, которые определяют веб сервисы и веб клиенты
  • Компилятор классов также генерирует дескриптор класса. Caché использует его во время выполнения кода
  • Препроцессор (иногда называемый макро-препроцессор, MPP) использует INC файлы и заменяет макросы. Кроме того, он обрабатывает встроенный SQL в рутинах ObjectScript
  • Все эти изменения происходят в памяти, сам пользовательский код не изменяется
  • Далее компилятор создаёт INT код для рутин ObjectScript. Этот слой известен как промежуточный код. Весь доступ к данным на этом уровне осуществляется через глобалы
  • INT код компактен и человекочитаем. Для его просмотра нажмите в студии Ctrl+Shift+V или кнопку image
  • 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:

Пример 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 которые проверяют существование или отсутствие макроса соответственно.

Выводы

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

Что дальше?

В следующей статье расскажу о более прикладном примере использования макросов – системе логирования.

Ссылки

О компиляции
Список директив препроцессора
Список системных макросов
Класс с примерами

Автор выражает благодарность хабраюзерам @Daimor, @Greyder и еще одному очень компетентному инженеру, пожелавшему остаться неназванным, за помощь в написании кода.

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *