jextract
February 2024
Maurizio Cimadamore
With the finalization of the Foreign Function & Memory API near us, we have spent the last few weeks polishing jextract
. We have made several changes, from implementation-only ones (e.g. we now use string templates for all the code generation), to more visible ones, which will affect clients of the code jextract
generates. This document is an attempt at summarising the latter kind of changes, so as to provide some basic guidance of the (possible source-breaking) changes you might expect the next time you run jextract
.
But, before we dive into the details of what has changed in the jextract
tool, it would be useful to remind ourselves the design principles that are behind jextract
:
Minimize overhead. Using the code generated by jextract
should be as fast as using FFM directly.
Don't throw it away. If some piece of information is known to jextract
it should be reified in the generated code, where possible.
Don't be cheeky. Avoid fancy translation strategies which look appealing at first, but might introduce a non-obvious semantics mismatch between C and Java.
Don't be biased. jextract
should help developers writing the high-level API they want, not second guess it - for example, via an endless list of command line options.
Allow for a plan B. If jextract
cannot precisely reflect the semantics of the underlying API, then it should allow developers to take what jextract
has and customize it to fit their needs.
Keep it local. The generated code corresponding to a C program element should be as localized as possible, to facilitate copy and paste into other projects.
Keep it human. While jextract
is a mechanical tool, the code it generates (and the code that relies on it) will eventually be checked into repositories, and read by humans. As such, jextract
should refrain, where possible, from obfuscating the generated code too much.
As we shall see, many of the changes described in this document all contribute, each in slightly different ways, to moving jextract
harmoniously towards the goals listed above. If you prefer learning what's changed by looking at some code instead, have a look at the (recently re-generated) libclang bindings used by jextract
itself. Or, if you just want to get a jextract
binary build with the new changes and play with it, please find it here.
State of jextract
Change #1: Source code is the only currencyChange #2: No more constant classesChange #3: No more RuntimeHelper
Change #4: Simpler layout declarationsAnd simpler function descriptor declarationsChange #5: Simpler accessors for global variables and struct fieldsRemoval of strided accessorsAccessors for composite typesArray element accessorsChange #6: Improved function pointer interfacesChange #7: More efficient translation of variadic functionsChange #8: Better library loadingChange #9: More constant accessorsOther miscellaneous changes
Perhaps the most obvious difference is that the --source
flag is gone. In fact, now jextract
always assumes that source code generation is required. After few years of providing both options, it seemed to us that the most idiomatic way to use jextract
is to generate bindings for the target platform, using the --source
option, and then check in the generated code in some repository. (In fact, this is the approach we use for jextract
itself!).
At the same time, the classfile mode was not really adding much: while we started with two separate backends for source vs. classfile generation, we have long moved to a shared backend, so that now the classfile mode is nothing but a more optimized form of source code generation with in-memory compilation. But even this simpler form is not worth the added complexity: after all, compiled classes are just a javac
call away.
Anybody who used jextract
at least once would know that the tool generates a fairly large amount of classes. Some of these classes are constant classes and are used as a sort of source-level constant pool. That is, these are the classes where the various method handles, var handles, layouts and function descriptors are defined. This means that all other classes that are part of the extracted API, will refer back to constants defined in these classes.
This approach was introduced so as to make startup as good as possible: we didn't want a single native function call to pay the cost for the initialization of all the method handles in a library (as such library can be quite big, as is the case for windows.h
). Unfortunately, this also means that the generated code becomes less scrutable. Consider the following struct declaration in C:
struct Point {
int x;
int y;
};
If we extract the above declaration with jextract
, we obtain something like this:
xpublic class Point {
public static MemoryLayout $LAYOUT() {
return constants$0.const$0;
}
...
}
When looking at this code, it's easy to be confused: we know that Point
is a struct, and that it has a layout. But sadly, this layout is not defined in this class, but in one of the constant classes (namely constants$0
). And, to make things worse, even simple structs can easily contain dozens of such opaque constants.
This makes the generated code not only hard to read, but also hard to reuse. Consider the case where a developer wants to first generate a struct using jextract, but then copy and paste it in a different code base. Now we have a problem: since the source of the Point
class is not self-contained, the developer will also have to follow the dependency chain, and pull in the relevant bits from the constant classes.
This is unfortunate: as mentioned above, generated bindings should be self-evident, self-contained, and easy to copy and paste across code bases. For this reason, we have switched to a different code generation strategy that is more localized. With the latest changes, Point
now looks as follows:
public class Point {
private static final GroupLayout $LAYOUT = MemoryLayout.structLayout(
foo_h.C_INT.withName("x"),
foo_h.C_INT.withName("y")
).withName("Point");
...
}
The struct layout is now defined in place, thus making the code more readable and self-contained. Of course this is a trade-off: now the layout initialization logic will run as soon as Point
is initialized. But typically, since each struct is defined in its own class, there is never a large amount of constants to initialize. In cases where this strategy could lead to issues (e.g. initialization cycles), we resorted to the old good class holder idiom instead.
This means that the code generated by jextract doesn't contain any additional (toplevel) constant classes. Or, in other words, the classes generated by jextract will correspond to API elements in the corresponding C header file. After some extensive testing, we found that this new generation strategy strikes a much better balance between performance and readability of the generated classes.
RuntimeHelper
There is another class that jextract always generates, namely RuntimeHelper
. This class contains several helper functions that are used throughout the generated code. Upon further examination of the methods in this class, it quickly became apparent that most of the functionality in RuntimeHelper
has been added at a time where the FFM API was not expressive as it is today. This means that we can safely drop RuntimeHelper
without significant loss of functionality, as most of the methods declared there now have a corresponding method in the FFM API. One notable exception is the translation of variadic native functions, which we'll cover in a later section.
Consider the following C struct declaration:
struct Line {
struct Point begin;
struct Point end;
};
For this struct, jextract
generates the following code:
public class Line {
public static MemoryLayout $LAYOUT() {
return constants$0.const$3;
}
...
}
Now, if we follow the reference into the constant class, we find this:
class constant$0 {
static final StructLayout const$3 = MemoryLayout.structLayout(
MemoryLayout.structLayout(
JAVA_INT.withName("x"),
JAVA_INT.withName("y")
).withName("begin"),
MemoryLayout.structLayout(
JAVA_INT.withName("x"),
JAVA_INT.withName("y")
).withName("end")
).withName("Line");
}
Not only is the layout constant buried away into a constant class: the layout definition contains the definition for the nested Point
layout twice! It is easy to see how this fail to scale when a struct contains many nested struct fields.
The correct solution is for layouts to depend on each other by name, like so:
public class Line {
private static final GroupLayout $LAYOUT = MemoryLayout.structLayout(
Point.layout().withName("begin"),
Point.layout().withName("end")
).withName("Line");
...
}
The resulting code is much more readable, contains no repetitions and it more closely matches the corresponding C declaration. One obvious benefit of this strategy is that, should any change be made to the Point
layout (e.g. so that ByteOrder.BIG_ENDIAN
is used for both fields), such changes are immediately reflected into any other dependent layout (or function descriptor, see below).
Note that, while It is possible, in principle, for initialization cycles to arise when layouts depends on other layouts in this way, in practice that's not a problem as the C language does not allow mutually dependent struct declarations. To have a cycle in one or more struct declarations in C there has to be at least one indirection (pointer).
The same principle is also applied to the construction of function descriptors - consider the following C function:
double distance(Point one, Point two);
The function descriptor for this function goes from this:
static final FunctionDescriptor const$4 = FunctionDescriptor.of(JAVA_DOUBLE,
MemoryLayout.structLayout(
JAVA_INT.withName("x"),
JAVA_INT.withName("y")
).withName("Point"),
MemoryLayout.structLayout(
JAVA_INT.withName("x"),
JAVA_INT.withName("y")
).withName("Point")
);
To just this:
static final FunctionDescriptor DESC = FunctionDescriptor.of(
foo_h.C_DOUBLE,
Point.layout(),
Point.layout()
);
Consider the following C global variable declaration:
int value;
To allow access to this global variable from Java code, jextract generates two accessors, namely value$get
and value$set
. A similar pair of accessors is also generated for each struct fields.
The weird accessor name is mostly a relic of the past - a previous incarnation of jextract
used to expose three accessors for each variable/field (getter, setter, and reffer, roughly corresponding to the &
C operator). Today, we no longer need three accessors, and a simple pair of getter/setter is enough. For this reason, we have decided to simplify jextract
to generate accessor names that are consistent with the naming convention followed by Java records. That is, for the above global variable declaration, the following accessors are generated:
/**
* Getter for variable:
* {@snippet lang=c :
* int value
* }
*/
public static int value() {
return value$SEGMENT().get(value$LAYOUT(), 0L);
}
/**
* Setter for variable:
* {@snippet lang=c :
* int value
* }
*/
public static void value(int varValue) {
value$SEGMENT().set(value$LAYOUT(), 0L, varValue);
}
In the case of struct fields, jextract
used to generate a further pair of accessor methods, called strided accessors. For instance, given the usual C struct declaration Point
(see above) jextract
would emit the following extra pair of getter/setter for Point::x
:
public static int y$get(MemorySegment seg, long index) {
return (int)constants$0.const$2.get(seg, index * sizeof());
}
public static void y$set(MemorySegment seg, long index, int x) {
constants$0.const$2.set(seg, index * sizeof(), x);
}
These strided accessors were especially useful when striding over the contents of a struct array:
MemorySegment points = Arena.ofAuto().allocate(POINT, 10);
for (int i = 0 ; i < 10 ; i++) {
System.out.println(Point.x$get(points, i));
System.out.println(Point.y$get(points, i));
}
Unfortunately, strided accessors can introduce ambiguities when using simpler struct names as the one showed above - more specifically, an strided getter for a field of type long
might clash with a plain setter for the same field (as they would both have signature (MemorySegment, long)
).
To avoid ambiguities, we have decided to drop strided accessors, and to introduce a slicing helper method instead (Point::asSlice
). The above code can then be rewritten like so:
MemorySegment points = Arena.ofAuto().allocate(POINT, 10);
for (int i = 0 ; i < 10 ; i++) {
MemorySegment point = Point.asSlice(points, i);
System.out.println(Point.x(point));
System.out.println(Point.y(point));
}
Consider the following global variable declaration:
struct Point { int x; int y; } aPoint;
For a variable of this type, jextract
used to generate a single segment accessor:
public static MemorySegment aPoint$SEGMENT() {
return RuntimeHelper.requireNonNull(constants$0.const$3,"aPoint");
}
We have updated jextract
to use a more consistent translation scheme that supports composite setters, as well as getters. In other words, for the above global variable declaration, jextract
would now generate the following code (similar considerations hold for struct fields whose type is a composite type):
/**
* Getter for variable:
* {@snippet lang=c :
* struct Point aPoint
* }
*/
public static MemorySegment aPoint() {
return aPoint$SEGMENT();
}
/**
* Setter for variable:
* {@snippet lang=c :
* struct Point aPoint
* }
*/
public static void aPoint(MemorySegment varValue) {
MemorySegment.copy(varValue, 0L, aPoint$SEGMENT(), 0L, aPoint$LAYOUT().byteSize());
}
Finally, let's consider a global variable declaration whose type is an array type:
int ints[2][3][4];
In this case, jextract
did not generate anything special, other than the segment accessor shown above (as the variable type is a composite type). This makes it difficult for clients to access individual array elements, as they would have to either derive array access var handles manually. We have now updated jextract
to generate the following array accessors (again, a similar consideration holds for struct fields whose type is an array type):
/**
* Indexed getter for variable:
* {@snippet lang=c :
* int ints[2][3][4]
* }
*/
public static int ints(long index0, long index1, long index2) {
return (int)ints$ELEM_HANDLE().get(ints$SEGMENT(), 0L, index0, index1, index2);
}
/**
* Indexed setter for variable:
* {@snippet lang=c :
* int ints[2][3][4]
* }
*/
public static void ints(long index0, long index1, long index2, int varValue) {
ints$ELEM_HANDLE().set(ints$SEGMENT(), 0L, index0, index1, index2, varValue);
}
These new accessors allow clients to access individual elements in the underlying array, simply by providing the required access coordinates (of type long
). Note that jextract
will also provide access to the size of the array dimensions, via special constant accessors (see below).
Translation of functional pointer relies heavily on functional interfaces; this allows jextract
to expose handy factories that create an upcall stub segment modelling a function pointer simply from a lambda expression. Consider the following C code:
typedef int compare(int *a, int *b);
This function pointer is translated using the following functional interface:
/**
* {@snippet :
* int (*compare)(int* a,int* b);
* }
*/
public interface compare {
int apply(java.lang.foreign.MemorySegment a, java.lang.foreign.MemorySegment b);
static MemorySegment allocate(compare fi, Arena scope) { ... }
static compare ofAddress(MemorySegment addr, Arena arena) { ... }
}
Function pointers are created easily, like so:
MemorySegment compar = compare.allocate((a, b) -> Integer.compare(a.get(JAVA_INT, 0), b.get(JAVA_INT, 0)));
Interestingly, the generated code also supports the reverse conversion - that is, going from an upcall stub segment to a functional interface which can be called from Java. This is useful if a native function returns a function pointer:
MemorySegment upcall = ... // some native call
compare compar = compare.ofAddress(upcall, Arena.ofAuto());
compar.apply(...);
Unfortunately, doubling down on functional interfaces both for creating new upcall stubs and for wrapping existing ones seems confusing. Also, in order to call a function pointer obtained from native code, we need to wrap the segment into a new instance (and bind it to an existing arena), which is a relatively expensive operation. This also means that jextract
now has to generate special functional interface accessors for global variables and struct fields whose type is a function pointer, in case clients prefer to interact with the functional interface directly.
For these reasons, we have simplified the translation strategy for function pointer, and made it more static:
/**
* {@snippet lang=c :
* typedef int (compare)(int *, int *)
* }
*/
public class compare {
/**
* The function pointer signature, expressed as a functional interface
*/
public interface Function {
int apply(MemorySegment a, MemorySegment b);
}
/**
* Allocates a new upcall stub, whose implementation is defined by {@code fi}.
* The lifetime of the returned segment is managed by {@code arena}
*/
public static MemorySegment allocate(compare.Function fi, Arena arena) { ... }
/**
* Invoke the upcall stub {@code funcPtr}, with given parameters
*/
public static int invoke(MemorySegment funcPtr, MemorySegment a, MemorySegment b) { ... }
}
Note that this new scheme retains the strength of the previous translation, and still allows us to create upcall stub segments easily, using lambda expressions. But, instead of providing an ofAddress
method which returns a new wrapper, the compare
interface now exposes a static method, namely compare::invoke
, which can be used to invoke the provided function pointer (modelled as a memory segment, of course). This leads to simpler client code that doesn't require any wrapping:
MemorySegment upcall = ... // some native call
compare.invoke(upcall, ...);
Thanks to this simplification, jextract
no longer needs to generate ad-hoc functional interface accessors. Note that clients can still create functional interfaces that are backed by an upcall stub segment, like so:
MemorySegment upcall = ... // some native call
compare.Function compar = (a, b) -> compare.invoke(upcall, a, b);
A variadic function is a function that can be called with an unspecified number of parameters. For instance:
int printf(const char *format, ...);
Here, the format
specifier can be followed by zero or more arguments (usually, one for each hole in the format specifier). Modelling variadic function in Java is challenging, because we need a different calling sequence depending on the number and types of arguments passed to the variadic function. This means that, for each variadic call, we might need to create a new specialized downcall method handle.
jextract
models variadic function as simple Java varargs method:
public static int printf(MemorySegment format, Object... x1) { ... }
While the signature of this method is intuitive, as it closely matches its C counterpart, its implementation is, unfortunately, rather convoluted. As explained above, each new call to this method leads to the creation of a new specialized downcall method handle. More specifically, jextract
has to:
unpack the argument Object[]
array
infer a layout for each of the variadic argument in the array
create a new specialized downcall method handle, by appending the inferred variadic layouts to the non-variadic function descriptor
invoke the specialized downcall with the provided arguments
It is easy to see how this logic is very convoluted. Even worse, this process has to be repeated for every call to the varargs method - even if a client is only interested in calling the underlying function with the same variadic argument types. It is also worth mentioning that this process is inherently lossy: the process for inferring layouts from actual arguments (see step (3) above) contains some unavoidable heuristics. For instance, what if one argument has type MemorySegment
? In this case there's two possible layout choices: an address layout (e.g. the argument is a pointer) or a struct/union layout (e.g. the argument is a struct/union). But we have no good way to disambiguate based of the runtime type of the provided argument. And, even if we did, how would we recover the struct layout associated with the memory segment being processed? Hence, jextract
assumes that all memory segments passed as variadic arguments to a variadic function are pointers (e.g. are associated with an address layout).
Ultimately, C variadic functions behave quite differently from their counterparts in Java-land. A C variadic function is closer, in spirit, to a function template, which can be specialized at the call-site, depending on the arguments being passed (and this analogy remains true if we look at how ABIs translate variadic calls into machine code). A Java varargs method, on the other hand, is just a method that allows to pass multiple arguments concisely, hiding the underlying array creation using compile-time desugaring. Indeed, this semantics mismatch is to be blamed for the expressiveness and performance issues discussed above.
To improve this situation, jextract
will now translate the above variadic function as follows:
/**
* Variadic invoker class for:
* {@snippet lang=c :
* int printf(const char *format, ...)
* }
*/
public static class printf {
// some details omitted
/**
* Variadic invoker factory for:
* {@snippet lang=c :
* int printf(const char *format, ...)
* }
*/
public static printf makeInvoker(MemoryLayout... layouts) { ... }
/**
* {@return the specialized method handle}
*/
public MethodHandle handle() { ... }
/**
* {@return the specialized descriptor}
*/
public FunctionDescriptor descriptor() { ... }
public int apply(MemorySegment format, Object... x1) { ... }
}
The generated code is now less similar to its C counterpart. For each variadic function, an invoker class is created (not a method!). The invoker class feature a method (printf::apply
) whose parameter types are the non-variadic parameters, followed by an Object...
for the variadic parameters. This method can be used to perform the variadic call. To create an instance of the invoker class, the printf::makeInvoker
factory method must be used. This method takes a list of variadic memory layouts, and return an instance of the invoker class, whose underlying downcall method handle is specialized for the provided layouts:
var formatString = Arena.ofAuto().allocateFrom("Hello %d %d");
var printf = printf.makeInvoker(C_INT, C_INT);
printf.apply(formatString, 1, 2);
Alternatively, clients can avoid boxing variadic arguments into a new array, by calling the invoker's downcall method handle directly, like so:
printf.handle().invokeExact(formatString, 1, 2);
The jextract
tool supports loading a set of shared libraries that are specified, on the command line, using the -l
option. Consider, for example, the following command line, which is used to extract the OpenGL library on Linux/x64:
jextract -t opengl --output gensrc \
-l GL \
-l GLU \
-l glut \
/usr/include/GL/glut.h
Here, three libraries are specified to jextract
via the -l
option, namely GL
, GLU
and glut
. This will cause jextract
to insert the following preamble in the generated header class:
static {
System.loadLibrary("GL");
System.loadLibrary("GLU");
System.loadLibrary("glut");
}
This static initializer ensures that the libraries are loaded before any function or global variable in the header class is accessed (which would cause a lookup in the loader lookup).
While this works, relying on System::loadLibrary
can lead to issues. First, System::loadLibrary
suffers from poor integration with the dynamic linker used by the underlying OS. This means that the above loading requests will likely fail, unless the correct folder is configured, either using LD_LIBRARY_PATH
or -Djava.library.path
. Secondly, System::loadLibrary
associates a library with the current class loader, and prevents the same library from ever be associated with any other classloader.
The FFM API provides a more general library loading mechanism - SymbolLookup::libraryLookup
- which is a very thin wrapper around the basic library loading capabilities provided by the OS (e.g. dlopen
). This allows clients to deterministically load and unload shared libraries, even across multiple classloaders - while at the same time enjoying better integration with the OS dynamic linker.
For this reason, we have changed the behavior of the -l
option to use a library lookup instead. That is, given the above command line, jextract
will now set up a symbol lookup that looks like this:
static final Arena LIBRARY_ARENA = Arena.ofAuto();
static final SymbolLookup SYMBOL_LOOKUP = SymbolLookup.libraryLookup(System.mapLibraryName("GL"), LIBRARY_ARENA)
.or(SymbolLookup.libraryLookup(System.mapLibraryName("GLU"), LIBRARY_ARENA))
.or(SymbolLookup.libraryLookup(System.mapLibraryName("glut"), LIBRARY_ARENA))
.or(SymbolLookup.loaderLookup())
.or(Linker.nativeLinker().defaultLookup());
This (composite) lookup will search symbols in:
the GL
library;
the GLU
library;
the glut
library;
the loader lookup (e.g. any library loaded with System::load
/System::loadLibrary
);
the native linker's default lookup.
This lookup behavior ensures maximum flexibility as well as good integration with the underlying OS, and is a much better default for jextract
. There might be, however, cases where libraries end up in -Djava.library.path
. To support such cases, jextract
provides the option --use-system-load-library
which reverts the lookup logic to the previous, classloader-based behavior.
The bindings generated by jextract
rely on several constants: memory layouts, function descriptors, var handles and method handles. Since these constants can be useful to customize the behavior of the generated bindings, jextract
now exposes them in full. For instance, consider the following function declaration in C:
int func(double d);
The following constant accessors are generated by jextract
:
private static class func {
public static final FunctionDescriptor DESC = FunctionDescriptor.of(
foo_h.C_INT,
foo_h.C_DOUBLE
);
public static final MethodHandle HANDLE = Linker.nativeLinker().downcallHandle(
foo_h.findOrThrow("func"),
DESC);
}
/**
* Function descriptor for:
* {@snippet lang=c :
* int func(double d)
* }
*/
public static FunctionDescriptor func$descriptor() {
return func.DESC;
}
/**
* Downcall method handle for:
* {@snippet lang=c :
* int func(double d)
* }
*/
public static MethodHandle func$handle() {
return func.HANDLE;
}
This might be useful, for instance, for clients that want to adapt the generated method handle. A similar strategy is also used to expose:
layouts and offsets of struct fields;
layouts and segments of global variables;
array dimensions of struct fields/global variables.
In other words, jextract
now provides access to all the constants that are used inside the bindings it generates.
A number of other minor fixes and enhancements were carried out. We will just list them briefly here:
improve parameter names for generated methods;
add compact javadoc to all generated methods;
align helper method names with the names used in the FFM API (e.g. reinterpret
/asSlice
);
add support for tracing downcalls;
add support for macos/arm64 binary builds (these will show up here soon after Java 22 is released).