Memory Layouts and Structured Access

Accessing structured data using only basic operations can lead to hard-to-read code that's difficult to maintain. Instead, you can use memory layouts to more efficiently initialize and access more complicated native data types such as C structures.

For example, consider the following C declaration, which defines an array of Point structures, where each Point structure has two members, Point.x and Point.y:

struct Point {
   int x;
   int y;
} pts[10];

You can initialize and access such a native array as follows:

        try (Arena arena = Arena.ofConfined()) {

            MemorySegment segment =
                arena.allocate((long)(2 * 4 * 10), 1);

            for (int i = 0; i < 10; i++) {
                int xValue = i;
                int yValue = i * 10;
                segment.setAtIndex(ValueLayout.JAVA_INT, (i * 2),     xValue);
                segment.setAtIndex(ValueLayout.JAVA_INT, (i * 2) + 1, yValue);
            }
        
            for (int i = 0; i < 10; i++) {
                int xVal = segment.getAtIndex(ValueLayout.JAVA_INT, (i * 2));
                int yVal = segment.getAtIndex(ValueLayout.JAVA_INT, (i * 2) + 1);
                System.out.println("(" + xVal + ", " + yVal + ")");
            }
        }

The first argument in the call to the Arena::allocate method calculates the number of bytes required for the array. The arguments in the calls to the MemorySegment::setAtIndex method calculate which memory address offsets to write into each member of a Point structure. Similar arguments perform the same calculations for the MemorySegment::getAtIndex method. To avoid these calculations, you can use a memory layout.

To represent the array of Point structures, the following example uses a sequence memory layout:

        try (Arena arena = Arena.ofConfined()) {
            
            SequenceLayout ptsLayout
                = MemoryLayout.sequenceLayout(10,
                    MemoryLayout.structLayout(
                        ValueLayout.JAVA_INT.withName("x"),
                        ValueLayout.JAVA_INT.withName("y")));

            VarHandle xHandle
                = ptsLayout.varHandle(PathElement.sequenceElement(),
                    PathElement.groupElement("x"));
            VarHandle yHandle
                = ptsLayout.varHandle(PathElement.sequenceElement(),
                    PathElement.groupElement("y"));            

            MemorySegment segment = arena.allocate(ptsLayout);
            
            for (int i = 0; i < ptsLayout.elementCount(); i++) {
                int xValue = i;
                int yValue = i * 10;
                xHandle.set(segment, 0L, (long) i, xValue);
                yHandle.set(segment, 0L, (long) i, yValue);
            }
            
            for (int i = 0; i < ptsLayout.elementCount(); i++) {
                int xVal = (int) xHandle.get(segment, 0L, (long) i);
                int yVal = (int) yHandle.get(segment, 0L, (long) i);
                System.out.println("(" + xVal + ", " + yVal + ")");
            }
        }

The first statement creates a sequence memory layout, which is represented by a SequenceLayout object. It contains a sequence of ten structure layouts, which are represented by StructLayout objects. The method MemoryLayout::structLayout returns a StructLayout object. Each structure layout contains two JAVA_INT value layouts named x and y:

            SequenceLayout ptsLayout
                = MemoryLayout.sequenceLayout(10,
                    MemoryLayout.structLayout(
                        ValueLayout.JAVA_INT.withName("x"),
                        ValueLayout.JAVA_INT.withName("y")));

The predefined value ValueLayout.JAVA_INT contains information about how many bytes a Java int value requires.

The next statements create two memory-access VarHandles that obtain memory address offsets. A VarHandle is a dynamically strongly typed reference to a variable or to a parametrically-defined family of variables, including static fields, non-static fields, array elements, or components of an off-heap data structure.

            VarHandle xHandle
                = ptsLayout.varHandle(PathElement.sequenceElement(),
                    PathElement.groupElement("x"));
            VarHandle yHandle
                = ptsLayout.varHandle(PathElement.sequenceElement(),
                    PathElement.groupElement("y")); 

The method PathElement.sequenceElement() retrieves a memory layout from a sequence layout. In this example, it retrieves one of the structure layouts from ptsLayout. The method call PathElement.groupElement("x") retrieves a memory layout named x. You can create a memory layout with a name with the withName(String) method.

The for statements call VarHandle::set and VarHandle::get to access memory like MemorySegment::setAtIndex and MemorySegment::getAtIndex.

            MemorySegment segment = arena.allocate(ptsLayout);
            
            for (int i = 0; i < ptsLayout.elementCount(); i++) {
                int xValue = i;
                int yValue = i * 10;
                xHandle.set(segment, 0L, (long) i, xValue);
                yHandle.set(segment, 0L, (long) i, yValue);
            }
            
            for (int i = 0; i < ptsLayout.elementCount(); i++) {
                int xVal = (int) xHandle.get(segment, 0L, (long) i);
                int yVal = (int) yHandle.get(segment, 0L, (long) i);
                System.out.println("(" + xVal + ", " + yVal + ")");
            }

In this example, the set method uses four arguments:

  1. segment: the memory segment in which to set the value
  2. 0L: the base offset, which is a long coordinate that points to the start of the array
  3. (long) i: a second long coordinate that indicates the array index in which to set the value
  4. xValue and yValue: the actual value to set

The VarHandles xHandle and yHandle know the size of the Point structure (8 bytes) and the size of its int members (4 bytes). This means you don't have to calculate the number of bytes required for the array's elements or the memory address offsets like in the setAtIndex method.

Tip:

The base offset enables you to express complex access operations by injecting additional offset computation into the VarHandle. In particular, you can use memory segments and base offsets to model variable-length arrays. These are arrays whose size are not known statically and that cannot be represented using a sequence layout. You can access such memory segments with the MemoryLayout::arrayElementVarHandle method. See the section Working with variable-length arrays in the JavaDoc API documentation for the MemoryLayout interface for examples.