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:
fraction1
is the memory segment in which to set the value.0
is the base offset and enables you to express complex access operations by injecting additional offset computation into theVarHandle
.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 aMemorySegment
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