Monet tulips in holland

Введение


Хочу рассказать про использование макросов в 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:

Пример 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 и еще одному очень компетентному инженеру, пожелавшему остаться неназванным, за помощь в написании кода.

Комментарии (17)


  1. AlexeyMaslov
    29.05.2015 10:11
    +1

    Компилятор классов использует определение класса для генерации MAC кода
    На самом деле, компилятор классов генерирует INT-код.

    Стоило бы упомянуть и о том, что команда $$$Многострочного_Макроса должна быть единственной в строке.
    В остальном — неплохое введение в макросы, спасибо!


    1. eduard93 Автор
      29.05.2015 10:27
      +1

      >Стоило бы упомянуть и о том, что команда $$$Многострочного_Макроса должна быть единственной в строке.

      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!" }

      И в терминале выводит:

      Hello, World!Hello, World!
      Hello, World!
      Hello, World!
      Hello, World!
      Hello, World!
      Hello, World!
      Hello, World!
      Hello, World!
      Hello, World!
      Hello, World!
      Hello, World!


      1. AlexeyMaslov
        29.05.2015 11:34

        Так можно, потому что фигурные скобки инвариантны по отношению к концам строк.
        Если же в макроопределении (или при вызове макроса) используются традиционные блоки do с точками, то нельзя. Вот

        простой пример:
        Test7
        #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


        1. morisson
          29.05.2015 11:56

          Генерировать в Cache блоки «традиционные» блоки do с точками — не могу представить ни одного случая, зачем это может понадобиться. Синтаксис с фигурными скобками как раз и пришел на смену точек, чтобы устранить не писать такие потенциально ошибочные, затрудняющие чтение и наполненные лишними точками места.


          1. AlexeyMaslov
            29.05.2015 12:08

            Как видно, вы не можете представить себе проекта с тоннами унаследованного кода, куда, тем не менее, продолжает добавляться новый функционал )))
            Если уж на то пошло, гораздо труднее представить себе ситуацию, когда многострочные макросы привносят что-то полезное, недостижимое при использовании обычных вызовов функций (методов класса), А ограничений и дополнительных затруднений немало. Попробуйте, например, вернуть значение из многострочного макроса (как его может вернуть однострочный макрос-выражение).


            1. eduard93 Автор
              29.05.2015 12:23
              +1

              >Если уж на то пошло, гораздо труднее представить себе ситуацию, когда многострочные макросы привносят что-то полезное, >недостижимое при использовании обычных вызовов функций (методов класса),
              Они упрощают чтение INT кода.

              > А ограничений и дополнительных затруднений немало.
              Какие, кроме проблем с точками?

              >Попробуйте, например, вернуть значение из многострочного макроса (как его может вернуть однострочный макрос-выражение).
              Пример 3 оперирует со значением, возвращаемым многострочным макросом.


              1. AlexeyMaslov
                29.05.2015 12:55

                Они упрощают чтение INT кода.
                При «нормальном» программировании читать INT-код практически никогда не надо. А когда используешь макросы, то таки-да ))) По сути, это и есть наиболее важная причина, по которой я более не являюсь сторонником макросов. Время программиста дороже, чем сэкономленные милли- (или уже микро- ?) секунды на скорости выполнения при замене вызова подпрограммы вызовом макроса.
                Пример 3 оперирует со значением, возвращаемым многострочным макросом.
                Понятно, что через переменную вернуть можно (нарушая все правила приличия ))). Не увидел в вашем примере конструкций вида: set result=$$$SomeMacro(...)


                1. eduard93 Автор
                  29.05.2015 13:12
                  +1

                  Я являюсь сторонником макросов, так как после однократного выверения макроса с использованием INT кода, CLS код становится понятней, и на INT больше смотреть не надо. Кроме того вызов функций при компиляции может ощутимо уменьшить нагрузку в рантайме. Подробнее об этом я расскажу в следующей статье.

                  >Не увидел в вашем примере конструкций вида: set result=$$$SomeMacro(...)
                  Например:
                  ClassMethod Test8()
                  {
                      
                  #define SomeMacro(%name) "Hello "_ ##Continue
                      %name
                      set 
                  result=$$$SomeMacro("World")
                  }


                  1. AlexeyMaslov
                    29.05.2015 13:37

                    #define SomeMacro(%name) «Hello „_ ##Continue
                    %name
                    Это наверное шутка — оценил ))) Имелась в виду функционально оправданная многострочность, а не одно разбитое на несколько строк выражение.
                    Думаю, спорить дальше не стоит, у каждого свой опыт и свои предпочтения. Мне когда-то приходилось кодить на ассемблере для компов нескольких архитектур, на этом уровне макросы действительно большое подспорье, а в ЯВУ, в большинстве случаев, скорее анахронизм и источник ненужных проблем. Мы читаем не тот код, который выполняется — вот главная из них.


            1. morisson
              29.05.2015 12:27

              Как видно, вы не можете представить себе проекта с тоннами унаследованного кода, куда, тем не менее, продолжает добавляться новый функционал )))

              Представить не сложно ) Непонятно только, зачем в новую функциональность вносить точечный синтаксис? Читабельность от этого точно не повышается, а вероятность ошибок увеличивается.


              1. AlexeyMaslov
                29.05.2015 13:02

                Это уже вопрос философский. Представьте, что целый комплекс программ уже написан «в точках», а тут кто-то продвинутый решит развивать его (без полного переписывания), используя скобки. Какой будет результат? Улучшится читаемость?
                К тому же, корпоративный стандарт может предписывать программирование на классическом M (даже в Cache').


    1. eduard93 Автор
      29.05.2015 10:45
      +1

      Несколько многострочных макросов в одной строке тоже возможно:
      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 $$$WriteLn("Hello, World!",$$$Five)
      }
      }

      Компилируется в:

      zTest6(public {
          
      Write "Hello, World!" for %mmmu1=1:1:

              
      Write "Hello, World!",
               
      Write "Hello, World!",
          
      Write "Hello, World!" for %mmmu2=1:1:
              
      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!
      Hello, World!
      Hello, World!


    1. DAiMor
      29.05.2015 11:20
      +1

      На самом деле, компилятор классов генерирует INT-код.
      Он так же может генерировать MAC код, который в дальнейшем будет генерировать INT. Так делаете codemode = objectgenerator. И вот для кодогенераторов можно определить отдельные макросы с помощью IncludeGenerator


      1. AlexeyMaslov
        29.05.2015 11:48
        +1

        Это твои скрытые знания ))) По тексту статьи можно понять, что сначала генерится .MAC, а потом из него .INT. Что неправильно, ибо тот .MAC, который в objectgenerator-е, имеет отношение к генерации кода, а не к его выполнению. В общем случае .MAC не порождается, лишь в частном случае objectgenerator-а. Стоило бы это уточнить, статья ведь рассчитана на начинающих.


    1. morisson
      29.05.2015 12:55
      +1

      На самом деле, компилятор классов генерирует INT-код.

      Тоже думал так, но похоже что сначала mac? Источник.


      1. AlexeyMaslov
        29.05.2015 13:04
        +1

        А вы проверьте на практике…


    1. eduard93 Автор
      02.06.2015 14:27
      +1

      > На самом деле, компилятор классов генерирует INT-код.
      Поговорил с разработчиком компилятора классов COS. Класс преобразуется в MAC-подобный набор методов, на основе которого генерируется INT код.