Meu amigo Alexssander me pediu para escrever sobre macros e logo em seguida eu vi um post muito abrangente que trata justamente de macros e resolvi traduzi-lo, vamos lá.

O X++ prove macros de uma maneira fácil e expansiva. Com eles, você pode definir e usar valores para fazer compilações condicionais e etc… Neste artigo eu vou descrever a semantica da construção e prover modos para resolver alguns dos problemas que iniciantes e experts tem com os macros.

Macros não são estruturados tanto que eles não seguem a estrutura do X++. O tratamento dos macros acontece antes que o texto chegue ao compilador.

Macros podem ser usados em métodos, declarações de classes, jobs e em todos os outros lugares onde se há código X++.

A semântica de cada chave macro é descrita abaixo:

Construtores

#define
A sintaxe é: #define.MyName(SomeValue)

Isto define um macro chamado MyName com o valor SomeValue. Quando esta definição está em uso, qualquer referencia à #MyName será substituída pelo que há em SomeValue. A definição semântica não tem outro objetivo além de definir o símbolo MyName: O texto não atinge o próprio compilador. Quando a compilação do método atual acabar, o símbolo (MyName neste caso) não é mais lembrado. Se o símbolo já estava definido, o valor antigo é descartado e substituído pelo novo valor.

#globaldefine
A sintaxe é: #globaldefine.MyName(SomeValue)

Este tem a mesma semântica do define descrito acima.

#definc
A sintaxe é: #definc.MyName

Este construtor é usado só quando o valor é um inteiro. O pré processador incrementará o valor do símbolo por um. Se o valor não foi definido antes da instrução #definc é gerado um erro. Caso o valor não é um inteiro, o valor antigo será sobrescrito pelo valor 0 e então incrementado, tornado-se 1.

#defdec
A sintaxe é: #defdec.MyName

Este é o mesmo caso do anterior, no entanto o pré processador irá decrementar o valor do símbolo por um. Caso não exista, gerará um erro e caso exista e não seja um inteiro, será convertido para 0 e então decrementado por 1, o que retorna -1.

#undef
A sintaxe é: #undef.MyName

Como o nome já diz, ele remove a definição do símbolo e caso o símbolo não tenha sido previamente declarado, nada acontece.

#if … #endif
A sintaxe é:
#if.MySymbol

#endif

Ou

#if.MySymbol(SomeValue)

#endif

No primeiro caso o texto marcado com … nos exemplos acima é inserido dentro do fonte se o MySymbol foi préviamente definido, já no segundo caso, o conteúdo é inserido no fonte apenas se o valor definido for igual ao que está sendo validado.

O construtor #if pode ser usado em qualquer nível, mas não há um construtor #else.

#ifnot … #endif
A sintaxe é: #ifnot.MySymbol

#endif

Ou

#ifnot.MySymbol(SomeValue)

#endif

É exatamente igual ao anterior, no entanto, negativo. Equivale ao != usado em ifs.

#macrolib
A sintaxe é: #macrolib.MyName

Neste caso, o nome deve referenciar um macro existente na AOT. O texto do Macro é então processado pelo pré processador. O valor da macro é inserido no código fonte onde a diretiva aparece. Caso o Macro não exista na AOT um erro será gerado.

#macro / #localmacro

As chaves #macro e #localmacro são intercambiáveis, não há diferença na semântica das duas. O construtor é usado para definir um símbolo para detonar um conteúdo que possivelmente usará várias linhas.

A sintaxe é:
#localmacro.MySymbol
….
#endmacro

Exemplo:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class MyBaseClass extends Runbase
{
     int v1;
     #define.myMacro(“Hello world”)
     #localmacro.currentlist
        v1
     #endmacro
 
     public container pack()
     {
         return [#currentlist];  // #currentlist expande-se para v1
     }
 
     public void run()
     {
         print #myMacro;   // #myMacro expande-se para “Hello world”
     }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class MyDerivedClass extends myBaseClass
{
    int v2;
    #localmacro.currentlist
       v2
    #endmacro
 
 
    public container pack()
    {
        return [super(), #currentlist];  // #currentlist expande-se para v2
    }
 
    public void run()
    {
        print #myMacro;  // #myMacro expande-se para “Hello world”
    }
}

#MySymbol
A sintaxe é: #MySymbol

Insere o valor do símbolo no código e é um erro se referir a um símbolo que não foi definido. Se o nome denotar um Macro definido na AOT, o texto do nó será processado pelo pré processador (neste caso, #MySymbol é um atalho para #macrolib.MySymbol).

Problemas comuns

Nesta parte do post, eu gostaria de descrever alguns dos problemas que os desenvolvedores tem quando utilizam macros.

Macros e parâmetros

Isto parece ser um fato pouco conhecido e a origem de certa confusão: Os valores dos símbolos podem receber os placeholders % como os usados no strfmt e caso nenhum parâmetro seja passado uma string nula é usada. Ou seja #define.MyString(“Hello World from %1”) definirá um macro chamado MyString com um parâmetro. Se o macro for expandido conforme abaixo:

#MyString(X++)

o resultado será:

“Hello World from X++”

Veja que no exemplo acima não usamos aspas para passar o valor X++ como argumento. Caso usado, irá gerar um erro de compilação!

A confusão também pode acontecer por causa da notação % também é usada como parâmetro de substituição na função strFmt. E como sabem, esta função tem uma lista de parâmetros com comprimento variável, e cada referencia de %n no primeiro parâmetro (a string) será expandida para conter a representação informada no argumento n, conforme podemos ver abaixo:

print strfmt(“The value is %1”, theValue);

Agora, alguns programadores tentarão fazer isto:

#define.TheText(“The value is %1”)
print strfmt(#TheText, theValue);

Mas a substituição em macros acontece antes do compilador passar pelo código. A substituição do macro não irá encontrar o parâmetro para ser colocado onde o %1 encontra-se, então o compilador encontrará:

print strfmt(“The value is “, theValue);

o que provavelmente não é a intenção do desenvolvedor.

Macros nas declarações de classes

Algumas confusões surgem em situações que os macros são definidos na declaração das classes. Para entender como isto é tratado é necessário rever o que o compilador faz quando ele compila um método. O processo inicia por calcular a sequencia das derivações da classe que a classe faz parte. Então o processo analisa cada declaração da classe com a primeira derivação primeiro, preenchendo sua tabela interna de símbolos. Após compilar a declaração da classe o compilador compila os métodos. Qualquer símbolo definido em qualquer classe derivada será subseqüentemente disponível para uso nos métodos. Símbolos definidos na declaração da classes talvez sejam substituídos por valores definidos em mais de uma derivação da classe.

Parênteses dentro das strings nos macros

O scanner que trata com as strings nos macros é muito simplista. Ele não ira tratar a situação onde onde parênteses forem incluídosa string, ou seja

#define.Another(“(This is text in parenthesis)”)

irá gerar um erro léxico. Se você precisar fazer isto, você deve usar no lugar a diretiva #localmacro:

#localmacro.Another
“(This is text in parenthesis)”
#endmacro

Neste caso, o fim do macro é sinalizado pela string #endmacro e não no parêntese direito.

Fonte.

Aqui acaba a tradução.

Se navegarem pela AOT vocês vão ver exemplos de macros mais compostos e complexos, que é o exemplo do macro InventDimSelect que implementa:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
select
 
    #ifnot.empty(%4)
        %4
    #endif
 
    %1
 
    #ifnot.empty(%5)
        index hint %5
    #endif
 
    #ifnot.empty(%6)
        order by %6
    #endif
 
    where (%1.ConfigId          == %2.ConfigId              || ! %3.ConfigIdFlag)           &&
          (%1.InventSizeId      == %2.InventSizeId          || ! %3.InventSizeIdFlag)       &&
          (%1.InventColorId     == %2.InventColorId         || ! %3.InventColorIdFlag)      &&
          (%1.InventSiteId      == %2.InventSiteId          || ! %3.InventSiteIdFlag)       &&
          (%1.InventLocationId  == %2.InventLocationId      || ! %3.InventLocationIdFlag)   &&
          (%1.InventBatchId     == %2.InventBatchId         || ! %3.InventBatchIdFlag)      &&
          (%1.WMSLocationId     == %2.WMSLocationId         || ! %3.WMSLocationIdFlag)      &&
          (%1.WMSPalletId       == %2.WMSPalletId           || ! %3.WMSPalletIdFlag)        &&
          (%1.InventSerialId    == %2.InventSerialId        || ! %3.InventSerialIdFlag)
 
#InventDimDevelop

e é utilizado em vários lugares para se fazer algumas consultas as dimensões de estoque. Veja que ele válida se alguns parâmetros foram passados e com isso consegue fazer if no meio de instruções SQL, o que é muito útil já que em uma instrução ‘normal’, você não conseguiria.

Existem outras possibilidades, alias, as possibilidades nos usos de macros são infinitas, vai da criatividade de cada um! Eu tenho outro amigo (Daniel Zanni) que usa os macros para verificar se determinada custom está ativa nas configuration keys e caso sim, aplica a custom no que diz respeito a código.

É isso ai pessoal, espero que tenha ajudado.

Abraço,
Pichler