FFM is excellent at calling native code and moving data efficiently between Java and off-heap memory. In many real systems, the next bottleneck is what you do with the data once you have it:
- converting raw buffers into domain-friendly Java types
- scanning/aggregating large arrays (register blocks, sensor samples, signal vectors)
- doing small math kernels repeatedly
This is where the Vector API (jdk.incubator.vector) can complement FFM: it lets you express SIMD-friendly bulk
operations in Java so the JVM can use vector instructions (when available).
Status: the Vector API is an incubator module in JDK 25. It requires
--add-modules jdk.incubator.vectorat compile/run time.
mymodbus reads Modbus registers/coils via a native backend (libmodbus) and returns:
int[]for registers (unsigned 16-bit values promoted to int)boolean[]for coils
The native call itself is only part of the cost. The post-processing step—copying and converting values—is often a hot path for large reads (e.g., 125 registers repeatedly).
Today, this repo uses simple scalar loops for conversions (clear + correct). The Vector API is a tool you can reach for after profiling if conversion becomes dominant.
A common pattern in native interop is “read uint16_t[] (or short[]) and expose int[] in Java”.
Scalar baseline:
static int[] toUnsignedIntScalar(short[] in) {
int[] out = new int[in.length];
for (int i = 0; i < in.length; i++) {
out[i] = Short.toUnsignedInt(in[i]);
}
return out;
}Vector API version (JDK 25), using ZERO_EXTEND_S2I and widening in two “parts”:
import jdk.incubator.vector.*;
static final VectorSpecies<Short> SS = ShortVector.SPECIES_PREFERRED;
static final VectorSpecies<Integer> IS = IntVector.SPECIES_PREFERRED;
static int[] toUnsignedIntVector(short[] in) {
int[] out = new int[in.length];
int shortLanes = SS.length();
int intLanes = IS.length();
int i = 0;
int upper = SS.loopBound(in.length);
for (; i < upper; i += shortLanes) {
ShortVector sv = ShortVector.fromArray(SS, in, i);
IntVector v0 = (IntVector) sv.convertShape(VectorOperators.ZERO_EXTEND_S2I, IS, 0);
IntVector v1 = (IntVector) sv.convertShape(VectorOperators.ZERO_EXTEND_S2I, IS, 1);
v0.intoArray(out, i);
v1.intoArray(out, i + intLanes);
}
for (; i < in.length; i++) out[i] = Short.toUnsignedInt(in[i]);
return out;
}To compile/run code like this:
javac --add-modules jdk.incubator.vector YourFile.java
java --add-modules jdk.incubator.vector YourMainFFM helps you:
- avoid JNI glue code and keep adapters in Java
- allocate/copy deterministically with arenas
- keep native lifetimes and errors isolated behind clean APIs
The Vector API helps you:
- make the “copy/convert/process” step faster for large blocks
- keep computation in Java (no need to bounce back to C for every small kernel)
- express bulk transforms while staying within the JVM tooling ecosystem (JFR, profilers, tests)
In practice, you often combine them like:
- FFM/jextract to pull data from native into a flat buffer
- Copy into Java arrays (safety boundary)
- Vector API to transform/aggregate large blocks efficiently
- Profile first. SIMD adds complexity; only worth it if it’s a proven hot path.
- Keep fallbacks. A scalar loop is the reference implementation and is easier to reason about.
- Be explicit about flags. Vector API requires
--add-modules jdk.incubator.vectorfor compilation and runtime.