All documentations are contained in this single README file.
This page is very long.
On Github, you may click to open the Github Table of Contents, and redirect to the section you are interested in.
It's recommended to go here to read this page.
[[TOC]]
A way of using Project Panama
that is completely different from using jextract
.
The jextract
parses C code and generates Java code, while Panama Native Interface
reads Java byte code and generates C code as well as user-friendly and native-friendly Java code.
This approach is similar to how JNI(Java Native Interface)
works, so this project is named as PNI(Panama Native Interface)
.
Panama Native Interface
also allows you to directly invoke functions based on symbols without generating any wrapper function, which is one of the purposes of Project Panama
.
Click to reveal/hide
- vproxy: LoadBalancer and virtual networking on Java, migrated from the old
JNI
toPNI
, using theJNI-style
C functions. - luajn: A Lua/C/Java binding, built upon
PNI
, using theCritical
style C functions. - msquic-java: MsQuic for Java, built upon
PNI
, heavily usesStruct(skip=true)
. - vpxdp: A bunch of easy-to-use AF_XDP and eBPF related apis. It provides a Java binding, which doesn't use any wrapper function.
Click to reveal/hide
The jextract
tool can automatically generate Java types from C headers. This seems great at first, you can create structs and call functions in Java just like writing C code.
However, when you apply it to your project, you will find that the generated code is so long and so complicated, and contains so much symbols you would never use. Well, this is just the beginning of a nightmare.
Things start to go messy when a C library provides its APIs through macro
s or static inline
functions. These information will not preserve after compiling, and you will not be able to call them because they just don't exist.[1]
Using macro
s or static inline
functions to define user friendly APIs, is very common in the C world.[2]
It would be much messier when you are trying to adapt to a cross platform library, or a bunch of different versions of libraries of the same origin.
In this situation, you will have to ask jextract
to generate code for each version, and you will have to write adaptors for all versions because these generated types are different.[3]
After all, C libraries are written for C users. The users are happy as long as the code compiles with a C compiler.[4]
Yes, the real world is much more complex than a simple poc.
So why not take the opposite direction. Let's put the dirty work in the C world and let the C compiler take care of things for us, and let's provide a group of clean and nice APIs for Java.
Since the APIs are specifically made for Java, why not just put the definitions on the Java side.
Oh, wait, doesn't that sound familiar? This is exactly the Java Native Interface
approach.
Let's take one more step further. We define not only methods(functions) in Java, but also struct
s and union
s, and we use Project Panama
as the base.
The Panama Native Interface
helps you deal with all the dirty work mentioned above.
You can not only define types/functions in Java, but also bring pre-defined type/function into Java.
[1]: For example, a very common variable:
errno
is defined using macro, actually you are calling a function. The macro is platform specific, buterrno
is cross platform.
[2]: For example, in Lua, a lot ofdocumented
APIs are defined usingmacro
s.
[3]: For example, themsquic
supports multiple platforms, each platform has some specifictypedef
s.
[4]: For example, the Lua 5.1-5.4 exposes their APIs in different ways, however if you are writing C you are likely to compile properly across all platforms (only a few deprecated functions need to be modified).
BTW,jextract
cannot handle some commonly used syntax for now (2023-09-24), e.g.align or packed
attributes,bit fields
, ...
Click to reveal/hide
You need at least JDK 21
to build the project.
If you are using JDK 21
, the building system will add --enable-preview
compiler option, and include the 21
source root.
Otherwise there will be no --enable-preview
and will include the 22
source root.
- Configure
JAVA_HOME
to your JDK >= 21. - Configure
PATH
to make surejavac
points to JDK >= 21
If you are using
Windows
, it's recommended to useMinGW UCRT64
to work with this project.
- Configure
MINGW_BASH
to the path tobash.exe
in yourMinGW
directory, usually it'sC:\msys64\usr\bin\bash.exe
- If you need to build Graal native-image on Windows, you will need to install Visual Studio 2022, and at least install the following components:
- MSVC v143 - VS 2022 C++ x64/x86 生成工具(最新)
- Windows 通用 CRT SDK
- Windows 通用 C 运行时
- Windows 10 SDK (10.0.20348.0)
After configuring the environment variables, you might need to restart your terminal/ide, and stop current Gradle daemons using ./gradlew --stop
You will need GCC
to compile with the generated headers. Any GCC
that supports gnu99
or c11
should be fine.
./gradlew clean shadowJar
You will find an executable jar in build/libs/pni.jar
java -jar build/libs/pni.jar -version
java -jar build/libs/pni.jar -help
You can make a native-image for pni.jar
./gradlew clean nativeImage
./pni.run --help
You will need
GraalVM
, whose version corresponds to your JDK, to make the native-image.
There's a sample program, which is an http server listening on :80
.
./gradlew clean runSample
curl 127.0.0.1:80
To run the native image:
./gradlew clean runSampleNativeImage
You will need
GraalVM
, whose version corresponds to your JDK, to make the native-image.
./gradlew clean runAcceptanceTest
./gradlew clean runUnitTest
# for native image tests:
./gradlew clean runGraalTest
Click to reveal/hide
It's recommended to use Gradle
as the building system, otherwise you will have to manually generate files using the pni
command line tool.
Here's the tutorial for Gradle
:
- Configure your building environment
- Choose a pni version to use
- Add a new source root for generated java classes
- Create folders for C files
- Add
pni-api
dependency to your project - Add a Gradle subproject for template classes
- Add
pni-api
dependency to the subproject - Add a Gradle task to run the code generator
- Add
-parameters
compiler argument - Write template classes
- Generate
- Implement functions in C
- Compile
- Load the shared library in Java
Please follow the steps in chapter How to build
. Installing JDKs, configuring environment variables, installing compiling tools, etc.
If you are able to run the sample program, then you are ready to go!
You will need to make sure you Gradle
can work with your JDK, e.g. JDK 21==Gradle 8.3
, or JDK 22==Gradle 8.8
, etc.
Check and modify distributionUrl
in gradle/wrapper/gradle-wrapper.properties
properly.
In your build.gradle
, add the following snippet:
allprojects {
apply plugin: 'java'
java {
sourceCompatibility = '21' // corresponds to your jdk version
targetCompatibility = '21' // corresponds to your jdk version
}
tasks.withType(JavaCompile) {
options.compilerArgs += '--enable-preview' // remove this line if you are using jdk >= 22
}
tasks.withType(JavaExec) {
jvmArgs += '--enable-preview' // remove this line if you are using jdk >= 22
jvmArgs += '--enable-native-access=ALL-UNNAMED'
}
tasks.withType(Test) {
jvmArgs += '--enable-preview' // remove this line if you are using jdk >= 22
jvmArgs += '--enable-native-access=ALL-UNNAMED'
}
repositories {
mavenLocal()
mavenCentral()
}
}
This tells Gradle to build all projects (including subprojects) with specific JDK version and optionally adding --enable-preview
compiler option, and also specifies the maven repositories for all projects.
The latest Panama Native Interface
version is 21.0.0.21
, if you are using JDK >= 22
, you should switch to 22.0.0.21
The version will appear multiple times in build.gradle
, so you can define a variable at the beginning of the file:
buildscript {
def PNI_VERSION = '21.0.0.21' // use 22.0.0.21 if you are using jdk >= 22
ext.set("PNI_VERSION", PNI_VERSION)
// more configuration later ...
}
plugins {
// ...
}
def PNI_VERSION = project.PNI_VERSION
The buildscript
block will be used later.
It's recommended to separate generated files and handwritten files, so you may need a new source root
:
sourceSets {
main {
java {
srcDirs = ['src/main/java', 'src/main/generated']
}
}
}
Now the project has two folders for java source files: java
and generated
.
You will still need to create the folders manually:
mkdir -p src/main/java
mkdir -p src/main/generated
Create a directory src/main/c
to store handwritten C files and src/main/c-generated
to store generated C files.
mkdir -p src/main/c
mkdir -p src/main/c-generated
dependencies {
implementation "io.vproxy:pni-api-jdk21:"+PNI_VERSION
// if you are using jdk >= 22, you should switch to the following line:
// implementation "io.vproxy:pni-api-jdk22:"+PNI_VERSION
}
This module contains necessary classes for Panama Native Interface
to work,
and also contains useful classes for you to interact with the native world.
The subproject is used to hold template classes
, you may name it as pni-template
.
If you want to use a different subproject name, make sure you change all names accordingly during this tutorial.
If you are using IDEA
, it's easy to create a subproject simply by adding a new module.
Otherwise, you may have to edit settings.gradle
and create subproject folders manually.
You may refer to the following bash script to create the subproject:
#!/bin/bash
set -e
set -x
SUBPROJECT="pni-template"
echo "include '$SUBPROJECT'" >> ./settings.gradle
mkdir -p "./$SUBPROJECT/src/main/java"
echo 'compileJava {}' > "./$SUBPROJECT/build.gradle"
tee -a ./build.gradle <<EOF
project(":$SUBPROJECT") {
}
EOF
dependencies {
implementation "io.vproxy:pni-api-jdk21:"+PNI_VERSION
// if you are using jdk >= 22, you should switch to the following line:
// implementation "io.vproxy:pni-api-jdk22:"+PNI_VERSION
}
The template classes also require access to types in the pni-api
module.
You can add it to the build.gradle
inside the subproject folder, or you can add it in the root build.gradle
just like this:
project(':pni-template') {
// ...
dependencies { /* ... */ }
}
At the beginning of the build.gradle
file, insert the following code snippet into the buildscript
block:
buildscript {
// ...
dependencies {
classpath group: 'io.vproxy', name: 'pni-exec', version: PNI_VERSION
}
}
This snippet adds the code generator into classpath of the Gradle building system, so that you can directly invoke the generator in build.gradle
.
Add the following task inside the subproject:
task pniGenerate() {
dependsOn compileJava
def workingDir = project.rootProject.rootDir.getAbsolutePath()
def gen = new io.vproxy.pni.exec.Generator(
new io.vproxy.pni.exec.CompilerOptions()
.setClasspath(List.of(workingDir + '/pni-template/build/classes/java/main'))
.setJavaOutputBaseDirectory(workingDir + '/src/main/generated')
.setCOutputDirectory(workingDir + '/src/main/c-generated')
.setCompilationFlag(io.vproxy.pni.exec.CompilationFlag.RELEASE_PNI_H_FILE)
.setCompilationFlag(io.vproxy.pni.exec.CompilationFlag.RELEASE_PNI_C_FILE)
.setCompilationFlag(io.vproxy.pni.exec.CompilationFlag.RELEASE_JNI_H_MOCK_FILE)
)
doLast {
gen.generate()
}
}
You can add it to the build.gradle
inside the subproject folder, or you can add it in the root build.gradle
just like this:
project(':pni-template') {
task pniGenerate() { /* ... */ }
}
In order to retrieve parameter names from Java byte code, the -parameters
compiler argument should be added explicitly.
Add the following code snippet inside project pni-template
:
compileJava {
options.compilerArgs += '-parameters'
}
Write template classes in project pni-template
. See the below section How to use
.
./gradlew clean pniGenerate
Then you will find generated C files in src/main/c-generated
and generated Java classes in src/main/generated
Go to src/main/c
, write C implementation here.
To compile the C files, you will need pni.h
and jni.h
in your include search path (-I
option), and normally you will need to compile with pni.c
.
It's recommended to use the following compiler flags to release these files:
-frelease-pni-h-file[=<output-directory>]
-frelease-pni-c-file[=<output-directory>]
-frelease-jni-h-mock-file[=<output-directory>]
or programmatically:
new CompilerOptions()
.setCompilationFlag(CompilationFlag.RELEASE_PNI_H_FILE /* , new File(...) */)
.setCompilationFlag(CompilationFlag.RELEASE_PNI_C_FILE /* , new File(...) */)
.setCompilationFlag(CompilationFlag.RELEASE_JNI_H_MOCK_FILE /* , new File(...) */)
The
output-directory
defaults to the c output directory (-h
option).
Or you can include these files manually:
You can find pni.h
here.
and you can use the mocked jni.h
here,
or you can add "$JAVA_HOME/include"
and "$JAVA_HOME/include/$your_platform"
in your include search path instead, which is the traditional way when using JNI
.
Normally you will need to add pni.c
to the c file list and compile it into you library.
You could also build pni.c
into a standalone library libpni.so|libpni.dylib|pni.dll
, and call PanamaUtils.loadLib()
before loading your own library.
You may refer to make-sample.sh for more info.
Note: the pni.c is not always required. You can forget about it as long as you do not use
PNIRef
andPNIFunc
.
The shared library should be loaded in Java before any native capability is used:
System.loadLibrary("your-library-name");
The shared library file must be placed in -Djava.library.path
for Java to load.
Click to reveal/hide
gradle
dependencies {
implementation "io.vproxy:pni-api-jdk21:21.0.0.21"
// if you are using jdk >= 22, you should switch to the following line:
implementation "io.vproxy:pni-api-jdk22:22.0.0.21"
}
maven
<dependencies>
<dependency>
<groupId>io.vproxy</groupId>
<artifactId>pni-api-jdk21</artifactId>
<version>21.0.0.21</version>
<!-- if you are using jdk >= 22, you should switch to the following lines: -->
<!-- <artifactId>pni-api-jdk22</artifactId> -->
<!-- <version>22.0.0.21</version> -->
</dependency>
</dependencies>
For performance concern, simple POJOs are not directly converted to/from their native representations,
but users can define template
POJO classes, and then automatically generate both user-friendly and native-friendly Java classes.
You may define all template classes inside one single Java file, they don't have to be public.
@Struct
@Name("mbuf_t")
@AlwaysAligned
abstract class MBuf { // typedef struct mbuf_t {
MemorySegment bufAddr; // void* bufAddr;
@Unsigned int pktLen; // uint32_t pktLen;
@Unsigned int pktOff; // uint32_t pktOff;
@Unsigned int bufLen; // uint32_t bufLen;
UserData userdata; // union {
// void* userdata;
// uint64 udata64;
// };
} // } mbuf_t;
@Union(embedded = true)
@AlwaysAligned
abstract class UserData {
MemorySegment userdata;
@Unsigned long udata64;
}
@Downcall
interface SampleFunctions {
int read(int fd, MBuf buf) throws IOException;
// int Java_package_name_SampleFunctions_read(PNIEnv_int * env, int32_t fd, mbuf_t * buf);
}
Methods defined in template classes will also automatically result in methods in Java and functions in C.
Their return types or parameters should be pre-supported types or user defined template classes.
You can add throws list to the method if the native code is expected to raise exceptions.
It's recommended to define methods in template classes as abstract
.
java -jar pni.jar \
-cp 'path1:path2:jar3' \
-d java_output_base_directory \
-h c_headers_output_directory
// -frelease-pni-h-file -frelease-pni-c-file -frelease-jni-h-mock-file
// see -verbose --help for more info
The pni program will scan all classes in classpath then generate Java and C codes.
The generated Java types will share the same package as the template ones,
the generated C headers will have almost the same format as JNI output, see the following section for more details.
If you have multiple projects, let's say project A
and project B
, where template files of B
depends on
template files of A
, you can add both projects' classpath to -cp
, and specify -F <regexp>
to filter which
class needs to be generated.
The regexp matches the full name of the generated Java class, for example io\.vproxy\.luajn\.n\..*
.
Some compilation flags can be provided via -f<flag-name>[=<value>]
. See -verbose --help
for more info.
You may also use the Generator
class programmatically to achieve the same effect as using the command line tool.
You may refer to: chapter How to bundle into a Gradle project
, section Add a Gradle task to run the code generator
.
if @Style
is NOT annotated or is @Style(pni)
: (JNI-like
Style Function)
- take an argument
PNIEnv* env
as the first argument, but with different type variations based on the result type; - return
int
where0
means OK and any other value (usually-1
) means an exception is thrown; - the actual result should be stored in
env->return_
field;
For example:
JNIEXPORT int JNICALL Java_io_vproxy_vfd_posix_GeneralPosix_createIPv4TcpFD
(PNIEnv_int* env) {
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd < 0) {
return PNIThrowException(env, "java.io.IOException", strerror(errno));
}
env->return_ = sockfd;
return 0;
}
If you need to pass errno
to Java, you can call PNIStoreErrno(env)
. You can retrieve it from env.ex().errno()
in Java.
If @Style(critical)
is annotated: (JavaCritical
Style Function)
- There will be no
PNIEnv
argument. - Directly return values.
- Since the
PNIEnv
is absent, you will NOT be able to use any functionality associated with it, e.g. throwing exceptions from the native function.
For example:
JNIEXPORT int32_t JNICALL JavaCritical_io_vproxy_pni_test_Func_writeCritical
(int32_t fd, void * buf, int32_t off, int32_t len) {
int n = write(fd, buf + off, len);
if (n < 0) {
return -errno;
}
return n;
}
If the Java method is defined inside a class, then the generated C function will have an extra parameter right after PNIEnv
, providing the self
pointer.
For Critical
style functions, self
will be the first parameter.
If the method's return type requires memory allocation, the generated C function accepts one more parameter, as the memory address of that object.
You should set env->return_ = the_extra_variable
if you need to return the value, or env->return_ = NULL
if you want to return NULL
.
For Critical
style functions, you can simply return the extra variable or return NULL
.
By setting @NoAlloc
annotation on the method, the generated C function will not have the extra parameter.
Please see chapter Annotations
for more detail.
Sometimes you may want to directly invoke a function of a shared library, Panama Native Interface
also supports this usage.
You will need to add @Name("...")
on your template method, and make sure the generated function has exactly the same signature of the function you want to call.
Note: in this scenario, you will not need the generated C functions when compiling, but it's good for checking whether you've configured you template classes/methods properly.
For example:
Template class:
@Struct
abstract class PNIXskInfo {
// struct fields could be defined here ...
@Name("vp_xdp_fetch_pkt")
@Style(Styles.critical)
@LinkerOption.Critical
abstract int fetchPacket(@Raw @Unsigned int[] idxRxPtr, @Raw MemorySegment[] chunkPtrs);
}
The vp_xdp_fetch_pkt
corresponds to the following C function:
int vp_xdp_fetch_pkt(struct vp_xsk_info* xsk, uint32_t* idx_rx_ptr, struct vp_chunk_info** chunkptr);
Please see chapter Annotations
for more info.
All generated Java classes have getters for all fields, and setters for all non-embedded fields (struct/union/array),
as well as methods defined in the templates.
Template interfaces will generated singleton classes.
All generated classes do NOT have dependencies on the template classes/interfaces.
The generated Java types have almost the same names of their templates.
You can customize name prefix of template or generated types using -ftype-name-prefix="..."
or setCompilationFlag(TYPE_NAME_PREFIX, "...")
.
If the template types have the specified prefix, then the generated types will discard the prefix. If template types do not have the prefix, then the generated types will prepend the prefix.
By setting -ftype-name-prefix=''
or setCompilationFlag(TYPE_NAME_PREFIX, "")
, there will be no prefix discarded or prepended, i.e. generated types will have exactly the same names as the template ones.
If the method's return type requires memory allocation, an extra parameter Allocator ALLOCATOR
will be added to the last of the arguments list.
You can release the memory by closing the allocator.
It's recommended to use PooledAllocator.ofPooled()
or PooledAllocator.ofConcurrentPooled()
whenever possible.
You could also use PooledAllocator.ofUnsafePooled()
if really needed.
You can define your own memory pool via PooledAllocator.setXxxProvider(...)
.
The default behavior for Pooled
allocators when custom allocator is not present, is the same as Confined
allocators.
The default behavior for ConcurrentPooled
allocators is the same as Shared
allocators.
Please see the below sections: Graal Native Image
and Graal Native Image Upcall
.
Click to reveal/hide
Panama Native Interface
supports inheritance. You can use Java extends
keyword in template classes.
Only a struct
can extend from another struct
.
union
s are not allowed to inherit nor to be inherited.
For example:
@Struct
@AlwaysAligned
abstract class BaseClass {
byte a;
}
@Struct
@AlwaysAligned
abstract class ChildClass extends BaseClass {
short x;
}
@Struct
@AlwaysAligned
abstract class GrandChildClass extends ChildClass {
long y;
}
The memory layout of ChildClass
would be:
struct ChildClass {
BaseClass SUPER;
short x;
};
So basically what the code generator does is to insert the parent struct before the first field.
Supporting inheritance can make use of Java's object oriented type system, while composition cannot achieve this.
Click to reveal/hide
GraalVM
supports building native image with Panama support.
You can add a flag to the pni
program to generate a Feature
implmentation, which is required by the native image generation process.
java -jar pni.jar <...> -fgraal-native-image-feature=<feature-class-name>
# you might also want to add argument -fgraal-c-entrypoint-literal-upcall, see the below section for more info
or programmatically:
new CompilerOptions()
.setCompilationFlag(io.vproxy.pni.exec.CompilationFlag.GRAAL_NATIVE_IMAGE_FEATURE, "$featureClassName")
// you migh also want to add the flag:
// .setCompilationFlag(io.vproxy.pni.exec.CompilationFlag.GRAAL_C_ENTRYPOINT_LITERAL_UPCALL)
// see the below section for more info
To compile your project with the Feature
class, you should use GraalVM
instead of a traditional JDK, or:
You could also use the native image sdk: org.graalvm.sdk:nativeimage:+
instead of changing the JDK.
Another (maybe better) way of managing the dependencies is to use the mock version graal sdk:
- for compiling, add dependency
compileOnly 'io.vproxy:graal-sdk-mock-nativeimage:+'
- for running, add dependency
runtimeOnly 'io.vproxy:graal-sdk-mock-runtime:+'
The mock libraries provides all necessary types and members for Panama Native Interface
generated graal related classes.
Detailed information can be found in the repo.
Adding extra dependencies:
gradle
dependencies {
// ...
implementation "io.vproxy:pni-api-graal:21.0.0.21"
// if you are using jdk >= 22, you should switch to the following line:
// implementation "io.vproxy:pni-api-graal-jdk22:22.0.0.21"
compileOnly "io.vproxy:graal-sdk-mock-nativeimage:1.2.1"
runtimeOnly "io.vproxy:graal-sdk-mock-runtime:1.2.1"
}
maven
<dependencies>
<!-- ... -->
<dependency>
<groupId>io.vproxy</groupId>
<artifactId>pni-api-graal</artifactId>
<version>21.0.0.21</version>
// if you are using jdk >= 22, you should switch to the following line:
<!-- <artifactId>pni-api-graal-jdk22</artifactId> -->
<!-- <version>22.0.0.21</version> -->
</dependency>
<dependency>
<groupId>io.vproxy</groupId>
<artifactId>graal-sdk-mock-nativeimage</artifactId>
<version>1.2.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>io.vproxy</groupId>
<artifactId>graal-sdk-mock-runtime</artifactId>
<version>1.2.1</version>
<scope>runtime</scope>
</dependency>
</dependencies>
Please pay additional attention when using the native-image:
- You can check whether the code is running in Graal native-image or in a normal JRE by
ImageInfoDelegate.inImageCode()
GraalUtils.init()
must be called at the beginning of your application.GraalUtils.setThread()
must be called when a new thread is spawn. This method can be safely called for multiple times, which is useful if you are using a thread pool and don't know whether the thread is initialized.- When compiling the C code, an additional compiler option
-DPNI_GRAAL=1
must be added, which will enable Graal related functions. Note that some PNI functions are defined usingstatic inline
, so you must compile all related files withPNI_GRAAL=1
.
Click to reveal/hide
As for now (2024-08-10)
the graal native-image only supports Panama upcall on Linux x86_64
.
But Panama Native Interface
provides the upcall support based on Graal C native features:
Add compilation flag -fgraal-c-entrypoint-literal-upcall
on the command line, or call
.setCompilationFlag(CompilationFlag.GRAAL_C_ENTRYPOINT_LITERAL_UPCALL)
programmatically to enable this feature.
Note that the native-image
CEntryPoint
upcall performance is much slower comparing to the Panama upcall.
To build the native image, you may use the following command:
native-image -jar <jar-file> \
--features=<feature-class-name> \
--enable-preview \
--enable-native-access=ALL-UNNAMED \
--no-fallback \
-O3 -march=native \
-o <binary-name>
See sampleNativeImage
task in build.gradle
for more info.
Extra attention:
If the upcall happens on a non java thread, SetPNIGraalThread(isolate_thread)
must be called before the upcall inside the C code.
To create an IsolateThread
in the C code, you can firstly use GetPNIGraalIsolate()
to retrieve the Isolate
object for the currently running native-image, and then use graal_attach_thread(isolate, &isolate_thread)
to attach current thread and create the IsolateThread
object.
See this page for more info.
If the thread in created on the Java side, you should call GraalUtils.setThread()
(as suggested in the previous chapter) before interacting with the native world.
Click to reveal/hide
Panama provides a way for C to invoke Java methods. Panama Native Interface
provides multiple ways to simplify this process.
@Upcall
template interfacePanamaUtils.defineCFunction
- CallSite and PNIFunc
- PNIRef
You can use @Upcall
template interfaces to generate upcall functions.
The pni
program will generate the following files:
- a
.h
file, containing the function declarations - a
.c
file, containing the function definitions, which calls thePanama upcall stub
function pointer - a Java class, containing static fields of function memory addresses (
MemorySegment
) - a Java interface, defining the method signature for you to implement
You must call TheGeneratedClass.setImpl(yourImpl)
before using these upcall functions,
otherwise the program will print an error message and exit when the functions get called.
You can use PanamaUtils.defineCFunction
or PanamaUtils.defineCFunctionByName
to define C functions easily.
- Store an
Arena
globally, which is used to allocate memory for the defined function. - Define a
static
Java method, and make it public. - Call
PanamaUtils.defineCFunctionByName(arena, YouClassName.class, "yourMethodName")
Done!
Note: only primitive types
and MemorySegment
are allowed to be used as the method parameter types and return types.
For native-image
s, you should use GraalUtils.defineCFunctionXxx
methods, and store the CFunctionLiteral
in a static final
field, and the class which stores static final
fields must be initialized at build time.
It would be better to use @Upcall
template interface instead, which had managed everything for you.
Panama Native Interface
also provides another encapsulation, which allows you to pass lambda expressions to C.
Use PNIFunc<T>
as a method parameter in template classes, where T
must be a Struct
or Union
or java.lang.Void
or PNIRef<U>
.
The generated Java method uses CallSite<T>
as its parameter.
It is a functional interface,
whose function signature is (T) -> int
, where T
allows you to share variables between Java and C,
while the returned int
provides the execution result.
On the C side, the function pointer is wrapped inside a PNIFunc * func
variable.
To invoke the function, use int result = PNIFuncInvoke(func, &value);
You may store the PNIFunc
object and use it later, you can even invoke it on a new thread.
As a result, you MUST release the object when you finished using it: PNIFuncRelease(func);
The PNIFunc
struct has a union field union { void * userdata; uint64_t udata64; }
for you to store your own data in it.
This is useful for example when you store the PNIFunc*
in epoll_event.data.ptr
.
You can allocate some extra memory when creating a PNIFunc
by calling T.Func.of(<lambda>, new Options().setUserdataByteSize(...))
.
The extra memory will be stored inside the userdata
pointer field automatically. In this way, the memory gets released with the func.
If any error thrown from the CallSite, the PNIFunc will catch it and print the exception,
then return ((int32_t) PNIFuncInvokeExceptionCaught)
to C.
You can add @Raw
annotation on the parameter to set the generated Java method parameter to PNIFunc<T>
instead of CallSite<T>
.
You can use PNIFunc<T>
in method parameters, return types, or fields.
You can create PNIFunc<T>
using PNIFunc.VoidFunc.of(CallSite<Void>)
or T.Func.of(CallSite<T>)
or PNIRef.Func.of(CallSite<T>)
.
You can also release a PNIFunc<T>
using func.close()
on the Java side.
To share Java objects with C, you can use PNIRef<T>
: PNIRef.of(object)
.
You can release the PNIRef<T>
on the Java side: ref.close()
, or release it on the C side: PNIRefRelease(ref)
.
You will not be able to manipulate the Java object on the C side obviously, but you can pass it around and use it as an argument in an upcall function or store it in a field.
Also the PNIRef
provides a similar userdata
union as the PNIFunc
does.
You can call PNIRef.of(obj, new Options().setUserdataByteSize(...))
, the behavior is exactly the same as PNIFunc
.
Click to reveal/hide
@Struct
: generate C struct from the marked class, you can set@Struct(skip=true)
to skip generating the type definition (this is useful if the type is already defined in another C header file).@Union
: generate C union from the marked class, you can set@Union(skip=true)
to skip generating the type definition, while setting@Union(embedded=true)
will make it embedded into other types automatically.@Downcall
: generate downcall functions from the marked interface.@Upcall
: generate upcall functions from the marked interface.
If a
union
is already defined in another C header file, you should use@Union(skip=true)
. If it's not pre-defined and you want it to be embedded into another struct, you should use@Union(embedded=true)
.
Mixing both will have the same effect of only using@Union(embedded=true)
.
@LinkerOption.Critical
: make a MethodHandlecritical
. Seejdk21 Linker.Option#isTrivial
orjdk22 Linker.Option#critical
for more info.
You may set@LinkerOption.Critical(allowHeapAccess=true)
to use heap memory in native code.
Note:allowHeapAccess
is not supported by jdk21, the value ofallowHeapAccess
will be ignored on jdk21. Also, this feature is not supported by Graal native-image and compilation will fail. You must use-fdisable-allow-heap-access
to disable this feature.@Align
: define the minimum alignment bytes. You can set@Align(packed=true)
to disable padding. This annotation has the same effect as setting__attribute__((aligned(N)))
or__attribute__((packed))
inGCC
.@AlwaysAligned
: assumes that the annotated class or field to be always aligned. This will result in a JavaValueLayout
without_UNALIGNED
suffix. A jmh benchmark shows that accessing "manually aligned" fields has the same performance as accessing "unaligned" fields, and is a little bit slower than "aligned" fields in Panama.
This annotation is not the default behavior because adding it means that you will not be allowed to put the type on a random memory location.
The generated C code will not be affected by this annotation. The generator calculates the paddings only based on type info and@Align
annotation, and decide to generate packed or non-packed structs, with or without explicit paddings.
@Pointer
: make a custom type field to be a pointer. The default behavior without@Pointer
annotation, is embedding the type into the parent struct.@Len
: define the element count of an array, or the native memory length of a string (memory length, not string length).@Unsigned
: make an integer typeunsinged
.@Raw
: convert to raw form for native invocation. See the below section@Raw Annotation
for more info.@PointerOnly
: this annotation is only effective during validation phase. The marked class cannot have fields, the type should only be used as a pointer. However the generated Java class or C struct/union will stay the same as they were.@Bit
and@Bit.Field
: mark a byte/short/int/long field to be a bit field. For example:@Bit({@Bit.Field(name = "a", bits = 1), @Bit.Field(name = "b", bits = 1)}) @Unsigned byte x
, defines two bit fieldsuint8_t a : 1
anduint8_t b : 1
, and automatically generates an anonymous padding to fillup the field's type (uint8_t : 6
). You can set@Bit.Field(name="x", bits = 1, bool=true)
to generateboolean
getter/setters.
WARNING: bit fields' memory layout is NOT specified in C standard and is NOT compiler/platform portable, use with caution!!!@Sizeof
: specify the minimum byte size of the type. This is useful for@PointerOnly
types andskip=true
types when only part of fields are specified in Java.
For example:With the help of@Struct(skip = true) @Include("msquic.h") @Name("QUIC_ADDR") @PointerOnly @Sizeof("QUIC_ADDR") // you may also write multi-line statements here, and you can include header files as well public abstract class QuicAddr { }
@Sizeof
, you can allocate memory forQuicAddr
from Java and pass it to native.
Note: You CANNOT use a@Sizeof
class for a non-pointer field unless it's in a union or is the last field in a struct.
Also, the@Sizeof
annotation is infectious, ifclass A
has a non-pointer field whose class is annotated with@Sizeof
, thenclass A
must be annotated withSizeof
as well.@NativeType
: specify the native type of the field or parameter.@NativeReturnType
: specify the native return type of the method.
The@NativeType
and@NativeReturnType
are useful for defining the real type for avoid*
pointer.
@Name
: define the native name.@Style
: when set to@Style(critical)
, pni will generate native functions withoutPNIEnv
. You can directly usereturn
to return values to Java. However, since thePNIEnv
is absent, you will not be able to use any functionality associated with it, for example, throwing exceptions from the C code.@NoAlloc
: generate functions withoutAllocator
, even if the return type might require one.@SpecifyGeneratedMembers
and@GenerateMember
: manually specify members to be included into the generator. This is useful for some JVM languages which might generate additional members.
@Include
: add#include ...
when generating the header file. This is useful if some type is defined in another C header file.
@Include("...")
will generate#include "..."
, while@Include("<...>")
will generate#include <...>
.@Impl
: write C function definition in Java. See the following example:When@Impl( include = {"<unistd.h>"}, // language="c" c = """ int ret = write(fd, buf + off, len); if (ret < 0) { return PNIThrowException(env, "java.io.Exception", strerror(errno)); } env->return_ = ret; return 0; """ ) int write(int fd, @Raw ByteBuffer buf, int off, int len) throws IOException;
@Impl
is specified, an extra header file with.impl.h
suffix will be generated along with the normal.h
header. You can include the.impl.h
header in your C file.
Note that, the comment// launuage="c"
will let JetBrains IDEA highlight the text block with C syntax.
Click to reveal/hide
Annotate the data type to be converted to its raw form. You can only mark method parameters with this annotation.
ByteBuffer
: will be converted toMemorySegment
. This has the same effect as callingMemorySegment.ofBuffer(...)
after settingByteBuffer.position()
to 0 andByteBuffer.limit()
toByteBuffer.capacity()
, without actually modifying these properties.T[]
: arrays will be converted to their raw form without thePNIBuf
wrapper. There will be no length info, so you might need to pass in their length manually.PNIRef<T>
: if without@Raw
annotation, templatePNIRef<T>
params will result inT
in generated java params.
With@Raw
annotation, templatePNIRef<T>
params will result inPNIRef<T>
in generated java params.PNIFunc<T>
: usePNIFunc<T>
in the generated Java method parameters, instead of the defaultCallSite<T>
.
Click to reveal/hide
You can use --verbose --help
to show all available warnings and flags, as well as the current values.
You can also add -W<...>
or -f<...>
before --help
to test these warnings or flags.
Click to reveal/hide
Java | @Unsigned |
@Pointer |
@Len |
C Field | C Function Param | C Extra Return Param | C PNIEnv_${type} |
Generated Java Type | Generated Layout |
---|---|---|---|---|---|---|---|---|---|
int | No | - | - | int32_t |
int32_t |
- | int |
int | JAVA_INT |
int | Yes | - | - | uint32_t |
uint32_t |
- | int |
int | JAVA_INT |
long | No | - | - | int64_t |
int64_t |
- | long |
long | JAVA_LONG |
long | Yes | - | - | uint64_t |
uint64_t |
- | long |
long | JAVA_LONG |
short | No | - | - | int16_t |
int16_t |
- | short |
short | JAVA_SHORT |
short | Yes | - | - | uint16_t |
uint16_t |
- | short |
short | JAVA_SHORT |
byte | No | - | - | int8_t |
int8_t |
- | byte |
byte | JAVA_BYTE |
byte | Yes | - | - | uint8_t |
uint8_t |
- | byte |
byte | JAVA_BYTE |
float | - | - | - | float |
float |
- | float |
float | JAVA_FLOAT |
double | - | - | - | double |
double |
- | double |
double | JAVA_DOUBLE |
boolean | - | - | - | uint8_t |
uint8_t |
- | bool |
boolean | JAVA_BOOLEAN |
char | - | - | - | uint16_t |
uint16_t |
- | char |
char | JAVA_CHAR |
String | - | - | No | char * |
char * |
- | pointer |
PNIString | ADDRESS |
String | - | - | Yes | char x[len] |
- | - | - | String | sequenceLayout(len, JAVA_BYTE) |
MemorySegment | - | - | - | void * |
void * |
- | pointer |
MemorySegment | ADDRESS |
ByteBuffer | - | - | - | PNIBuf |
PNIBuf * |
PNIBuf * |
buf |
ByteBuffer | PNIBuf.LAYOUT |
ByteBuffer (@Raw ) |
- | - | - | - | char * |
- | - | ByteBuffer | - |
Struct/Union | - | No | - | Type |
- | - | - | Type | Type.LAYOUT |
Struct/Union | - | Yes | - | Type * |
Type * |
Type * |
pointer |
Type | ADDRESS |
int[] | No | * |
No | PNIBuf_int |
PNIBuf_int * |
PNIBuf_int * |
buf_int |
IntArray | PNIBuf.LAYOUT |
int[] | Yes | * |
No | PNIBuf_uint |
PNIBuf_uint * |
PNIBuf_uint * |
buf_uint |
IntArray | PNIBuf.LAYOUT |
long[] | No | * |
No | PNIBuf_long |
PNIBuf_long * |
PNIBuf_long * |
buf_long |
LongArray | PNIBuf.LAYOUT |
long[] | Yes | * |
No | PNIBuf_ulong |
PNIBuf_ulong * |
PNIBuf_ulong * |
buf_ulong |
LongArray | PNIBuf.LAYOUT |
short[] | No | * |
No | PNIBuf_short |
PNIBuf_short * |
PNIBuf_short * |
buf_short |
ShortArray | PNIBuf.LAYOUT |
short[] | Yes | * |
No | PNIBuf_ushort |
PNIBuf_ushort * |
PNIBuf_ushort * |
buf_ushort |
ShortArray | PNIBuf.LAYOUT |
byte[] | No | * |
No | PNIBuf_byte |
PNIBuf_byte * |
PNIBuf_byte * |
buf_byte |
MemorySegment | PNIBuf.LAYOUT |
byte[] | Yes | * |
No | PNIBuf_ubyte |
PNIBuf_ubyte * |
PNIBuf_ubyte * |
buf_ubyte |
MemorySegment | PNIBuf.LAYOUT |
float[] | - | * |
No | PNIBuf_float |
PNIBuf_float * |
PNIBuf_float * |
buf_float |
FloatArray | PNIBuf.LAYOUT |
double[] | - | * |
No | PNIBuf_double |
PNIBuf_double * |
PNIBuf_double * |
buf_double |
DoubleArray | PNIBuf.LAYOUT |
boolean[] | - | * |
No | PNIBuf_bool |
PNIBuf_bool * |
PNIBuf_bool * |
buf_bool |
BoolArray | PNIBuf.LAYOUT |
char[] | - | * |
No | PNIBuf_char |
PNIBuf_char * |
PNIBuf_char * |
buf_char |
CharArray | PNIBuf.LAYOUT |
MemorySegment[] | - | * |
No | PNIBuf_ptr |
PNIBuf_ptr * |
PNIBuf_ptr * |
buf_ptr |
PointerArray | PNIBuf.LAYOUT |
Type[] | - | * |
No | PNIBuf_Type |
PNIBuf_Type * |
PNIBuf_Type * |
buf_Type |
Type.Array | PNIBuf.LAYOUT |
int[] (@Raw ) |
No | * |
No | - | int32_t * |
- | - | IntArray | - |
int[] (@Raw ) |
Yes | * |
No | - | uint32_t * |
- | - | IntArray | - |
long[] (@Raw ) |
No | * |
No | - | int64_t * |
- | - | LongArray | - |
long[] (@Raw ) |
Yes | * |
No | - | uint64_t * |
- | - | LongArray | - |
short[] (@Raw ) |
No | * |
No | - | int16_t * |
- | - | ShortArray | - |
short[] (@Raw ) |
Yes | * |
No | - | uint16_t * |
- | - | ShortArray | - |
byte[] (@Raw ) |
No | * |
No | - | void * |
- | - | MemorySegment | - |
byte[] (@Raw ) |
Yes | * |
No | - | uint8_t * |
- | - | MemorySegment | - |
float[] (@Raw ) |
- | * |
No | - | float * |
- | - | FloatArray | - |
double[] (@Raw ) |
- | * |
No | - | double * |
- | - | DoubleArray | - |
boolean[] (@Raw ) |
- | * |
No | - | uint8_t * |
- | - | BoolArray | - |
char[] (@Raw ) |
- | * |
No | - | uint16_t * |
- | - | CharArray | - |
MemorySegment[] (@Raw ) |
- | * |
No | - | void ** |
- | - | PointerArray | - |
Type[] (@Raw ) |
- | * |
No | - | Type * |
- | - | Type.Array | - |
int[] | No | - | Yes | int32_t x[len] |
- | - | - | IntArray | sequenceLayout(len, JAVA_INT) |
int[] | Yes | - | Yes | uint32_t x[len] |
- | - | - | IntArray | sequenceLayout(len, JAVA_INT) |
long[] | No | - | Yes | int64_t x[len] |
- | - | - | LongArray | sequenceLayout(len, JAVA_LONG) |
long[] | Yes | - | Yes | uint64_t x[len] |
- | - | - | LongArray | sequenceLayout(len, JAVA_LONG) |
short[] | No | - | Yes | int16_t x[len] |
- | - | - | ShortArray | sequenceLayout(len, JAVA_SHORT) |
short[] | Yes | - | Yes | uint16_t x[len] |
- | - | - | ShortArray | sequenceLayout(len, JAVA_SHORT) |
byte[] | No | - | Yes | int8_t x[len] |
- | - | - | MemorySegment | sequenceLayout(len, JAVA_BYTE) |
byte[] | Yes | - | Yes | uint8_t x[len] |
- | - | - | MemorySegment | sequenceLayout(len, JAVA_BYTE) |
float[] | - | - | Yes | float x[len] |
- | - | - | FloatArray | sequenceLayout(len, JAVA_FLOAT) |
double[] | - | - | Yes | double x[len] |
- | - | - | DoubleArray | sequenceLayout(len, JAVA_DOUBLE) |
boolean[] | - | - | Yes | uint8_t x[len] |
- | - | - | BoolArray | sequenceLayout(len, JAVA_BOOLEAN) |
char[] | - | - | Yes | uint16_t x[len] |
- | - | - | CharArray | sequenceLayout(len, JAVA_CHAR) |
MemorySegment[] | - | - | Yes | void * x[len] |
- | - | - | PointerArray | sequenceLayout(len, ADDRESS) |
Type[] | - | - | Yes | Type x[len] |
- | - | - | Type.Array | sequenceLayout(len, Type.LAYOUT) |
PNIFunc<T> (field or return) |
- | - | - | PNIFunc * |
PNIFunc * |
- | func |
PNIFunc<T> |
ADDRESS |
PNIFunc<T> (param) |
- | - | - | - | PNIFunc * |
- | - | CallSite<T> |
- |
PNIFunc<PNIRef<T>> (param) |
- | - | - | - | PNIFunc * |
- | - | CallSite<T> |
- |
PNIFunc<T> (param @Raw ) |
- | - | - | - | PNIFunc * |
- | - | PNIFunc<T> |
- |
PNIFunc<PNIRef<T>> (param @Raw ) |
- | - | - | - | PNIFunc * |
- | - | PNIFunc<T> |
- |
PNIRef<T> (field or return) |
- | - | - | PNIRef * |
PNIRef * |
- | ref |
PNIRef<T> |
ADDRESS |
PNIRef<T> (param) |
- | - | - | - | PNIRef * |
- | - | T |
- |
PNIRef<T> (param @Raw ) |
- | - | - | - | PNIRef * |
- | - | PNIRef<T> |
- |
*
: Both Yes
and No
.
-
: Cannot mark the annotation.
Note that the return types and parameters are always considered to be marked with
@Pointer
when possible.
Any other combination except the above table is disallowed.
Click to reveal/hide
Panama Native Interface
does not provide built-in pooled allocators implementation, but allow you to register your own impl into the framework. You can use PooledAllocator.ofXxx
functions to retrieve a pooled allocator.
There are three kinds of Pooled Allocators:
ofPooled
: the simplest pooled allocator, which doesn't have to provide multi-thread support.ofConcurrentPooled
: must provide multi-thread support.ofUnsafePooled
: usually wraps around unsafe, or provided by a native allocator implementation, such asjemalloc
, the memory must not be managed by the JVM (because JVM managed MemorySegments and Arenas may have certain limitations).
You can register you implementation via PooledAllocator.setXxxProvider
:
setPooledAllocatorProvider
setConcurrentPooledAllocatorProvider
setUnsafePooledAllocatorProvider
Click to reveal/hide
- This project has a pre assumption:
sizeof(void*)
is 8 bytes. In other words, you can only use this project on a 64bit processor. This shouldn't be a problem because there's very rare chance that you would run Java on a 32bit platform. - When you throw an exception from native code, you should ensure that the exception type name char array does not require releasing.
- Only primitive types or
MemorySegment
or custom types can be used to generate arrays, and the arrays can only be 1 dimension. To use 2 or more dimension arrays, the only way to achieve this is to calculate the array length and use 1 dimension array instead. - You should avoid using "all upper case" type names or variables. The extra params or local variables in the generated code are "all upper case", and naming collisions of these variables are not checked during the validation phase. This shouldn't be a problem, because normally people won't define "all upper case" type names or member fields.
- Bit fields are not compiler/platform portable.
Panama Native Interface
provides limited bit fields support, it allows you to define bit fields, but you must define them on an integer type. The total bit must not exceed the integer type's memory size, and a padding will automatically be generated if the bit fields' total bit is less than the integer type memory size.
You must use bit fields with caution. The most common use case of bit fields is to define switches, and this should work well usually, but it's NOT guarenteed. [1] - A JMH benchmark shows that disabling inlining of
ConcurrentHashMap#get
can improve performance when retrieving values fromObjectHolder
. However the benchmark only shows performance of the certain case. You may try to add or remove jvm option-XX:CompileCommand=dontinline,io.vproxy.pni.impl.ForceNoInlineConcurrentLongMap::*
and bench your own code.
[1] C11: An implementation may allocate any addressable storage unit large enough to hold a bit-field. If enough space remains, a bit-field that immediately follows another bit-field in a structure shall be packed into adjacent bits of the same unit. If insufficient space remains, whether a bit-field that does not fit is put into the next unit or overlaps adjacent units is implementation-defined. The order of allocation of bit-fields within a unit (high-order to low-order or low-order to high-order) is implementation-defined. The alignment of the addressable storage unit is unspecified.