JDK 14 Foreign-Memory Access API Overview

Ty Young
Ty Young
Mar 18 · 7 min read

With the release of JDK 14 comes plenty of new or previewing features such as pattern matching for instanceof, Helpful NullPointerExceptions, switch expressions, and more. Those features have been covered extensively by many news and blogging websites already, however, the “incubating” Foreign Memory Access API hasn’t gotten as much coverage, with it being omitted or given a 1–2 line mention by many news sites covering JDK 14. Chances are not many people know anything about it or what it will eventually allow you to do within Java.

The short of it is that the Foreign-Memory Access API, part of Project Panama, is a replacement for ByteBuffers which were previously used for off-heap memory. Off-heap memory is desirable for anything low level I/O as the memory avoids GC, thereby being faster and dependable than on-heap memory. ByteBuffers, however, have many limitations including a 2GB size limit.

If you want to know more, you can watch the presentation by Maurizio Cimadamore here:

As the above video goes over, the incubating Foreign-Memory Access API isn’t the goal but rather a stepping stone to an even greater purpose: native C library access in Java. Sadly there is no ETA on when that will be delivered.

That said, if you do want to try the real good stuff, then you can build your own JDK build from Github. I’ve been doing just that, making bindings for various Nvidia APIs that I needed for my overclocking utility which utilize an abstraction layer over Panama to make things easier.

With that all out of the way, how do you actually use it?

MemoryAddress & MemorySegment

The two major interfaces in Project Panama are MemoryAddress and MemorySegment. In the Foreign-Memory Access API, getting a MemoryAddress first requires that you create a MemorySegment using the static allocateNative() method and then get the base address of that segment:

import jdk.incubator.foreign.MemoryAddress;
import jdk.incubator.foreign.MemorySegment;
public class PanamaMain
{
public static void main(String[] args)
{
MemoryAddress address = MemorySegment.allocateNative(4).baseAddress();
}
}

You can then, of course, get the segment of that same MemoryAddress again via the segment() method of MemoryAddress . In the above, we are using the overloaded allocateNative() which takes in a long value of the size in bytes the new MemorySegment is for. There are two other versions of this method, one which accepts a MemoryLayout, which I’ll get to later, and one which accepts a size in bytes AND the byte alignment.

MemoryAddress doesn’t have a whole lot of API in itself. The only notable methods are segment() andoffset() . There is no method to get the raw address of a MemoryAddress.

MemorySegment, on the other hand, has a bit more going on with its API. You can convert a MemorySegment to a ByteBuffer via asByteBuffer(), close(read: free) the segment via close() (from the AutoClosable interface), and slice it with asSlice() (more on that later).

OK, we’ve allocated a chunk of memory but how do you read and write to it?

MemoryHandles

MemoryHandles is a class that provides VarHandles for reading and writing memory values. It provides a few static methods for getting a VarHandle, but the major one is varHandle which accepts a class of either:

  • byte.class
  • short.class
  • char.class
  • int.class
  • double.class
  • long.class

(None of which are to be confused with the Object version, like Integer.class)

…and a ByteOrder. In most cases, you just want to use the native order via nativeOrder() . As for the class that you use, you use a class that fits the MemorySegment’s byte size, so for the above example, int.class since int takes up 4 bytes in Java.

Once you’ve created a VarHandle you can now use it to read and write memory. Reading is done via the VarHandle’s various get() methods. The documentation on these get method aren’t really that helpful IMO, but the short of it is that you pass the MemoryAddress instance to the get method like so:

import java.lang.invoke.VarHandle;
import java.nio.ByteOrder;
import jdk.incubator.foreign.MemoryAddress;
import jdk.incubator.foreign.MemoryHandles;
import jdk.incubator.foreign.MemorySegment;
public class PanamaMain
{
public static void main(String[] args)
{
MemoryAddress address = MemorySegment.allocateNative(4).baseAddress();

VarHandle handle = MemoryHandles.varHandle(int.class, ByteOrder.nativeOrder());

int value = (int)handle.get(address);

System.out.println("Memory Value: " + value);
}
}

You'll notice here that the value returned from the VarHandle is type casted. If you’ve worked with VarHandles before this isn’t a shock to you, but if you haven’t then just know that this is normal as VarHandle instances return Object.

By default, all memory allocated by the Foreign-Memory Access API is zeroed. This is great since you won’t get random junk left in memory but potentially bad for performance-critical situations.

As for setting a value, you use the set() method. Just as with get() , you pass the address followed by the value you want to pass into memory:

import java.lang.invoke.VarHandle;
import java.nio.ByteOrder;
import jdk.incubator.foreign.MemoryAddress;
import jdk.incubator.foreign.MemoryHandles;
import jdk.incubator.foreign.MemorySegment;
public class PanamaMain
{
public static void main(String[] args)
{
MemoryAddress address = MemorySegment.allocateNative(4).baseAddress();

VarHandle handle = MemoryHandles.varHandle(int.class, ByteOrder.nativeOrder());

handle.set(address, 10);

int value = (int)handle.get(address);

System.out.println("Memory Value: " + value);
}
}

MemoryLayout & MemoryLayouts

MemoryLayouts class provides predefined implementations of the MemoryLayout interface. These allow you to quickly allocate MemorySegments which are guaranteed to allocate the equivalent type, such as a Java int. Generally speaking, using these predefined layouts is way easier than allocating chunks of memory as they provide common layout types you’d want to use without having to lookup their size:

import java.lang.invoke.VarHandle;
import java.nio.ByteOrder;
import jdk.incubator.foreign.MemoryAddress;
import jdk.incubator.foreign.MemoryHandles;
import jdk.incubator.foreign.MemoryLayouts;
import jdk.incubator.foreign.MemorySegment;
public class PanamaMain
{
public static void main(String[] args)
{
MemoryAddress address = MemorySegment.allocateNative(MemoryLayouts.JAVA_INT).baseAddress();

VarHandle handle = MemoryHandles.varHandle(int.class, ByteOrder.nativeOrder());

handle.set(address, 10);

int value = (int)handle.get(address);

System.out.println("Memory Value: " + value);
}
}

If you don’t want to use these predefined layouts, you don’t have to. MemoryLayout(notice no ‘s’) has static methods that allow you to create your own. These methods return extended interfaces such as:

  • ValueLayout
  • SequenceLayout
  • GroupLayout

ValueLayout interface implementations are returned from the ofValueBits() method. All this does is create a basic single-value MemoryLayout like MemoryLayouts.JAVA_INT.

SequenceLayout is for creating a sequence of the same MemoryLayout, like arrays. Interface implementations are returned via the two static ofSequence() methods, although only the one which specifies a length can be used to allocate memory.

GroupLayout is for structs and union type memory allocations as they are fairly similar to one another. Interface implementations of these come from either ofStruct() for structs or ofUnion() for unions.

If it wasn’t made clear before, the use of MemoryLayout(s) are completely optional, however, they make the API a bit easier to use and debug as instead of reading raw numbers you instead have constant names.

However, they come with their own issues. Anything which accepts a var args MemoryLayout input as part of a method or constructor will also accept a GroupLayout or some other MemoryLayout when that isn’t an expected input. Make sure you specify the correct layouts!

Slicing & Arrays

MemorySegments can be sliced in order to store multiple values within a single block of Memory and is commonly used when working with arrays, structs, and unions. This, as was mentioned above, is done via the asSlice() method. In order to slice, you need to know both the starting location, in bytes, of the MemorySegment you want to slice as well as the size of the value stored in that location, in bytes. This returns a MemorySegment which you can then get the MemoryAddress of:

import java.lang.invoke.VarHandle;
import java.nio.ByteOrder;
import jdk.incubator.foreign.MemoryAddress;
import jdk.incubator.foreign.MemoryHandles;
import jdk.incubator.foreign.MemorySegment;
public class PanamaMain
{
public static void main(String[] args)
{
MemoryAddress address = MemorySegment.allocateNative(24).baseAddress();

MemoryAddress address1 = address.segment().asSlice(0, 8).baseAddress();
MemoryAddress address2 = address.segment().asSlice(8, 8).baseAddress();
MemoryAddress address3 = address.segment().asSlice(16, 8).baseAddress();

VarHandle handle = MemoryHandles.varHandle(long.class, ByteOrder.nativeOrder());
handle.set(address1, Long.MIN_VALUE);
handle.set(address2, 0);
handle.set(address3, Long.MAX_VALUE);

long value1 = (long)handle.get(address1);
long value2 = (long)handle.get(address2);
long value3 = (long)handle.get(address3);


System.out.println("Memory Value 1: " + value1);
System.out.println("Memory Value 2: " + value2);
System.out.println("Memory Value 3: " + value3);
}
}

Something to point out here is that you DO NOT need to create new VarHandles for each MemoryAddress you are working with.

Out of a large 24-byte block of memory, we’ve carved it into 3 different slices, making it an array.

Instead of hardcoding the slicing values, you can use a for loop to iterate over it:

import java.lang.invoke.VarHandle;
import java.nio.ByteOrder;
import jdk.incubator.foreign.MemoryAddress;
import jdk.incubator.foreign.MemoryHandles;
import jdk.incubator.foreign.MemorySegment;
public class PanamaMain
{
public static void main(String[] args)
{
MemoryAddress address = MemorySegment.allocateNative(24).baseAddress();

VarHandle handle = MemoryHandles.varHandle(long.class, ByteOrder.nativeOrder());

for(int i = 0; i <= 2; i++)
{
MemoryAddress slice = address.segment().asSlice(i*8, 8).baseAddress();

handle.set(slice, i*8);

System.out.println("Long slice at location " + handle.get(slice));
}
}
}

and of course, you can use a SequenceLayout instead of using raw, hardcoded values:

import java.lang.invoke.VarHandle;
import java.nio.ByteOrder;
import jdk.incubator.foreign.MemoryAddress;
import jdk.incubator.foreign.MemoryHandles;
import jdk.incubator.foreign.MemoryLayout;
import jdk.incubator.foreign.MemoryLayouts;
import jdk.incubator.foreign.MemorySegment;
import jdk.incubator.foreign.SequenceLayout;
public class PanamaMain
{
public static void main(String[] args)
{
SequenceLayout layout = MemoryLayout.ofSequence(3, MemoryLayouts.JAVA_LONG);
MemoryAddress address = MemorySegment.allocateNative(layout).baseAddress();

VarHandle handle = MemoryHandles.varHandle(long.class, ByteOrder.nativeOrder());

for(int i = 0; i < layout.elementCount().getAsLong(); i++)
{
MemoryAddress slice = address.segment().asSlice(i*layout.elementLayout().byteSize(), layout.elementLayout().byteSize()).baseAddress();

handle.set(slice, i*layout.elementLayout().byteSize());

System.out.println("Long slice at location " + handle.get(slice));
}
}
}

What’s Not Included

Everything so far has only been in the scope of JDK 14’s incubating version, however, as was mentioned before, this is all a stepping stone towards native C library access and is even out-of-date with one or two method names being changed. There is yet another layer on top of all this which finally enables you to access native library calls. To summarize what’s missing:

  • jextract
  • Library Lookups
  • ABI specific ValueLayout
  • Runtime ABI layouts
  • FunctionDescriptor interface
  • ForeignUnsafe

All of which are layered on top of and in addition to the Foreign-Memory Access API. If you plan on creating bindings for some native C library, learning the API now won’t go to waste.

Ty Young

Written by

Ty Young

More From Medium

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade