Macros in X++
This article describes how to create and use macros in X++.
Precompiler directives are processed before the code is compiled. The directives declare and handle macros and their values. The directives are removed by the precompiler so that the X++ compiler never encounters them. The X++ compiler only sees the sequence of characters written into the X++ code by the directives.
Warning
Use of macros is not recommended. Macros are supported for backwards compatibility only.
Instead of macros, use language constructs like these:
#define and #if directives
All precompiler directives and symbols begin with the #
character. A macro can be defined at any point in the code. The variable can have a value that is a sequence of characters, but it is not required to have a value. The #define
directive tells the precompiler to create the macro variable, including an optional value. The #if
directive tests whether the variable is defined, and optionally, whether it has a specific value. The X++ precompiler directives, the macro names that they define, and the #if
directive value tests are all case-insensitive. However, it is a best practice to begin macro names with an uppercase letter.
#undef directive
You can use the #undef
directive to remove a macro definition that exists from a previous #define
. After a macro name has been created by #define
and then removed by #undef
, the macro can be created again by another #define
. #undef
has no effect on macros that are created by the #localmacro
directive.
Use a Macro Value
You can define a macro name to have a value. A macro value is a sequence of characters. A macro value is not a string (or str) in the formal sense of a data type. You assign a value to a macro by appending the value enclosed in parentheses at the end of a #define
directive. You can use the macro symbol where you want the value to occur in the X++ code. A macro symbol is the name of the macro with the #
character added as a prefix. The following code sample shows a macro symbol #MyMacro. The symbol is replaced by the value of the macro.
Test a Macro Value
You can test a macro to see whether it has a value. You can also test to see whether its value is equal to a specific sequence of characters. These tests enable you to conditionally include lines of code in your X++ program. There is no way you can test whether a defined macro has a value. You can only test whether a specific value matches the value of a macro. As a best practice, any macro name that you define should always have a value, or it should never have a value. When you alternate between these modes, your code becomes difficult to understand. For macros that have a value, you can vary the value when you see fit.
#defInc and #defDec directives
#defInc
and #defDec
are the only directives that interpret the value of a macro and they apply only to macros that have a value that can be converted to the formal int type. The value can only contain numerals. The only non-numeric character allowed is a leading negative sign (-). The integer value is treated as an X++ int, not as an int64. For macro names that are used by the #defInc
directive, it is important that the #define
directive that creates the macro not reside in a class declaration. The behavior of #defInc
in these cases is unpredictable. Instead, such macros should be defined in only a method. We recommend that the #defInc
and #defDec
directives only be used for macros that have an integer value. The precompiler follows special rules for #defInc
when the macro value is not an integer, or when the value is unusual or extreme. The following table lists the values that #defInc
converts to zero (0) and then increments. When a value is converted to 0 by #defInc
, the original value cannot be recovered, not even by #defDec
.
Macro value | Behavior |
---|---|
(+55) | The positive sign (+) prefix makes the precompiler treat this as a non-numeric string. The precompiler treats all non-numeric strings as 0 when it handles a #defInc (or #defDec ) directive. |
("3") | Integers enclosed in quotation marks are treated as 0. The quotation marks are discarded, and these changes persist. |
( ) | A string of spaces is treated as 0, and then incremented. |
() | A zero-length string is treated as 0, and then incremented, when the value is enclosed in parentheses, as in #define.MyMac(). |
(Random string.) | Any non-numeric string of characters is treated as 0, and then incremented. |
(0x12) | Hexadecimal numbers are treated as non-numeric strings. Therefore they are converted to 0, and then incremented. |
(-44) | Negative numbers are acceptable, including integers without the negative sign (- ). |
(2147483647) | The maximum positive int value is changed to the minimum negative int value by #defInc . |
(999888777666555) | Any large number, beyond the capacity of int and int64. This is treated as the maximum positive int value. |
(5.8) | Real numbers are truncated by #defDec (and #defInc ). Subsequent symbol substitution shows that the truncation persists. |
When no value and no parentheses are provided for the directive #define.MyValuelessMacro , the precompiler rejects use of the directive #defInc .MyValuelessMacro. |
#globaldefine directive
The #globaldefine
directive is similar to the #define
directive. The difference is that #define
directives generally take precedence over #globalmacro
directives. This is true regardless of which directive occurs first in the X++ code. A #globaldefine
never overwrites a #define
directive that has both a macro name and a value. A #globaldefine
can overwrite another #globaldefine
. A #define
directive that has only a name does not overwrite a #globalmacro
that has both a name and a value. It is recommended that you use #define,
and that you do not use #globaldefine.
Use of #globaldefine
can create uncertainty that makes code difficult to maintain. The exact semantics of #globaldefine
cannot be achieved through #if
test directives. By using #if
tests you can avoid overwriting a #define
and a #globaldefine.
But #if
tests cannot distinguish between #define
and #globaldefine
macros.
Macro parameters
You can define macro values to include parameter symbols. The first parameter symbol is %1
, the second is %2
, and so on. You pass values for the parameters when you reference the macro symbol name for expansion. Macro parameter values are character sequences of no formal type, and they are comma delimited. There is no way to pass in a comma as part of a parameter value. The number of parameters passed can be less than, greater than, or equal to the number of parameters that the macro value is designed to receive. The system tolerates mismatches in the number of parameters passed. If fewer parameters are passed than the macro expects, each omitted parameter is treated as a zero-length sequence of characters.
#localmacro and #globalmacro directives
The #localmacro
directive is a good choice when you want a macro to have a value that is several lines long, or when your macro value contains a closing parenthesis. The #localmacro
directive is a good choice when you want your macro value to be lines of X++ or SQL code. The #localmacro
directive can be written as #macro
. However, #localmacro
is the recommended term. Both macros have the same behavior. By using the #if
directive, you can test whether a macro name is declared with the #define
directive. However, you cannot test whether the macro name is declared with the #localmacro
directive. Only macros declared by using the #define
directive are affected by the #undef
directive. In a #define
directive, you can specify a name that is already in scope as a #localmacro
. The effect is to discard the #localmacro
and create a #define
macro. This also applies to the opposite sequence, which means that a #localmacro
can redefine a #define
. A #localmacro
(that has both a macro name and a value) always overrides a previous #localmacro
that has the same name. However, you cannot always be sure whether the override occurs when you use #globalmacro.
For this reason we recommend that you do not use #globalmacro
. This same problem occurs with #globaldefine
. The main difference between a #define
macro and a #localmacro
macro is in how their syntax is terminated. The terminators are as follows:
#define
– is terminated by–)
#localmacro
– is terminated by–#endmacro
#localmacro
is a better choice for macros with multiple line values. Multiple line values are typically lines of X++ or SQL code. X++ and SQL contain lots of parentheses, and these would prematurely terminate a #define. Both #define
and #localmacro
can be declared and terminated on either a single line or on subsequent lines. In practice, the #define
is terminated on the same line that it is declared on. In practice, the #localmacro
is terminated on a subsequent line. Where both macro names and values are supplied, the #globalmacro
directive cannot override the #define directive. Also, the #globaldefine
directive cannot override the #localmacro
directive.
Nesting Macro Symbols
You can nest precompiler definition directives inside an outer definition directive. The main definition directives are #define
and #localmacro
.
A #define
directive can be given inside a #localmacro
directive, and a #localmacro
can be inside a #define
.
#macrolib directive
In the Application Explorer under the Macros node, there are many library nodes that contain sets of macro directives. Both #define
and #localmacro
often appear in the contents of these macro libraries. You can use the #macrolib.MyAOTMacroLibrary to include the contents of a macro library in your X++ code. The #if
and #undef
directives do not apply to #macrolib
names. However, they do apply to #define
directives that are the contents of a #macrolib
macro. The directive #macrolib.MyAOTMacroLibrary can also be written as #MyAOTMacroLibrary. The #macrolib
prefix is recommended because it is never ambiguous to a person who later reads the code.
#linenumber Directive
You can use the #linenumber
directive during your development and debugging of code. It is replaced by the physical line number in the code file.
Range (scope) of macros
The range in which a macro can be referenced depends on where the macro is defined. In a class, macros that are defined in the parent class can be referenced, but macros defined in a child class cannot be referenced. When the precompiler handles a child class, the precompiler first traces the inheritance chain to the most ascendant class. The precompiler processes all the directives from the class declaration part of the ascendant class. It stores all the macros and their values in its internal tables. The precompiler handles the next class in the inheritance chain the same way. The results of the directives in each class declaration are applied to the internal tables that are already populated from directives that were found earlier in the inheritance chain. When the precompiler reaches the target child class, it again handles the class declaration part. However, it next handles each method in a series of separate operations. The precompiler updates its internal tables in a way that the state of the tables can be restored as they were before processing of the current method began. After the first method is handled, the internal tables are restored before the next method is handled.
The Method is All Contents of the Node
In this context, a method is defined as the contents of a method node in the Application Object Tree (AOT). In the AOT, you can expand the Classes node, expand a class node, right-click a method node, and then select Edit. Then you can add a line for #define.MyMacro("abc")
before the method declaration. The precompiler treats this #define
directive as part of the method, even though the #define
occurs outside the {}
block of the method.