Skip Top Navigation Bar
Previous in the Series
Current Tutorial
Access Native Data Types

Access Native Data Types

 

Describe a C structure with a MemoryLayout

Getting access to structured data with only basic operations can produce complicated code difficult to maintain. An elegant solution is to use memory layouts that are more efficiently to initialize and can access more complex native data types such as C structures.

For example, let's consider a C structure that describes a fraction:

struct Fraction {
   int numerator;
   int denominator;
};

In Java, you can choose to represent this structure with a MemoryLayout:

MemoryLayout fractionLayout = MemoryLayout.structLayout(
        ValueLayout.JAVA_INT.withName("numerator"),
        ValueLayout.JAVA_INT.withName("denominator")
);

The method MemoryLayout.structLayout(MemoryLayout...) returns a StructLayout object. The structure layout contains two JAVA_INT value layouts, one for the numerator and another one for denominator. The predefined value ValueLayout.JAVA_INT contains information about how many bytes a Java int value requires.

Let's calculate the addition between two fractions using the custom MemoryLayout fractionLayout. Start with instantiating an arena to manage off-heap memory and allocating memory for two fractions and one for result fraction:

try (Arena arena = Arena.ofConfined()) {
    MemorySegment fraction1 = arena.allocate(fractionLayout);
    MemorySegment fraction2 = arena.allocate(fractionLayout);
    MemorySegment resultFraction = arena.allocate(fractionLayout);
}

Next, you need two VarHandles with access to the memory address offsets:

try (Arena arena = Arena.ofConfined()) {
    MemorySegment fraction1 = arena.allocate(fractionLayout);
    MemorySegment fraction2 = arena.allocate(fractionLayout);
    MemorySegment resultFraction = arena.allocate(fractionLayout);
    
    VarHandle numeratorHandle = fractionLayout.varHandle(PathElement.groupElement("numerator"));
    VarHandle denominatorHandle = fractionLayout.varHandle(PathElement.groupElement("denominator"));
}

A VarHandle is a dynamically strongly typed reference to a variable or to a parameterized family of variables, including static fields, non-static fields, array elements, or components of an off-heap data structure. The method call PathElement.groupElement("numerator") retrieves a memory layout named numerator.

The following step sets the values of the fractions by calling VarHandle.set(java.lang.Object...):

try (Arena arena = Arena.ofConfined()) {
    MemorySegment fraction1 = arena.allocate(fractionLayout);
    MemorySegment fraction2 = arena.allocate(fractionLayout);
    MemorySegment resultFraction = arena.allocate(fractionLayout);
    
    VarHandle numeratorHandle = fractionLayout.varHandle(PathElement.groupElement("numerator"));
    VarHandle denominatorHandle = fractionLayout.varHandle(PathElement.groupElement("denominator"));
    
    numeratorHandle.set(fraction1, 0, 1);
    denominatorHandle.set(fraction1, 0, 3);

    numeratorHandle.set(fraction2, 0, 1);
    denominatorHandle.set(fraction2, 0, 2);
}

In the numeratorHandle.set(fraction1, 0, 1) example, the set method uses three arguments:

  1. fraction1 is the memory segment in which to set the value.
  2. 0 is the base offset and enables you to express complex access operations by injecting additional offset computation into the VarHandle.
  3. 1 is the actual value to set.

Similarly, you can get access to values with VarHandle.get(java.lang.Object...) and finally calculate the result:

import java.lang.foreign.*;
import java.lang.invoke.VarHandle;
import static java.lang.foreign.MemoryLayout.*;

final MemoryLayout fractionLayout = MemoryLayout.structLayout(
        ValueLayout.JAVA_INT.withName("numerator"),
        ValueLayout.JAVA_INT.withName("denominator")
);

try (Arena arena = Arena.ofConfined()) {
    MemorySegment fraction1 = arena.allocate(fractionLayout);
    MemorySegment fraction2 = arena.allocate(fractionLayout);
    MemorySegment resultFraction = arena.allocate(fractionLayout);
    
    VarHandle numeratorHandle = fractionLayout.varHandle(PathElement.groupElement("numerator"));
    VarHandle denominatorHandle = fractionLayout.varHandle(PathElement.groupElement("denominator"));
    
    numeratorHandle.set(fraction1, 0, 1);
    denominatorHandle.set(fraction1, 0, 3);

    numeratorHandle.set(fraction2, 0, 1);
    denominatorHandle.set(fraction2, 0, 2);

    resultDenominator = d1 * d2
    int n1 = (int) numeratorHandle.get(fraction1, 0);
    int d1 = (int) denominatorHandle.get(fraction1, 0);
    int n2 = (int) numeratorHandle.get(fraction2, 0);
    int d2 = (int) denominatorHandle.get(fraction2, 0);

    // Add fractions: resultNumerator = (n1 * d2) + (n2 * d1); 
    int resultNumerator = (n1 * d2) + (n2 * d1);
    int resultDenominator = d1 * d2;
    
    // Store the result in resultFraction
    numeratorHandle.set(resultFraction, 0, resultNumerator);
    denominatorHandle.set(resultFraction, 0, resultDenominator);

    System.out.println("Result Fraction: " +
                       numeratorHandle.get(resultFraction, 0L) + "/" +
                        denominatorHandle.get(resultFraction, 0L));
}

You can check the result of your work by pasting the above snippet in a jshell session and obtain:

fractionLayout ==> [i4(numerator)i4(denominator)]
Result Fraction: 5/6

While this example adds the value of two fractions, let's see how you can handle addition of each two fractions in an array of such elements.

 

Slicing Allocators

There are two options to scale the previous example and perform addition of each two fractions in an array with a custom data type:

  • Use a slicing allocator that returns a segment allocator. The SegmentAllocator responds to allocation requests by returning consecutive contiguous regions of memory, or slices, obtained from an existing memory segment.
  • Obtain a slice of a memory segment of any location within a memory segment with the method MemorySegment.asSlice.

Let's start with storing twenty fractions in a SequenceLayout:

try (Arena arena = Arena.ofConfined()) {
    // Define the layout for a fraction 
    MemoryLayout fractionLayout = MemoryLayout.structLayout(
            ValueLayout.JAVA_INT.withName("numerator"),
            ValueLayout.JAVA_INT.withName("denominator")
    );
    
    // Create a sequence layout for storing 20 fractions
    SequenceLayout fractionArrayLayout = MemoryLayout.sequenceLayout(20, fractionLayout);
}

Next, you can allocate memory for the twenty fractions using a SegmentAllocator.slicingAllocator(MemorySegment) :

try (Arena arena = Arena.ofConfined()) {
    // Define the layout for a fraction (two integers: numerator and denominator)
    MemoryLayout fractionLayout = MemoryLayout.structLayout(
            ValueLayout.JAVA_INT.withName("numerator"),
            ValueLayout.JAVA_INT.withName("denominator")
    );
    
    
    // Create a sequence layout for storing 20 fractions
    SequenceLayout fractionArrayLayout = MemoryLayout.sequenceLayout(20, fractionLayout);
    
    // Allocate memory for 20 fractions
    MemorySegment segment = arena.allocate(fractionArrayLayout);
    SegmentAllocator allocator = SegmentAllocator.slicingAllocator(segment);
}

With the SegmentAllocator you can obtain consecutive slices from a given segment and further allocate an array of integers in each slice. In case of our example, a fraction is allocated in each slice.

try (Arena arena = Arena.ofConfined()) {
    // Define the layout for a fraction (two integers: numerator and denominator)
    MemoryLayout fractionLayout = MemoryLayout.structLayout(
            ValueLayout.JAVA_INT.withName("numerator"),
            ValueLayout.JAVA_INT.withName("denominator")
    );


    // Create a sequence layout for storing 20 fractions
    SequenceLayout fractionArrayLayout = MemoryLayout.sequenceLayout(20, fractionLayout);

    // Allocate memory for 20 fractions
    MemorySegment segment = arena.allocate(fractionArrayLayout);
    SegmentAllocator allocator = SegmentAllocator.slicingAllocator(segment);

    // Create an array of MemorySegments for each fraction
    MemorySegment[] fractions = new MemorySegment[20];

    // VarHandles to access the fields
    VarHandle numeratorHandle = fractionLayout.varHandle(PathElement.groupElement("numerator"));
    VarHandle denominatorHandle = fractionLayout.varHandle(PathElement.groupElement("denominator"));

    // Initialize fractions with values (e.g., (1/2), (3/4), etc.)
    for (int i = 0; i < 20; i++) {
        fractions[i] = allocator.allocate(fractionLayout);
        numeratorHandle.set(fractions[i],0, i + 1); 
        denominatorHandle.set(fractions[i],0, (i + 2));
    }
}

Finally, complete the logic of the example by adding each two fractions:

import java.lang.foreign.*;
import java.lang.invoke.VarHandle;
import static java.lang.foreign.MemoryLayout.*;

try (Arena arena = Arena.ofConfined()) {
    // Define the layout for a fraction (two integers: numerator and denominator)
    MemoryLayout fractionLayout = MemoryLayout.structLayout(
            ValueLayout.JAVA_INT.withName("numerator"),
            ValueLayout.JAVA_INT.withName("denominator")
    );


    // Create a sequence layout for storing 20 fractions
    SequenceLayout fractionArrayLayout = MemoryLayout.sequenceLayout(20, fractionLayout);

    // Allocate memory for 20 fractions
    MemorySegment segment = arena.allocate(fractionArrayLayout);
    SegmentAllocator allocator = SegmentAllocator.slicingAllocator(segment);

    // Create an array of MemorySegments for each fraction
    MemorySegment[] fractions = new MemorySegment[20];

    // VarHandles to access the fields
    VarHandle numeratorHandle = fractionLayout.varHandle(PathElement.groupElement("numerator"));
    VarHandle denominatorHandle = fractionLayout.varHandle(PathElement.groupElement("denominator"));

    // Initialize fractions with values (e.g., (1/2), (3/4), etc.)
    for (int i = 0; i < 20; i++) {
        fractions[i] = allocator.allocate(fractionLayout);
        numeratorHandle.set(fractions[i],0, i + 1); 
        denominatorHandle.set(fractions[i],0, (i + 2));
    }

    MemorySegment resultFraction = arena.allocate(fractionLayout);
    
    for (int i = 0; i < 20; i+=2) {

         int n1 = (int) numeratorHandle.get(fractions[i], 0);
         int d1 = (int) denominatorHandle.get(fractions[i], 0);
         int n2 = (int) numeratorHandle.get(fractions[i+1], 0);
         int d2 = (int) denominatorHandle.get(fractions[i+1], 0);

         int resultNumerator = (n1 * d2) + (n2 * d1);
         int resultDenominator = d1 * d2;

         // Store the result in resultFraction
        numeratorHandle.set(resultFraction, 0L, resultNumerator);
        denominatorHandle.set(resultFraction, 0L, resultDenominator);

        // Retrieve and print the result
        System.out.println("Result Fraction: " +
                           numeratorHandle.get(resultFraction, 0L) + "/" +
                        denominatorHandle.get(resultFraction, 0L));
    }

}

Paste the above code in a jshell session and check if the results match:

Result Fraction: 7/6
Result Fraction: 31/20
Result Fraction: 71/42
Result Fraction: 127/72
Result Fraction: 199/110
Result Fraction: 287/156
Result Fraction: 391/210
Result Fraction: 511/272
Result Fraction: 647/342
Result Fraction: 799/420

 

Slice a Memory Segment

A slicing allocator returns a slice and is useful when working with consecutive contiguous regions of memory. The slice's starting address is right after the end of the last slice that the slicing allocator returned. In case you want to obtain a slice of a memory segment of any location within a memory segment and of any size, you can call MemorySegment.asSlice(long,long).

Warning: MemorySegment.asSlice(long,long) will return a slice of a MemorySegment as long as the slice's size stays within the spatial bounds of the original memory segment.

Let's reuse the previous MemoryLayout for fractions and obtain slices of a memory segment:

try (Arena arena = Arena.ofShared()) {
    MemoryLayout fractionLayout = MemoryLayout.structLayout(
            ValueLayout.JAVA_INT.withName("numerator"),
            ValueLayout.JAVA_INT.withName("denominator")
    );

    long fractionSize = fractionLayout.byteSize();
    int amount = 20;

    // Allocate memory for 20 fractions in a contiguous memory block
    MemorySegment fractionsSegment = arena.allocate(fractionLayout.byteSize() * amount);

    // VarHandles to access the fields
    VarHandle numeratorHandle = fractionLayout.varHandle(PathElement.groupElement("numerator"));
    VarHandle denominatorHandle = fractionLayout.varHandle(PathElement.groupElement("denominator"));

    // Initialize fractions with sample values (e.g., (1/2), (2/3), etc.)
    for (int i = 0; i < amount; i++) {
        MemorySegment fractionSlice = fractionsSegment.asSlice(i * fractionSize, fractionSize);
        numeratorHandle.set(fractionSlice, 0,  i + 1);
        denominatorHandle.set(fractionSlice, 0, (i + 2)); 
    }
}

As multiple threads can work in parallel to access each of the slices, the memory segment has to be accessible to them. You can achieve this by associating the memory segment with a shared arena. Copy and paste the snippet below to try the full example in a jshell session:

import java.lang.foreign.*;
import java.lang.invoke.VarHandle;
import static java.lang.foreign.MemoryLayout.*;

try (Arena arena = Arena.ofShared()) {
    MemoryLayout fractionLayout = MemoryLayout.structLayout(
            ValueLayout.JAVA_INT.withName("numerator"),
            ValueLayout.JAVA_INT.withName("denominator")
    );

    long fractionSize = fractionLayout.byteSize();
    int amount = 20;

    // Allocate memory for 20 fractions in a contiguous memory block
    MemorySegment fractionsSegment = arena.allocate(fractionLayout.byteSize() * amount);

    // VarHandles to access the fields
    VarHandle numeratorHandle = fractionLayout.varHandle(PathElement.groupElement("numerator"));
    VarHandle denominatorHandle = fractionLayout.varHandle(PathElement.groupElement("denominator"));

    // Initialize fractions with sample values (e.g., (1/2), (2/3), etc.)
    for (int i = 0; i < amount; i++) {
        MemorySegment fractionSlice = fractionsSegment.asSlice(i * fractionSize, fractionSize);
        numeratorHandle.set(fractionSlice, 0,  i + 1);
        denominatorHandle.set(fractionSlice, 0, (i + 2)); 
    }

    for (int i = 0; i < amount; i++) {
        MemorySegment fractionSlice = fractionsSegment.asSlice(i * fractionSize, fractionSize);
        int numerator = (int) numeratorHandle.get(fractionSlice, 0);
        int denominator = (int) denominatorHandle.get(fractionSlice, 0);
        System.out.println("Fraction " + i + ": " + numerator + "/" + denominator);
    }
}

Last update: December 17, 2024


Previous in the Series
Current Tutorial
Access Native Data Types