X++ Arrays Behave Differently in .NET CIL, in Dynamics AX 2012
1. Understanding the Difference
There has been some confusion about the behavior of X++ arrays in Microsoft Dynamics AX 2012, because the behavior can differ from AX 2009. Let’s clear up the confusion.
In X++, you can assign an array of primitive integers to another array. This is shown in line 3:
1 2 3 |
int myArray[3], yourArray[3]; myArray[1] = 101; yourArray = myArray; |
In AX 2009 and earlier, when one whole array was assigned to another, the assignment mechanism for primitive types was always by value. However...
Starting in AX 2012, for X++ arrays the assignment mechanism of primitive types is sometimes by value, and other times by reference. The assignment mechanism is determined by the X++ execution mode, which is either the interpreter or as CIL (new in AX 2012).
The following table relates both X++ execution modes to both assignment mechanisms:
X++ execution mode |
Execution context |
Array assignment mechanism |
Under the X++ interpreter |
Running on the AX client. |
Assigned by value. An independent copy is made of the value, and the address of the copy is given to the assignee. |
As .NET Framework CIL |
Running as AX batch, AX service, or by Global ::runClassMethodIL (or by the X++ runAs function). In CIL, X++ arrays are implemented as .NET managed arrays, which always pass by reference. |
Assigned by reference. No copy of the value is made. |
2. Terminology
By Value: In the by value case, the assignment creates a copy or second occurrence of the assigned primitive value. The assignee variable is given the address of the copy. Now the assigner’s copy is unaffected when the assignee changes its value.
By Reference: In the by reference case, the assignment does not create any copy or second occurrence of the assigned primitive value. Instead, the assignee variable is given the same internal memory location of the value that the assigner has. Now both the assigner and assignee variables are reading from and writing to the same memory location. One of the two variables might be used to over-write the location with a new value, thereby affecting both variables (perhaps unintentionally).
Primitive versus Object: In X++ the primitive data types include int, str, and boolean. An instance of a class is an object.
X++ arrays can store only primitives. They cannot store objects like those constructed from classes.
3. X++ Code Snippet for Explanation
In the following X++ code snippet, consider the array assignment at line 3. Merely reading the X++ code does not enable us to ascertain whether the array assignment mechanism is by value or by reference. We have to know the execution mode. Line 4 has different effects between the two execute modes:
1 2 3 4 5 6 |
int arrayA[2], arrayB[2]; arrayA[1] = 111; arrayB = arrayA; // By value, or by reference? It depends! arrayB[1] = 222; // Does this affect arrayA[1]? It depends! // If line 4 affected arrayA[1], then assignment is By REFERENCE. Global::info(strFmt("%1: changed to 222, or remains 111? ", arrayA[1])); |
4. The Fix: Uniform Behavior is Achievable
The variation in array assignment mechanisms has caught some X++ developers by surprise, quite understandably. To avoid array bugs in your X++ code, you should design your code to behave the same in both execution modes.
In both modes, your code should consistently use only one of the assignment mechanisms between the two arrays: uniformly use either (a) by value assignments between individual elements of the two arrays, or (b) by reference assignments between the two arrays.
Either of these two assignment mechanisms (by value or reference) can be achieved in either of these modes (X++ interpreted or as CIL), with a pair of either X++ arrays or AX Array class objects.
The following table provides code snippets to illustrate the design options.
Behavior type |
Design for uniform behavior |
X++ Code snippet |
all by Value |
Use a loop to assign one element at a time, from one X++ array to another. Each element is assigned by value. |
int aa[2], bb[2]; aa[1] = 101; aa[2] = 102; for (i=1; i < dimof(aa); i++) { bb[i] = aa[i]; } bb[1] = 201; // Not affect aa[1]! |
all by Reference |
Use the AX class named Array, instead of the array syntax that is built into the X++ language. All assignments from one Array object to another are by reference. |
Array oarrayC = new Array(Types::Integer); Array oarrayD = oarrayC; oarrayC.value(1, 301); oarrayD.value(1, oarrayC.value(1)); oarrayD.value(1, 401); // Affects oarrayC! //oarrayD = oarrayC; // Is by Reference. |
5. Design Decision of the AX Product Team
During development of AX 2012, the product team strove to minimize any functional differences between interpreted X++ versus X++ as CIL. However, X++ arrays were one compromise we felt was necessary.
It would have been expensive to generate CIL that implements pass-by-value semantics for arrays. Also, the interoperability between AX and code written in other .NET languages would have become contorted, which would have been an unwise trade-off in the long run.
6. Demo: ArrayTestClass
This optional section provides X++ code for a runnable demonstration. The demo does the following:
- Runs each test twice: once in the interpreter, and once as CIL.
- Compares the behaviors of X++ arrays versus the AX Array class.
- Compares the effects of assigning a whole array versus assigning each element one at a time.
Steps:
To run the demo, you can perform the following steps:
- In AX, create a class named ArrayTestClass.
- Add the following method names, and paste in the code from below.
- Main – Runs Test1 in interpreter, and again as CIL. Same for Test2.
- Test1 – Assigns a whole array; uses X++ arrays.
- Test2 – Assigns a whole array object; uses the AX Array class.
- Test3 – Assigns one element at a time; uses X++ arrays.
- Test4 – Assigns one element at a time; uses the AX Array class.
- Menu: Build > Generate Incremental CIL.
- In the X++ code editor, open the Main method, and then run it (by pressing F5, or clicking the green arrow to “Go”).
Outputs:
The demo outputs for each Test method are in /**comment sections**/ at the end of each Test method’s code.
Main
server static public void Main(Args _args) // Run this method.{ new XppILExecutePermission().assert(); // Using: Built-in X++ language array syntax. ArrayTestClass::Test1 (["X++_in_Interpreter, X++ language array, by Value", 1]); Global::runClassMethodIL("ArrayTestClass", "Test1", ["As_CIL, X++ language array, by Reference", 2]);
// Using: AX Array class. Global::info(" "); ArrayTestClass::Test2 (["X++_in_Interpreter, Array class, by Reference", 3]); Global::runClassMethodIL("ArrayTestClass", "Test2", ["As_CIL, Array class, by Reference", 4]);
// Using: Built-in X++ language array syntax, one element. Global::info(" "); ArrayTestClass::Test3 (["X++_in_Interpreter, Element of X++ lang array, by Value", 5]); Global::runClassMethodIL("ArrayTestClass", "Test3", ["As_CIL, Element of X++ lang array, by Value", 6]);
// Using: AX Array class. Global::info(" "); ArrayTestClass::Test4 (["X++_in_Interpreter, Array class, by Value", 7]); Global::runClassMethodIL("ArrayTestClass", "Test4", ["As_CIL, Array class, by Value", 8]);} |
Test1
static public void Test1(container _conr) // Built-in X++ language array syntax.{ int arrayA[2]; int arrayB[2]; int nTrialNum = conPeek(_conr, 2); str sExecMode = conPeek(_conr, 1); arrayA[1] = 101; arrayB = arrayA; // Whole array: By value or by reference? arrayB[1] = 201; Global::info(strFmt( "Test1(), Trial %1, whole array: arrayA[1] = %2 , arrayB[1] = %3 :MODE of %4.", nTrialNum, arrayA[1], arrayB[1], sExecMode));} /*** Output results: Notice arrayA[2] is changed only in CIL. Test1(), Trial 1, whole array: arrayA[1] = 101 , arrayB[1] = 201 :MODE of X++_in_Interpreter, X++ language array, by Value.
Test1(), Trial 2, whole array: arrayA[1] = 201 , arrayB[1] = 201 :MODE of As_CIL, X++ language array, by Reference. ***/ |
Test2
static public void Test2(container _conr) // AX Array class.{ Array orrayC = new Array(Types::Integer); Array orrayD = new Array(Types::Integer); int nTrialNum = conPeek(_conr, 2); str sExecMode = conPeek(_conr, 1); orrayC.value(1, 101); orrayD = orrayC; // Whole array object is assigned. orrayD.value(1, 201); //By value, or by reference? Always by reference! Global::info(strFmt( "Test2(), Trial %1, whole array: orrayC.value(1) = %2 , orrayD.value(1) = %3 :MODE of %4.", nTrialNum, orrayC.value(1), orrayD.value(1), sExecMode));} /*** Output results: Notice oarrayC.value(1) is changed in interpreter and CIL. Test2(), Trial 3, whole array: orrayC.value(1) = 201 , orrayD.value(1) = 201 :MODE of X++_in_Interpreter, Array class, by Reference.
Test2(), Trial 4, whole array: orrayC.value(1) = 201 , orrayD.value(1) = 201 :MODE of As_CIL, Array class, by Reference. ***/ |
Test3
static public void Test3(container _conr) // One element, of built-in X++ array.{ int arrayA[2]; int arrayB[2]; int nTrialNum = conPeek(_conr, 2); str sExecMode = conPeek(_conr, 1); arrayA[2] = 102; arrayB[2] = arrayA[2]; // One element: By value or by reference? By value! arrayB[2] = 202; Global::info(strFmt( "Test3(), Trial %1, one element: arrayA[2] = %2 , arrayB[2] = %3 :MODE of %4.", nTrialNum, arrayA[2], arrayB[2], sExecMode));} /*** Output results: Notice arrayA[2] is unchanged even in CIL. Test3(), Trial 5, one element: arrayA[2] = 102 , arrayB[2] = 202 :MODE of X++_in_Interpreter, Element of X++ lang array, by Value.
Test3(), Trial 6, one element: arrayA[2] = 102 , arrayB[2] = 202 :MODE of As_CIL, Element of X++ lang array, by Value. ***/ |
Test4
static public void Test4(container _conr) // AX Array class, assign one element.{ Array orrayC = new Array(Types::Integer); Array orrayD = new Array(Types::Integer); int nTrialNum = conPeek(_conr, 2); str sExecMode = conPeek(_conr, 1); orrayC.value(1, 101); orrayD.value(1, orrayC.value(1)); // One element: By value, or by reference? orrayD.value(1, 201); Global::info(strFmt( "Test4(), Trial %1, one element: orrayC.value(2) = %2 , orrayD.value(2) = %3 :MODE of %4.", nTrialNum, orrayC.value(1), orrayD.value(1), sExecMode));} /*** Output results: Notice arrayC.value(1) was never affected by arrayD. Thus by value. Test4(), Trial 7, one element: orrayC.value(2) = 101 , orrayD.value(2) = 201:MODE of X++_in_Interpreter, Array class, by Value.
Test4(), Trial 8, one element: orrayC.value(2) = 101 , orrayD.value(2) = 201:MODE of As_CIL, Array class, by Value. ***/ |
7. Links to AX 2012 Help on MSDN
X++ Scenarios that are Not Supported in CIL Debug in Interpreted Mode Your X++ Code that Runs as .NET CIL |
This blog post refers primarily to product version Microsoft Dynamics AX
2012 (AX 2012, AX2012).
"X++ Arrays Behave Differently in .NET CIL, in Dynamics AX 2012"
Gene Milener & Peter Villadsen, September 2012
//eof