The JEP 442 Foreign Function & Memory API (Third Preview) just broke cover. In this article we will explore some new features proposed in this JEP, the JEP being a candidate to be included in JDK 21.
The JEP is a Preview Feature meaning the Foreign Function & Memory API (hereafter "FFM") will not be a final feature in JDK 21. I think some of us hoped it could make it into finality in 21. As I see it, the principle "It’s ready when it’s ready" is really the responsible path forward. This means new features are streamlined and battle-proven once they reach finality. Good things are simply worth waiting for.
The evolution of the FFM API is driven by several factors whereof community feedback is perhaps the most important one. If you are less interested in the FFM history and evolution, please feel free to skip directly to the section Show Me the Code.
There was a lot of feedback from the previous FFM previews that were taken into consideration in the JEP, some of which are listed here:
-
There were two different ways to allocate native memory (e.g. via
MemorySegment::allocateNative
andArena::allocate
). -
Users are inclined to use
Arena
directly and so, using a derivedSegmentScope
for the lifecycle is a bit confusing. -
There is no default linker for unsupported platforms.
-
There is no "performance mode" for very short-lived native calls.
-
Allocation of native memory is now exclusively done via the
Arena::allocate
overloads. The rationale behind this is further described in this post. -
SegmentScope
was dropped in favor of a much smaller construct namedMemorySegment.Scope
which is a pure lifetime abstraction.Arena
is used directly in most cases. -
A default linker based on libffi was added greatly simplifying porting of Java to various platforms.
-
A new linker option was added to mark calls as "trivial" speeding up native calls. A "trivial" method must complete in a duration comparable to a no-op call and must not call back to Java.
- Note
-
The entire list of improvements can be viewed here
In this chapter, we will explore some of the basic features of the FFM API. The examples below will run only when this PR is merged into the Java mainline repo and only in JDK 21.
Here is an example of how to allocate 16 bytes of native memory that is automatically managed by the garbage collector (GC).
{
// ...
MemorySegment segment = Arena.ofAuto().allocate(16);
// ...
} // Segment eligible for collection by the GC here.
// Actual time of collection is unspecified.
Here is another example where memory is implicitly and deterministically released:
try (var arena = Arena.ofConfined()) {
var segment = arena.allocate(16);
} // Segment is deterministically freed here
Memory layouts can be used to describe the layout of a MemorySegment
. Here is how a Point with int
coordinates can be defined:
import static java.lang.foreign.MemoryLayout.PathElement.*;
import static java.lang.foreign.MemoryLayout.*;
import static java.lang.foreign.ValueLayout.*;
private static final MemoryLayout POINT = structLayout(
JAVA_INT.withName("x"),
JAVA_INT.withName("y")
).withName("point");
// Accessor for x
private static final VarHandle X = POINT.varHandle(groupElement("x"));
// Accessor for y
private static final VarHandle Y = POINT.varHandle(groupElement("y"));
Armed with these static variables, we can (somewhat manually) roll a memory-segment-backed point:
try (var arena = Arena.ofConfined()) {
MemorySegment point = arena.allocate(POINT);
X.set(point, 3);
Y.set(point, 4);
System.out.println(
Arrays.toString(point.toArray(JAVA_INT))
);
} // Point is deterministically freed here
When run, this program will produce the following output:
[3, 4]
It is often better to encapsulate the inner workings of constructs that are using memory layouts. Here is how a custom Point
class can be written using a backing native MemorySegment
:
static final class Point {
private static final MemoryLayout POINT = structLayout(
JAVA_INT.withName("x"),
JAVA_INT.withName("y")
).withName("point");
private static final VarHandle X = POINT.varHandle(groupElement("x"));
private static final VarHandle Y = POINT.varHandle(groupElement("y"));
private final MemorySegment segment;
public Point(Arena arena) {
this.segment = arena.allocate(POINT);
}
int x() {
return (int) X.get(segment);
}
int y() {
return (int) Y.get(segment);
}
void x(int x) {
X.set(segment, x);
}
void y(int y) {
Y.set(segment, y);
}
@Override
public String toString() {
return "(" + x() + ", " + y() + ")";
}
@Override
public boolean equals(Object o) {
return o instanceof Point that &&
this.x() == that.x() &&
this.x() == that.y();
}
@Override
public int hashCode() {
return Objects.hash(x(), y());
}
}
Note that the VarHandle
objects are declared static final
and that there are explicit (int)
casts for the getters x()
and y()
. Failure to observe these coding conventions will reduce performance substantially.
Also note that we are passing an Arena
to the constructor so that we can control the lifecycle of the MemorySegment
used. Here is how the Point
class can be used:
try (var arena = Arena.ofConfined()) {
var point = new Point(arena);
point.x(3);
point.y(4);
System.out.println(point);
} // Point is deterministically freed here
When run, this program will produce the following output:
(3, 4)
We could easily create a generic MemorySegment
wrapper that relieve us from the burden of writing a segment declaration, custom constructors, toString()
, hashCode()
and, equals()
methods in inheriting classes:
public abstract class AbstractSegmentWrapper {
private final MemorySegment segment;
protected AbstractSegmentWrapper(Arena arena) {
this.segment = arena.allocate(layout());
}
abstract protected StructLayout layout();
abstract protected List<VarHandle> varHandles();
protected MemorySegment segment() {
return segment;
}
@Override
public String toString() {
return getClass().getSimpleName() + layout().memberLayouts()
.stream()
.map(l -> l.name().orElse(l.toString()) + "=" + l.varHandle().get(segment))
.collect(Collectors.joining(", ", "{", "}"));
}
@Override
public boolean equals(Object o) {
if (o == null || !getClass().equals(o.getClass())) {
return false;
}
AbstractSegmentWrapper that = (AbstractSegmentWrapper) o;
return varHandles().stream()
.allMatch(vh -> Objects.equals(vh.get(this.segment), vh.get(that.segment)));
}
@Override
public int hashCode() {
return varHandles().stream()
.map(vh -> vh.get(segment))
.mapToInt(v -> v == null ? 0 : v.hashCode())
.reduce(1, (a, b) -> a * 31 + b);
}
}
Using the abstract class above, we could more easily create wrapper classes like Point
here:
public final class Point extends AbstractSegmentWrapper {
private static final StructLayout POINT = structLayout(
JAVA_INT.withName("x"),
JAVA_INT.withName("y")
).withName("point");
private static final VarHandle X = POINT.varHandle(groupElement("x"));
private static final VarHandle Y = POINT.varHandle(groupElement("y"));
private static final List<VarHandle> VAR_HANDLES = List.of(X, Y);
public Point(Arena arena) {
super(arena);
}
@Override
protected StructLayout layout() {
return POINT;
}
@Override
protected List<VarHandle> varHandles() {
return VAR_HANDLES;
}
int x() {
return (int) X.get(segment());
}
int y() {
return (int) Y.get(segment());
}
void x(int x) {
X.set(segment(), x);
}
void y(int y) {
Y.set(segment(), y);
}
}
As always, it is important to declare the VarHandle
instances final
.
With FFM, it is possible to call many native functions directly. Below is an example where we invoke the system library call 'strlen' directly from Java. This is made in two steps where, in step one, we obtain a MethodHandle
for the native method:
Linker linker = Linker.nativeLinker();
MethodHandle strlen = linker.downcallHandle(
linker.defaultLookup().find("strlen").get(),
FunctionDescriptor.of(JAVA_LONG, ADDRESS)
);
With the MethodHandle
strlen
, we can, in a second step, easily invoke the method directly from Java:
try (Arena arena = Arena.ofConfined()) {
MemorySegment str = arena.allocateUtf8String("Hello");
long len = (long) strlen.invoke(str);
System.out.println("The length is " + len);
}
When run, this program will produce the following output:
The length is 5
This is correct as "Hello" consists of five ASCII characters (not including the terminating '/0' character used by C/C++).
Here are some resources that could kickstart your Panama FFM voyage.
Run your own code on JDK 21 today by downloading a JDK 21 Early-Access Build. As mentioned above, you cannot run the new version of FFM until the PR is merged.
-
JEP 442 Foreign Function & Memory API (Third Preview)
-
Open-Source Panama FFM on GitHub: github.com/openjdk/panama-foreign
All code and the entire presentation can be cloned via https://github.com/minborg/articles
The following is intended to outline our general product direction. It is intended for information purposes only, and may not be incorporated into any contract. It is not a commitment to deliver any material, code, or functionality, and should not be relied upon in making purchasing decisions. The development, release, timing, and pricing of any features or functionality described for Oracle’s products may change and remains at the sole discretion of Oracle Corporation.