Skip to content

Commit 4fb7944

Browse files
sgammondarvld
andcommitted
Support JNA over Static JNI on GraalVM
Implements a new optional linkage feature, called Static JNI, under GraalVM Native Image. With `com.sun.jna.SubstrateStaticJNA` enabled (opt-in), JNA is loaded eagerly at image build time, and then linked against a static copy of `libjnidispatch` at image link-time. The result is that `libjnidispatch.a` is embedded within the final image. No precursor library unpacking step is necessary before using JNA in this circumstance, because JNA's native layer is built directly into the image itself. - feat: implement static jni feature - chore: full gvm ci build - chore: add static jni sample Signed-off-by: Sam Gammon <[email protected]> Signed-off-by: Dario Valdespino <[email protected]> Co-authored-by: Sam Gammon <[email protected]> Co-authored-by: Dario Valdespino <[email protected]>
1 parent 5c43428 commit 4fb7944

17 files changed

+838
-9
lines changed

.github/workflows/graalvm.yaml

+6-2
Original file line numberDiff line numberDiff line change
@@ -30,5 +30,9 @@ jobs:
3030
- name: Linux requirements
3131
run: sudo apt-get -y install texinfo
3232
- uses: gradle/actions/setup-gradle@v3
33-
- name: "Build: Native Image"
34-
run: ant dist && ant install && ant nativeImage && ant nativeRun
33+
- name: "Build: Compile & Install JNA"
34+
run: ant && ant install
35+
- name: "Build: Native Images (Dynamic JNI)"
36+
run: ant nativeImage && ant nativeRun
37+
- name: "Build: Native Image (Static JNI)"
38+
run: ant nativeImageStatic && ant nativeRunStatic

build.xml

+31-4
Original file line numberDiff line numberDiff line change
@@ -292,9 +292,9 @@
292292
<artifact:dependencies pathId="graalvm.classpath">
293293
<remoteRepository refid="central" />
294294
<dependency groupId="org.graalvm.sdk" artifactId="nativeimage" version="${graalvm.version}"/>
295+
<dependency groupId="org.graalvm.nativeimage" artifactId="svm" version="${graalvm.version}"/>
295296
</artifact:dependencies>
296297

297-
<delete dir="${classes-graalvm}" />
298298
<mkdir dir="${classes-graalvm}" />
299299
<delete dir="${build}/gvm-src" />
300300
<mkdir dir="${build}/gvm-src/com/sun/jna" />
@@ -656,9 +656,6 @@ osname=macosx;processor=aarch64
656656
<zipfileset src="${build}/${jar}" excludes="META-INF/MANIFEST.mf"/>
657657
<zipfileset dir="${build}/manifest/" includes="module-info.class" prefix="META-INF/versions/9"/>
658658
</jar>
659-
<jar jarfile="${build}/jna-graalvm.jar" duplicate="preserve" createUnicodeExtraFields="never" encoding="UTF-8">
660-
<zipfileset src="${build}/${jar-graalvm}" excludes="META-INF/MANIFEST.mf"/>
661-
</jar>
662659
</target>
663660

664661
<target name="aar" depends="jar" description="Build Android Archive">
@@ -1123,6 +1120,8 @@ cd ..
11231120
<copy todir="${classes-graalvm}/${native.path}">
11241121
<fileset dir="${build.native}"
11251122
includes="jnidispatch.dll,libjnidispatch.*"/>
1123+
<fileset dir="${build.native}/libffi/.libs"
1124+
includes="ffi.lib,libffi.a"/>
11261125
</copy>
11271126
<!-- For web start, native libraries may be provided in the root of -->
11281127
<!-- an included jar file -->
@@ -1135,6 +1134,7 @@ cd ..
11351134
</jar>
11361135
<jar jarfile="${build}/${native-static.jar}" createUnicodeExtraFields="never" encoding="UTF-8">
11371136
<fileset dir="${build.native}" includes="jnidispatch.lib,libjnidispatch.a"/>
1137+
<fileset dir="${build.native}/libffi/.libs" includes="libffi.a"/>
11381138
<manifest>
11391139
<attribute name="Implementation-Version" value="${jni.version} b${jni.build}"/>
11401140
<attribute name="Specification-Version" value="${jni.version}"/>
@@ -1781,9 +1781,36 @@ cd ..
17811781
</exec>
17821782
</target>
17831783

1784+
<target name="nativeImageStatic">
1785+
<exec executable="/bin/bash" dir="${basedir}/samples/graalvm-native-static-jna" failonerror="true">
1786+
<arg value="${basedir}/samples/graalvm-native-static-jna/gradlew"/>
1787+
<arg value="-PjnaVersion=${jna.version}"/>
1788+
<arg value="-PgraalvmVersion=${graalvm.version}"/>
1789+
<arg value="build"/>
1790+
<arg value="nativeCompile"/>
1791+
</exec>
1792+
</target>
1793+
1794+
<target name="nativeImageStaticDebug">
1795+
<exec executable="/bin/bash" dir="${basedir}/samples/graalvm-native-static-jna" failonerror="true">
1796+
<arg value="${basedir}/samples/graalvm-native-static-jna/gradlew"/>
1797+
<arg value="-PjnaVersion=${jna.version}"/>
1798+
<arg value="-PgraalvmVersion=${graalvm.version}"/>
1799+
<arg value="-PnativeImageDebug=true"/>
1800+
<arg value="build"/>
1801+
<arg value="nativeCompile"/>
1802+
</exec>
1803+
</target>
1804+
17841805
<target name="nativeRun" depends="nativeImage">
17851806
<exec executable="${basedir}/samples/graalvm-native-jna/build/native/nativeCompile/graalvm-native-jna" dir="${basedir}/samples/graalvm-native-jna" failonerror="true">
17861807
<arg value="testing-123"/>
17871808
</exec>
17881809
</target>
1810+
1811+
<target name="nativeRunStatic" depends="nativeImageStatic">
1812+
<exec executable="${basedir}/samples/graalvm-native-static-jna/build/native/nativeCompile/graalvm-native-static-jna" dir="${basedir}/samples/graalvm-native-jna" failonerror="true">
1813+
<arg value="testing-123"/>
1814+
</exec>
1815+
</target>
17891816
</project>

lib/gvm/SubstrateStaticJNA.java

+154-1
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,18 @@
2323
*/
2424
package com.sun.jna;
2525

26+
import com.oracle.svm.core.jdk.NativeLibrarySupport;
27+
import com.oracle.svm.core.jdk.PlatformNativeLibrarySupport;
28+
import com.oracle.svm.hosted.FeatureImpl.BeforeAnalysisAccessImpl;
29+
import org.graalvm.nativeimage.Platform;
2630
import org.graalvm.nativeimage.hosted.Feature;
2731

32+
import java.io.File;
33+
import java.io.IOException;
34+
import java.nio.file.Files;
35+
import java.util.Collections;
36+
import java.util.List;
37+
2838
/**
2939
* Feature for use at build time on GraalVM, which enables static JNI support for JNA.
3040
*
@@ -36,8 +46,125 @@
3646
*
3747
* <p>This class extends the base {@link com.sun.jna.JavaNativeAccess} feature by providing JNA's JNI layer statically,
3848
* so that no library unpacking step needs to take place.
49+
*
50+
* @since 5.15.0
51+
* @author Sam Gammon ([email protected])
52+
* @author Dario Valdespino ([email protected])
3953
*/
4054
public final class SubstrateStaticJNA extends AbstractJNAFeature {
55+
/**
56+
* Name for the FFI native library used during static linking by Native Image.
57+
*/
58+
private static final String FFI_LINK_NAME = "ffi";
59+
60+
/**
61+
* Name for the JNI Dispatch native library used during static linking by Native Image.
62+
*/
63+
private static final String JNA_LINK_NAME = "jnidispatch";
64+
65+
/**
66+
* Name prefix used by native functions from the JNI Dispatch library.
67+
*/
68+
private static final String JNA_NATIVE_LAYOUT = "com_sun_jna_Native";
69+
70+
/**
71+
* Name of the JNI Dispatch static library on UNIX-based platforms.
72+
*/
73+
private static final String JNI_DISPATCH_UNIX_NAME = "libjnidispatch.a";
74+
75+
/**
76+
* Name of the JNI Dispatch static library on Windows.
77+
*/
78+
private static final String JNI_DISPATCH_WINDOWS_NAME = "jnidispatch.lib";
79+
80+
/**
81+
* Name of the FFI static library on UNIX-based platforms.
82+
*/
83+
private static final String FFI_UNIX_NAME = "libffi.a";
84+
85+
/**
86+
* Name of the FFI static library on Windows.
87+
*/
88+
private static final String FFI_WINDOWS_NAME = "ffi.lib";
89+
90+
/**
91+
* Returns the name of the static JNI Dispatch library for the current platform. On UNIX-based systems,
92+
* {@link #JNI_DISPATCH_UNIX_NAME} is used; on Windows, {@link #JNI_DISPATCH_WINDOWS_NAME} is returned instead.
93+
*
94+
* @see #getStaticLibraryResource
95+
* @return The JNI Dispatch library name for the current platform.
96+
*/
97+
private static String getStaticLibraryFileName() {
98+
if (Platform.includedIn(Platform.WINDOWS.class)) return JNI_DISPATCH_WINDOWS_NAME;
99+
if (Platform.includedIn(Platform.LINUX.class)) return JNI_DISPATCH_UNIX_NAME;
100+
if (Platform.includedIn(Platform.DARWIN.class)) return JNI_DISPATCH_UNIX_NAME;
101+
102+
// If the current platform is not in the Platform class, this code would not run at all
103+
throw new UnsupportedOperationException("Current platform does not support static linking");
104+
}
105+
106+
/**
107+
* Returns the name of the static FFI library for the current platform. On UNIX-based systems,
108+
* {@link #FFI_UNIX_NAME} is used; on Windows, {@link #FFI_WINDOWS_NAME} is returned instead.
109+
*
110+
* @see #getStaticLibraryResource
111+
* @return The FFI library name for the current platform.
112+
*/
113+
private static String getFFILibraryFileName() {
114+
if (Platform.includedIn(Platform.WINDOWS.class)) return FFI_WINDOWS_NAME;
115+
if (Platform.includedIn(Platform.LINUX.class)) return FFI_UNIX_NAME;
116+
if (Platform.includedIn(Platform.DARWIN.class)) return FFI_UNIX_NAME;
117+
118+
// If the current platform is not in the Platform class, this code would not run at all
119+
throw new UnsupportedOperationException("Current platform does not support static FFI");
120+
}
121+
122+
/**
123+
* Returns the full path to the static JNI Dispatch library embedded in the JAR, accounting for platform-specific
124+
* library names.
125+
*
126+
* @see #getStaticLibraryFileName()
127+
* @return The JNI Dispatch library resource path for the current platform.
128+
*/
129+
private static String getStaticLibraryResource() {
130+
return "/com/sun/jna/" + com.sun.jna.Platform.RESOURCE_PREFIX + "/" + getStaticLibraryFileName();
131+
}
132+
133+
/**
134+
* Returns the full path to the static FFI library which JNA depends on, accounting for platform-specific
135+
* library names.
136+
*
137+
* @see #getFFILibraryFileName()
138+
* @return The FFI library resource path for the current platform.
139+
*/
140+
private static String getFFILibraryResource() {
141+
return "/com/sun/jna/" + com.sun.jna.Platform.RESOURCE_PREFIX + "/" + getFFILibraryFileName();
142+
}
143+
144+
/**
145+
* Extracts a library resource and returns the file it was extracted to.
146+
*
147+
* @param resource Resource path for the library to extract.
148+
* @param filename Expected filename for the library.
149+
* @return The extracted library file.
150+
*/
151+
private static File unpackLibrary(String resource, String filename) {
152+
// Unpack the static library from resources so Native Image can use it
153+
File extractedLib;
154+
try {
155+
extractedLib = Native.extractFromResourcePath(resource, Native.class.getClassLoader());
156+
157+
// The library is extracted into a file with a `.tmp` name, which will not be picked up by the linker
158+
// We need to rename it first using the platform-specific convention or the build will fail
159+
File platformLib = new File(extractedLib.getParentFile(), filename);
160+
if (!extractedLib.renameTo(platformLib)) throw new IllegalStateException("Renaming extract file failed");
161+
extractedLib = platformLib;
162+
} catch (IOException e) {
163+
throw new RuntimeException("Failed to extract native dispatch library from resources", e);
164+
}
165+
return extractedLib;
166+
}
167+
41168
@Override
42169
public String getDescription() {
43170
return "Enables optimized static access to JNA at runtime";
@@ -48,8 +175,34 @@ public boolean isInConfiguration(IsInConfigurationAccess access) {
48175
return access.findClassByName(JavaNativeAccess.NATIVE_LAYOUT) != null;
49176
}
50177

178+
@Override
179+
public List<Class<? extends Feature>> getRequiredFeatures() {
180+
return Collections.singletonList(JavaNativeAccess.class);
181+
}
182+
51183
@Override
52184
public void beforeAnalysis(BeforeAnalysisAccess access) {
53-
//
185+
var nativeLibraries = NativeLibrarySupport.singleton();
186+
var platformLibraries = PlatformNativeLibrarySupport.singleton();
187+
188+
// Register as a built-in library with Native Image and set the name prefix used by native symbols
189+
nativeLibraries.preregisterUninitializedBuiltinLibrary(JNA_LINK_NAME);
190+
platformLibraries.addBuiltinPkgNativePrefix(JNA_NATIVE_LAYOUT);
191+
192+
// Extract the main JNA library from the platform-specific resource path; next, extract the FFI
193+
// library it depends on
194+
unpackLibrary(getFFILibraryResource(), getFFILibraryFileName());
195+
var extractedLib = unpackLibrary(getStaticLibraryResource(), getStaticLibraryFileName());
196+
197+
// WARNING: the static JNI linking feature is unstable and may be removed in the future;
198+
// this code uses the access implementation directly in order to register the static library. We
199+
// inform the Native Image compiler that JNA depends on `ffi`, so that it forces it to load first
200+
// when JNA is initialized at image runtime.
201+
var nativeLibsImpl = ((BeforeAnalysisAccessImpl) access).getNativeLibraries();
202+
nativeLibsImpl.addStaticNonJniLibrary(FFI_LINK_NAME);
203+
nativeLibsImpl.addStaticJniLibrary(JNA_LINK_NAME, FFI_LINK_NAME);
204+
205+
// Enhance the Native Image lib paths so the injected static libraries are available to the linker
206+
nativeLibsImpl.getLibraryPaths().add(extractedLib.getParentFile().getAbsolutePath());
54207
}
55208
}

lib/gvm/native-image.properties

+6-1
Original file line numberDiff line numberDiff line change
@@ -21,4 +21,9 @@
2121
# A copy is also included in the downloadable source code package
2222
# containing JNA, in file "AL2.0".
2323

24-
Args = --features=com.sun.jna.JavaNativeAccess
24+
Args = --features=com.sun.jna.JavaNativeAccess \
25+
-J--add-exports=org.graalvm.nativeimage.builder/com.oracle.svm.hosted.jni=ALL-UNNAMED \
26+
-J--add-exports=org.graalvm.nativeimage.builder/com.oracle.svm.core.jni=ALL-UNNAMED \
27+
-J--add-exports=org.graalvm.nativeimage.builder/com.oracle.svm.hosted=ALL-UNNAMED \
28+
-J--add-exports=org.graalvm.nativeimage.builder/com.oracle.svm.hosted.c=ALL-UNNAMED \
29+
-J--add-exports=org.graalvm.nativeimage.builder/com.oracle.svm.core.jdk=ALL-UNNAMED

samples/README.md

+7-1
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,10 @@
22

33
This directory contains sample projects that use JNA in different ways. See below for a list of available samples:
44

5-
- **GraalVM Native JNA:** Builds a GraalVM native image using JNA features with Gradle.
5+
- **[GraalVM Native JNA][0]:** Builds a GraalVM native image using JNA features with Gradle.
6+
7+
- **[Graalvm Native JNA (Static)][1]:** Uses the [SubstrateStaticJNA](../lib/gvm/SubstrateStaticJNA.java) feature to build
8+
JNA code statically into the Native Image.
9+
10+
[0]: ./graalvm-native-jna
11+
[1]: ./graalvm-native-static-jna
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
/.gradle
2+
/build
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
# JNA Sample: GraalVM Native Image (Static)
2+
3+
This directory contains a sample Gradle project which uses JNA with [GraalVM](https://graalvm.org/). The project builds a
4+
[native image](https://www.graalvm.org/latest/reference-manual/native-image/) which uses JNA features, powered by JNA's integration library for Substrate.
5+
6+
This sample leverages [Static JNI](https://www.blog.akhil.cc/static-jni) to build JNA and JNA-related user code
7+
directly into the native image.
8+
9+
Using this technique can optimize startup time and other performance factors, because no dynamic library unpack-and-load
10+
step is required to use JNA.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
/* Copyright (c) 2015 Adam Marcionek, All Rights Reserved
2+
*
3+
* The contents of this file is dual-licensed under 2
4+
* alternative Open Source/Free licenses: LGPL 2.1 or later and
5+
* Apache License 2.0. (starting with JNA version 4.0.0).
6+
*
7+
* You can freely decide which license you want to apply to
8+
* the project.
9+
*
10+
* You may obtain a copy of the LGPL License at:
11+
*
12+
* http://www.gnu.org/licenses/licenses.html
13+
*
14+
* A copy is also included in the downloadable source code package
15+
* containing JNA, in file "LGPL2.1".
16+
*
17+
* You may obtain a copy of the Apache License at:
18+
*
19+
* http://www.apache.org/licenses/
20+
*
21+
* A copy is also included in the downloadable source code package
22+
* containing JNA, in file "AL2.0".
23+
*/
24+
plugins {
25+
java
26+
application
27+
alias(libs.plugins.graalvm)
28+
}
29+
30+
application {
31+
mainClass = "com.example.JnaNative"
32+
}
33+
34+
java {
35+
toolchain {
36+
languageVersion = JavaLanguageVersion.of(22)
37+
vendor = JvmVendorSpec.GRAAL_VM
38+
}
39+
}
40+
41+
dependencies {
42+
implementation(libs.bundles.jna)
43+
implementation(libs.bundles.graalvm.api)
44+
nativeImageClasspath(libs.jna.graalvm)
45+
}
46+
47+
val nativeImageDebug: String by properties
48+
49+
graalvmNative {
50+
testSupport = true
51+
toolchainDetection = false
52+
53+
binaries {
54+
named("main") {
55+
buildArgs.addAll(listOf(
56+
"--features=com.sun.jna.SubstrateStaticJNA",
57+
).plus(if (nativeImageDebug != "true") emptyList() else listOf(
58+
"--verbose",
59+
"--debug-attach",
60+
"-J-Xlog:library=info",
61+
"-H:+UnlockExperimentalVMOptions",
62+
"-H:+JNIEnhancedErrorCodes",
63+
"-H:+SourceLevelDebug",
64+
"-H:-DeleteLocalSymbols",
65+
"-H:-RemoveUnusedSymbols",
66+
"-H:+PreserveFramePointer",
67+
"-H:+ReportExceptionStackTraces",
68+
"-H:CCompilerOption=-v",
69+
"-H:NativeLinkerOption=-v",
70+
)))
71+
}
72+
}
73+
}
74+
75+
// Allow the outer Ant build to override the version of JNA or GraalVM.
76+
// These properties are used in JNA's CI and don't need to be in projects that use JNA.
77+
78+
val jnaVersion: String by properties
79+
val graalvmVersion: String by properties
80+
val overrides = jnaVersion.isNotBlank() || graalvmVersion.isNotBlank()
81+
82+
if (overrides) configurations.all {
83+
resolutionStrategy.eachDependency {
84+
if (requested.group == "net.java.dev.jna") {
85+
useVersion(jnaVersion)
86+
because("overridden by ant build")
87+
}
88+
if (requested.group == "org.graalvm") {
89+
useVersion(graalvmVersion)
90+
because("overridden by ant build")
91+
}
92+
}
93+
}

0 commit comments

Comments
 (0)