Generate Java Bindings with Jextract
Introduction
When developing Java applications there can be use cases when those require access to libraries and third-party memory written in other programming languages. Project Panama was designed to meet the demand for better developer support in accessing native libraries, particularly for those developed with C/C++. Interaction between the JVM and the "foreign" (non-Java) APIs has been made simpler with Foreign Function and Memory API (FFM API). The FFM API became a final feature in JDK 22 and adds support for foreign memory access, as well as for foreign function calls.
The jextract
tool parses header files (.h
) of native libraries, and generates Java code, named bindings, which internally use the Foreign Function and Memory API.
Thanks to the output of jextract
you can directly use a Java model of the native libraries you are interested in.
This tutorial will walk you through how to obtain and run the jextract
tool, but also on how to use the Java code that it generates.
Get jextract
Pre-built binaries for jextract
are periodically released here.
These binaries are built from the master branch of the jextract repository,
and target the foreign memory access and function API in the latest JDK.
Download the binary suitable to your operating system and you can start using it.
Alternatively, you can build jextract
from the latest sources by following the instructions inside its repository.
⚠️ If you are using macOS Catalina or later you may need to remove the quarantine attribute from the bits before you can use the jextract binaries. Run the following command to achieve this:
sudo xattr -r -d com.apple.quarantine path/to/jextract/folder/
Command Line Options
-D
or --define-macro <macro>=<value>
Defines <macro>
to <value>
(or 1 if <value>
omitted).
--header-class-name <name>
Designates the name of the generated header class. If this option is not specified, then header class name is derived from the header file name. For example, class "foo_h" for header "foo.h". If multiple headers are specified, then this option is mandatory.
-t, --target-package <package>
Defines the target package name for the generated classes. If this option is not specified, then unnamed package is used.
-I, --include-dir <dir>
Appends a directory to the include search paths. Include search paths are searched in order.
For example, if -I foo -I bar
is specified, header files will be searched in "foo" first, then (if nothing is found) in "bar".
-l, --library <name \| path>
Specifies a shared library that should be loaded by the generated header class. If :
, then what follows is interpreted as a library path.
Otherwise, <libspec>
denotes a library name. Examples:
-l GL
,-l :libGL.so.1
-l :/usr/lib/libGL.so.1
.
--use-system-load-library
Libraries specified using -l
are loaded in the loader symbol lookup (using either System::loadLibrary
, or System::load
).
This option is useful if the libraries must be loaded from one of the paths in java.library.path
.
--output <path>
Defines where to place generated files.
--dump-includes <file>
Dumps included symbols into specified file.
To filter on specific elements, jextract
can generate a dump of all the symbols encountered in a header file.
This dump can be manipulated, and then used as an argument file (with the @argfile
syntax) to generate bindings only for a subset of symbols seen by jextract
.
--include-[function,constant,struct,union,typedef,var]<String>
Includes a symbol of the given name and kind in the generated bindings. When one of these options is specified, any symbol that is not matched by any specified filters is omitted from the generated bindings.
--version
Prints tool version information, the JDK for which it was built, clang version and exits.
How to Run jextract
Let's take the following example: render a teapot using functions from freeglut (for Windows use freeglut MSVC package). freeglut
is an open-source alternative to the OpenGL Utility Toolkit (GLUT)
library.
⚠️ If you are using macOS Catalina or later you should install also mesa-glu, an open-source library that provides additional utility functions to complement the core OpenGL specification:
brew install mesa-glu
Typically, a native library has an include
directory which contains all the header files that define the interface of the library, with one main header file.
The freeglut
library located at /path/to/freeglut/
has a directory path/to/freeglut/version/include
where the header files are stored.
Let's open a terminal window in the root directory of the Java project you're working on, which has a src
source directory corresponding to the root package,
and run jextract
to transform the main freeglut
header into Java code:
# macOS and Linux compatible command
jextract --output src \
-l :/opt/homebrew/Cellar/freeglut/3.6.0/lib/libglut.3.dylib \
-I /opt/homebrew/Cellar/freeglut \
-I /opt/homebrew/Cellar/mesa \
-I /opt/homebrew/Cellar/mesa-glu \
-t org.freeglut \
/opt/homebrew/Cellar/freeglut/3.6.0/include/GL/freeglut.h
This command will transform the header file /opt/homebrew/Cellar/freeglut/3.6.0/include/GL/freeglut.h
into corresponding Java classes
by taking into account the following options:
--output src
to store the output insrc
root directory;-l :/opt/homebrew/Cellar/freeglut/3.6.0/lib/libglut.3.dylib
instructsjextract
that the generated bindings should load from the library path/opt/homebrew/Cellar/freeglut/3.6.0/lib/libglut.3.dylib
;-I /opt/homebrew/Cellar/freeglut
,-I /opt/homebrew/Cellar/mesa
,-I /opt/homebrew/Cellar/mesa-glu
specify the header file search directories. These locations are used to find header files included through#include
in the main header file;-t org.freeglut
specifies the target package to which the generated classes and interfaces will belong. (jextract
will automatically create the package structure under thesrc
directory specified through--output
);/opt/homebrew/Cellar/freeglut/3.6.0/include/GL/freeglut.h
is the main header file of the native library you want to generate bindings for.
The equivalent command on Windows is similar:
# Windows PowerShell command
jextract --output src `
-I "\path\to\freeglut\include" `
--use-system-load-library `
"-l" opengl32 `
"-l" glu32 `
"-l" freeglut `
"-t" "org.freeglut" `
"\path\to\freeglut\include\GL\glut.h"
Filtering
Some libraries are incredibly large (such as Windows.h
), and you might not be need all the jextract
generated code.
For such situations, you can use jextract's --include-XYZ
command line options to only generate classes only for the elements you specify.
To know which symbols you can filter, jextract
can generate a dump of all the symbols encountered in a header file:
# macOS and Linux compatible command
jextract --output src \
-l :/opt/homebrew/Cellar/freeglut/3.6.0/lib/libglut.3.dylib \
-I /opt/homebrew/Cellar/freeglut \
-I /opt/homebrew/Cellar/mesa \
-I /opt/homebrew/Cellar/mesa-glu \
-t org.freeglut \
--dump-includes glut.symbols \
/opt/homebrew/Cellar/freeglut/3.6.0/include/GL/freeglut.h
We obtain a glut.symbols
file containing almost 5000 lines like the following:
## Extracted from: /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/System/Library/Frameworks/OpenGL.framework/Headers/gl.h
--include-function glAccum # header: /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/System/Library/Frameworks/OpenGL.framework/Headers/gl.h
--include-function glActiveTexture # header: /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/System/Library/Frameworks/OpenGL.framework/Headers/gl.h
--include-function glAlphaFunc # header: /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/System/Library/Frameworks/OpenGL.framework/Headers/gl.h
--include-function glAreTexturesResident # header: /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/System/Library/Frameworks/OpenGL.framework/Headers/gl.h
--include-function glArrayElement # header: /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/System/Library/Frameworks/OpenGL.framework/Headers/gl.h
--include-function glAttachShader # header: /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/System/Library/Frameworks/OpenGL.framework/Headers/gl.h
...
You can edit it to contain only the symbols you need and then use it as an argument file (using the @argfile
syntax) to generate bindings only for a subset of symbols seen by jextract
:
# macOS and Linux compatible command
jextract --output src \
-l :/opt/homebrew/Cellar/freeglut/3.6.0/lib/libglut.3.dylib \
-I /opt/homebrew/Cellar/freeglut \
-I /opt/homebrew/Cellar/mesa \
-I /opt/homebrew/Cellar/mesa-glu \
-t org.freeglut @glut.symbols \
/opt/homebrew/Cellar/freeglut/3.6.0/include/GL/freeglut.h
⚠️ If you remove a declaration that is needed by another included structure,
jextract
will report the missing dependency and terminate without generating any bindings:$ jextract --include-var aVar test.h ERROR: aVar depends on A which has been excluded
Integrate Code Generated by jextract
Most of the methods that jextract
generates are static, and are designed to be imported using import static
.
To access the code that jextract
generates for the header file freeglut.h
, only the following two wildcard imports are needed:
import org.freeglut.*;
import static org.freeglut.freeglut_h.*;
The import static org.freeglut.freeglut_h.*;
statement will import all the static functions and fields from the class that jextract
generates for the main header file of the library.
This includes methods to access functions, global variables, macros, enums, primitive typedefs, and layouts for builtin C types.
The import org.freeglut.*;
statement imports all the other classes generated by jextract
, which can include:
- classes representing structs or unions,
- function types,
- and struct or union typedefs.
Now let's write the Teapot.java
code. To speed up things, will take some inspiration from the Teapot.c
native variant of the code:
#include <GL/glut.h>
void display(void)
{
//Clear color and depth buffers
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
glPushMatrix();
// Assign a color to the teapot
glColor3f(0.0, 1.0, 0.0);
// Rotation
glRotatef(10, 0.0, 0.0, 1.0);
glRotatef(10, 0.0, 1.0, 0.0);
//Draw
glutWireTeapot(1);
glPopMatrix();
//Must swap the buffer in double buffer mode
glutSwapBuffers();
}
void init(void)
{
glClearColor(0.0, 0.0, 0.0, 0.0);
//Model(Object coordinates), View (Camera coordinates), Projection (Screen coordinates)
glMatrixMode(GL_PROJECTION);
gluPerspective(40.0, 1.0, 1.0, 10.0);
glMatrixMode(GL_MODELVIEW);
gluLookAt(0.0, 0.0, 5.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.);
}
int main(int argc, char **argv)
{
glutInit(&argc, argv);
glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGB | GLUT_DEPTH);
glutInitWindowSize(500, 500);
glutCreateWindow("Hello Panama!");
init();
glutDisplayFunc(display);
glutMainLoop();
return 0;
}
We can keep the display
method in Java and change init
to a constructor:
import java.lang.foreign.Arena;
import java.lang.foreign.SegmentAllocator;
import org.freeglut.*;
import static org.freeglut.freeglut_h.*;
public class Teapot {
Teapot(SegmentAllocator allocator) {
// Reset Background
glClearColor(0f, 0f, 0f, 0f);
//Model(Object coordinates), View (Camera coordinates), Projection (Screen coordinates)
glMatrixMode(GL_PROJECTION());
gluPerspective(40.0, 1.0, 1.0, 10.0);
glMatrixMode(GL_MODELVIEW());
gluLookAt(0.0, 0.0, 5.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.);
}
void display() {
glClear(GL_COLOR_BUFFER_BIT() | GL_DEPTH_BUFFER_BIT());
glPushMatrix();
glColor3f(0.0f, 1.0f, 0.0f);
glRotatef(10, 0.0f, 0.0f, 1.0f);
glRotatef(10, 0.0f, 1.0f, 0.0f);
glutWireTeapot(1);
glPopMatrix();
glutSwapBuffers();
}
public static void main(String[] args) {
try (var arena = Arena.ofConfined()) {
var argc = arena.allocateFrom(C_INT, 0);
glutInit(argc, argc);
glutInitDisplayMode(GLUT_DOUBLE() | GLUT_RGB() | GLUT_DEPTH());
glutInitWindowSize(500, 500);
glutCreateWindow(arena.allocateFrom("Hello Panama!"));
var teapot = new Teapot(arena);
var displayStub = glutDisplayFunc$callback.allocate(teapot::display, arena);
glutDisplayFunc(displayStub);
glutMainLoop();
}
}
}
This example uses Arena.ofConfined()
, a confined Arena
because the application has a determined lifetime.
The scope of a confined arena is alive from when it is created to when it is closed. A confined arena has an owner thread and that is the thread that created it.
Just the owner thread can access the memory segments allocated in a confined arena.
If you try to close a confined arena with a thread other than the owner thread, you will get an exception.
The memory segment argc
is allocated with the arena by invoking Arena.allocateFrom(OfInt,int)
and is initialized on this line: var argc = arena.allocateFrom(C_INT, 0);
.
C_INT
is a constant generated by jextract
, that has the value ValueLayout.JAVA_INT
, and 0
is the value used to initialize the memory segment.
GLUT library initialization inside the main
method is kept as similar as possible to its native variant:
public static void main(String[] args) {
try (var arena = Arena.ofConfined()) {
var argc = arena.allocateFrom(C_INT, 0);
glutInit(argc, argc);
glutInitDisplayMode(GLUT_DOUBLE() | GLUT_RGB() | GLUT_DEPTH());
glutInitWindowSize(500, 500);
glutCreateWindow(arena.allocateFrom("Hello Panama!"));
var teapot = new Teapot(arena);
var displayStub = glutDisplayFunc$callback.allocate(teapot::display, arena);
glutDisplayFunc(displayStub);
glutMainLoop();
}
}
As the generated code for glutCreateWindow
requires a MemorySegment
, you allocate it with the previously initialized arena and store the window title in the off-heap memory associated with the memory segment:
glutCreateWindow(arena.allocateFrom("Hello Panama!"));
Finally, you can create a teapot by calling the Teapot.java
constructor with the arena and then invoke display callback for the current window.
var teapot = new Teapot(arena);
var displayStub = glutDisplayFunc$callback.allocate(teapot::display, arena);
glutDisplayFunc(displayStub);
glutMainLoop();
To run the Teapot.java
example you should first compile the freeglut
generated code:
# macOS and Linux compatible command
javac -d . src/org/freeglut/*.java
# For windows there are too many sources for command line. Put them into separate file
ls -r src/*.java | %{ $_.FullName } | Out-File sources.txt
javac -d classes '@sources.txt'
Next, let's launch the Teapot.java
program:
# macOS and Linux compatible command
java -XstartOnFirstThread --enable-native-access=ALL-UNNAMED src/Teapot.java
# Windows PowerShell command
java -cp classes `
--enable-native-access=ALL-UNNAMED `
-D"java.library.path=C:\Windows\System32`;\path\to\freeglut\bin\x64" `
src\Teapot.java
You should see a green teapot:
Tracing
When debugging an application is useful to inspect the parameters passed to a native call.
Code generated by jextract
supports tracing of native calls, meaning parameters passed to native calls can be printed on the standard output.
To enable the tracing support, just pass the -Djextract.trace.downcalls=true
flag as a VM argument when launching your application:
# macOS and Linux compatible command
java -XstartOnFirstThread -Djextract.trace.downcalls=true --enable-native-access=ALL-UNNAMED src/Teapot.java
# Windows PowerShell command
java -cp classes --enable-native-access=ALL-UNNAMED `
-D"jextract.trace.downcalls=true" `
-D"java.library.path=C:\Windows\System32`;\path\to\freeglut\bin\x64" `
src\Teapot.java
Bellow you can observe an excerpt of the previous command's output:
glutInit(MemorySegment{ address: 0x600001d9c080, byteSize: 4 }, MemorySegment{ address: 0x600001d9c080, byteSize: 4 })
glutInitDisplayMode(18)
glutInitWindowSize(500, 500)
glutCreateWindow(MemorySegment{ address: 0x600001d99070, byteSize: 14 })
glClearColor(0.0, 0.0, 0.0, 0.0)
glShadeModel(7425)
glLightfv(16384, 4611, MemorySegment{ address: 0x600001da4710, byteSize: 16 })
glLightfv(16384, 4608, MemorySegment{ address: 0x600001da4830, byteSize: 16 })
glLightfv(16384, 4609, MemorySegment{ address: 0x600001da4830, byteSize: 16 })
glLightfv(16384, 4610, MemorySegment{ address: 0x600001da4830, byteSize: 16 })
glMaterialfv(1028, 5633, MemorySegment{ address: 0x13789af10, byteSize: 452 })
glEnable(2896)
glEnable(16384)
glEnable(2929)
glutDisplayFunc(MemorySegment{ address: 0x11456c0c0, byteSize: 0 })
glutIdleFunc(MemorySegment{ address: 0x1145b6ac0, byteSize: 0 })
glutMainLoop()
Programming Language Support
Foreign Function and Memory API and jextract
support C header files, yet other languages have C interoperability.
You can still use jextract
to integrate with libraries written in those language through an intermediate C layer.
Checkout the table below to understand which other languages can work with jextract
and how to do that:
Language | Method of access |
---|---|
C++ | C++ allows declaring C methods using extern "C" syntax, and many C++ libraries have a C interface alongside. Jextract can consume such a C interface, which can then be used to access the library in question. |
Rust | The Rust ecosystem has a tool called cbindgen which can be used to generate a C interface for a Rust library. Such generated C interface can then be consumed by jextract, and be used to access the library in question. |
Useful Links
- Jextract repository: https://github.com/openjdk/jextract
- Jextract guide: https://github.com/openjdk/jextract/blob/master/doc/GUIDE.md
- Jextract binaries: https://jdk.java.net/jextract/
- State of jextract: https://cr.openjdk.org/~mcimadamore/panama/jextract_changes.html
- State of foreign memory support: https://github.com/openjdk/panama-foreign/blob/f75fbac0dd2f8a7861cd349872a27b86ddb53f35/doc/panama_memaccess.md
- State of foreign function support: https://github.com/openjdk/panama-foreign/blob/f75fbac0dd2f8a7861cd349872a27b86ddb53f35/doc/panama_ffi.md
Last update: December 17, 2024