Skip to content

Commit

Permalink
Support JNA over Static JNI on GraalVM
Browse files Browse the repository at this point in the history
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 <sam@elide.ventures>
Signed-off-by: Dario Valdespino <dario@elide.ventures>
Co-authored-by: Sam Gammon <sam@elide.ventures>
Co-authored-by: Dario Valdespino <dario@elide.ventures>
  • Loading branch information
sgammon and darvld committed Jun 13, 2024
1 parent d7467e2 commit 7fe4da6
Show file tree
Hide file tree
Showing 17 changed files with 838 additions and 9 deletions.
8 changes: 6 additions & 2 deletions .github/workflows/graalvm.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -30,5 +30,9 @@ jobs:
- name: Linux requirements
run: sudo apt-get -y install texinfo
- uses: gradle/actions/setup-gradle@v3
- name: "Build: Native Image"
run: ant dist && ant install && ant nativeImage && ant nativeRun
- name: "Build: Compile & Install JNA"
run: ant && ant install
- name: "Build: Native Images (Dynamic JNI)"
run: ant nativeImage && ant nativeRun
- name: "Build: Native Image (Static JNI)"
run: ant nativeImageStatic && ant nativeRunStatic
35 changes: 31 additions & 4 deletions build.xml
Original file line number Diff line number Diff line change
Expand Up @@ -292,9 +292,9 @@
<artifact:dependencies pathId="graalvm.classpath">
<remoteRepository refid="central" />
<dependency groupId="org.graalvm.sdk" artifactId="nativeimage" version="${graalvm.version}"/>
<dependency groupId="org.graalvm.nativeimage" artifactId="svm" version="${graalvm.version}"/>
</artifact:dependencies>

<delete dir="${classes-graalvm}" />
<mkdir dir="${classes-graalvm}" />
<delete dir="${build}/gvm-src" />
<mkdir dir="${build}/gvm-src/com/sun/jna" />
Expand Down Expand Up @@ -656,9 +656,6 @@ osname=macosx;processor=aarch64
<zipfileset src="${build}/${jar}" excludes="META-INF/MANIFEST.mf"/>
<zipfileset dir="${build}/manifest/" includes="module-info.class" prefix="META-INF/versions/9"/>
</jar>
<jar jarfile="${build}/jna-graalvm.jar" duplicate="preserve" createUnicodeExtraFields="never" encoding="UTF-8">
<zipfileset src="${build}/${jar-graalvm}" excludes="META-INF/MANIFEST.mf"/>
</jar>
</target>

<target name="aar" depends="jar" description="Build Android Archive">
Expand Down Expand Up @@ -1123,6 +1120,8 @@ cd ..
<copy todir="${classes-graalvm}/${native.path}">
<fileset dir="${build.native}"
includes="jnidispatch.dll,libjnidispatch.*"/>
<fileset dir="${build.native}/libffi/.libs"
includes="ffi.lib,libffi.a"/>
</copy>
<!-- For web start, native libraries may be provided in the root of -->
<!-- an included jar file -->
Expand All @@ -1135,6 +1134,7 @@ cd ..
</jar>
<jar jarfile="${build}/${native-static.jar}" createUnicodeExtraFields="never" encoding="UTF-8">
<fileset dir="${build.native}" includes="jnidispatch.lib,libjnidispatch.a"/>
<fileset dir="${build.native}/libffi/.libs" includes="libffi.a"/>
<manifest>
<attribute name="Implementation-Version" value="${jni.version} b${jni.build}"/>
<attribute name="Specification-Version" value="${jni.version}"/>
Expand Down Expand Up @@ -1781,9 +1781,36 @@ cd ..
</exec>
</target>

<target name="nativeImageStatic">
<exec executable="/bin/bash" dir="${basedir}/samples/graalvm-native-static-jna" failonerror="true">
<arg value="${basedir}/samples/graalvm-native-static-jna/gradlew"/>
<arg value="-PjnaVersion=${jna.version}"/>
<arg value="-PgraalvmVersion=${graalvm.version}"/>
<arg value="build"/>
<arg value="nativeCompile"/>
</exec>
</target>

<target name="nativeImageStaticDebug">
<exec executable="/bin/bash" dir="${basedir}/samples/graalvm-native-static-jna" failonerror="true">
<arg value="${basedir}/samples/graalvm-native-static-jna/gradlew"/>
<arg value="-PjnaVersion=${jna.version}"/>
<arg value="-PgraalvmVersion=${graalvm.version}"/>
<arg value="-PnativeImageDebug=true"/>
<arg value="build"/>
<arg value="nativeCompile"/>
</exec>
</target>

<target name="nativeRun" depends="nativeImage">
<exec executable="${basedir}/samples/graalvm-native-jna/build/native/nativeCompile/graalvm-native-jna" dir="${basedir}/samples/graalvm-native-jna" failonerror="true">
<arg value="testing-123"/>
</exec>
</target>

<target name="nativeRunStatic" depends="nativeImageStatic">
<exec executable="${basedir}/samples/graalvm-native-static-jna/build/native/nativeCompile/graalvm-native-static-jna" dir="${basedir}/samples/graalvm-native-jna" failonerror="true">
<arg value="testing-123"/>
</exec>
</target>
</project>
155 changes: 154 additions & 1 deletion lib/gvm/SubstrateStaticJNA.java
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,18 @@
*/
package com.sun.jna;

import com.oracle.svm.core.jdk.NativeLibrarySupport;
import com.oracle.svm.core.jdk.PlatformNativeLibrarySupport;
import com.oracle.svm.hosted.FeatureImpl.BeforeAnalysisAccessImpl;
import org.graalvm.nativeimage.Platform;
import org.graalvm.nativeimage.hosted.Feature;

import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.util.Collections;
import java.util.List;

/**
* Feature for use at build time on GraalVM, which enables static JNI support for JNA.
*
Expand All @@ -36,8 +46,125 @@
*
* <p>This class extends the base {@link com.sun.jna.JavaNativeAccess} feature by providing JNA's JNI layer statically,
* so that no library unpacking step needs to take place.
*
* @since 5.15.0
* @author Sam Gammon (sam@elide.dev)
* @author Dario Valdespino (dario@elide.dev)
*/
public final class SubstrateStaticJNA extends AbstractJNAFeature {
/**
* Name for the FFI native library used during static linking by Native Image.
*/
private static final String FFI_LINK_NAME = "ffi";

/**
* Name for the JNI Dispatch native library used during static linking by Native Image.
*/
private static final String JNA_LINK_NAME = "jnidispatch";

/**
* Name prefix used by native functions from the JNI Dispatch library.
*/
private static final String JNA_NATIVE_LAYOUT = "com_sun_jna_Native";

/**
* Name of the JNI Dispatch static library on UNIX-based platforms.
*/
private static final String JNI_DISPATCH_UNIX_NAME = "libjnidispatch.a";

/**
* Name of the JNI Dispatch static library on Windows.
*/
private static final String JNI_DISPATCH_WINDOWS_NAME = "jnidispatch.lib";

/**
* Name of the FFI static library on UNIX-based platforms.
*/
private static final String FFI_UNIX_NAME = "libffi.a";

/**
* Name of the FFI static library on Windows.
*/
private static final String FFI_WINDOWS_NAME = "ffi.lib";

/**
* Returns the name of the static JNI Dispatch library for the current platform. On UNIX-based systems,
* {@link #JNI_DISPATCH_UNIX_NAME} is used; on Windows, {@link #JNI_DISPATCH_WINDOWS_NAME} is returned instead.
*
* @see #getStaticLibraryResource
* @return The JNI Dispatch library name for the current platform.
*/
private static String getStaticLibraryFileName() {
if (Platform.includedIn(Platform.WINDOWS.class)) return JNI_DISPATCH_WINDOWS_NAME;
if (Platform.includedIn(Platform.LINUX.class)) return JNI_DISPATCH_UNIX_NAME;
if (Platform.includedIn(Platform.DARWIN.class)) return JNI_DISPATCH_UNIX_NAME;

// If the current platform is not in the Platform class, this code would not run at all
throw new UnsupportedOperationException("Current platform does not support static linking");
}

/**
* Returns the name of the static FFI library for the current platform. On UNIX-based systems,
* {@link #FFI_UNIX_NAME} is used; on Windows, {@link #FFI_WINDOWS_NAME} is returned instead.
*
* @see #getStaticLibraryResource
* @return The FFI library name for the current platform.
*/
private static String getFFILibraryFileName() {
if (Platform.includedIn(Platform.WINDOWS.class)) return FFI_WINDOWS_NAME;
if (Platform.includedIn(Platform.LINUX.class)) return FFI_UNIX_NAME;
if (Platform.includedIn(Platform.DARWIN.class)) return FFI_UNIX_NAME;

// If the current platform is not in the Platform class, this code would not run at all
throw new UnsupportedOperationException("Current platform does not support static FFI");
}

/**
* Returns the full path to the static JNI Dispatch library embedded in the JAR, accounting for platform-specific
* library names.
*
* @see #getStaticLibraryFileName()
* @return The JNI Dispatch library resource path for the current platform.
*/
private static String getStaticLibraryResource() {
return "/com/sun/jna/" + com.sun.jna.Platform.RESOURCE_PREFIX + "/" + getStaticLibraryFileName();
}

/**
* Returns the full path to the static FFI library which JNA depends on, accounting for platform-specific
* library names.
*
* @see #getFFILibraryFileName()
* @return The FFI library resource path for the current platform.
*/
private static String getFFILibraryResource() {
return "/com/sun/jna/" + com.sun.jna.Platform.RESOURCE_PREFIX + "/" + getFFILibraryFileName();
}

/**
* Extracts a library resource and returns the file it was extracted to.
*
* @param resource Resource path for the library to extract.
* @param filename Expected filename for the library.
* @return The extracted library file.
*/
private static File unpackLibrary(String resource, String filename) {
// Unpack the static library from resources so Native Image can use it
File extractedLib;
try {
extractedLib = Native.extractFromResourcePath(resource, Native.class.getClassLoader());

// The library is extracted into a file with a `.tmp` name, which will not be picked up by the linker
// We need to rename it first using the platform-specific convention or the build will fail
File platformLib = new File(extractedLib.getParentFile(), filename);
if (!extractedLib.renameTo(platformLib)) throw new IllegalStateException("Renaming extract file failed");
extractedLib = platformLib;
} catch (IOException e) {
throw new RuntimeException("Failed to extract native dispatch library from resources", e);
}
return extractedLib;
}

@Override
public String getDescription() {
return "Enables optimized static access to JNA at runtime";
Expand All @@ -48,8 +175,34 @@ public boolean isInConfiguration(IsInConfigurationAccess access) {
return access.findClassByName(JavaNativeAccess.NATIVE_LAYOUT) != null;
}

@Override
public List<Class<? extends Feature>> getRequiredFeatures() {
return Collections.singletonList(JavaNativeAccess.class);
}

@Override
public void beforeAnalysis(BeforeAnalysisAccess access) {
//
var nativeLibraries = NativeLibrarySupport.singleton();
var platformLibraries = PlatformNativeLibrarySupport.singleton();

// Register as a built-in library with Native Image and set the name prefix used by native symbols
nativeLibraries.preregisterUninitializedBuiltinLibrary(JNA_LINK_NAME);
platformLibraries.addBuiltinPkgNativePrefix(JNA_NATIVE_LAYOUT);

// Extract the main JNA library from the platform-specific resource path; next, extract the FFI
// library it depends on
unpackLibrary(getFFILibraryResource(), getFFILibraryFileName());
var extractedLib = unpackLibrary(getStaticLibraryResource(), getStaticLibraryFileName());

// WARNING: the static JNI linking feature is unstable and may be removed in the future;
// this code uses the access implementation directly in order to register the static library. We
// inform the Native Image compiler that JNA depends on `ffi`, so that it forces it to load first
// when JNA is initialized at image runtime.
var nativeLibsImpl = ((BeforeAnalysisAccessImpl) access).getNativeLibraries();
nativeLibsImpl.addStaticNonJniLibrary(FFI_LINK_NAME);
nativeLibsImpl.addStaticJniLibrary(JNA_LINK_NAME, FFI_LINK_NAME);

// Enhance the Native Image lib paths so the injected static libraries are available to the linker
nativeLibsImpl.getLibraryPaths().add(extractedLib.getParentFile().getAbsolutePath());
}
}
7 changes: 6 additions & 1 deletion lib/gvm/native-image.properties
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,9 @@
# A copy is also included in the downloadable source code package
# containing JNA, in file "AL2.0".

Args = --features=com.sun.jna.JavaNativeAccess
Args = --features=com.sun.jna.JavaNativeAccess \
-J--add-exports=org.graalvm.nativeimage.builder/com.oracle.svm.hosted.jni=ALL-UNNAMED \
-J--add-exports=org.graalvm.nativeimage.builder/com.oracle.svm.core.jni=ALL-UNNAMED \
-J--add-exports=org.graalvm.nativeimage.builder/com.oracle.svm.hosted=ALL-UNNAMED \
-J--add-exports=org.graalvm.nativeimage.builder/com.oracle.svm.hosted.c=ALL-UNNAMED \
-J--add-exports=org.graalvm.nativeimage.builder/com.oracle.svm.core.jdk=ALL-UNNAMED
8 changes: 7 additions & 1 deletion samples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,10 @@

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

- **GraalVM Native JNA:** Builds a GraalVM native image using JNA features with Gradle.
- **[GraalVM Native JNA][0]:** Builds a GraalVM native image using JNA features with Gradle.

- **[Graalvm Native JNA (Static)][1]:** Uses the [SubstrateStaticJNA](../lib/gvm/SubstrateStaticJNA.java) feature to build
JNA code statically into the Native Image.

[0]: ./graalvm-native-jna
[1]: ./graalvm-native-static-jna
2 changes: 2 additions & 0 deletions samples/graalvm-native-static-jna/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
/.gradle
/build
10 changes: 10 additions & 0 deletions samples/graalvm-native-static-jna/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# JNA Sample: GraalVM Native Image (Static)

This directory contains a sample Gradle project which uses JNA with [GraalVM](https://graalvm.org/). The project builds a
[native image](https://www.graalvm.org/latest/reference-manual/native-image/) which uses JNA features, powered by JNA's integration library for Substrate.

This sample leverages [Static JNI](https://www.blog.akhil.cc/static-jni) to build JNA and JNA-related user code
directly into the native image.

Using this technique can optimize startup time and other performance factors, because no dynamic library unpack-and-load
step is required to use JNA.
93 changes: 93 additions & 0 deletions samples/graalvm-native-static-jna/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
/* Copyright (c) 2015 Adam Marcionek, All Rights Reserved
*
* The contents of this file is dual-licensed under 2
* alternative Open Source/Free licenses: LGPL 2.1 or later and
* Apache License 2.0. (starting with JNA version 4.0.0).
*
* You can freely decide which license you want to apply to
* the project.
*
* You may obtain a copy of the LGPL License at:
*
* http://www.gnu.org/licenses/licenses.html
*
* A copy is also included in the downloadable source code package
* containing JNA, in file "LGPL2.1".
*
* You may obtain a copy of the Apache License at:
*
* http://www.apache.org/licenses/
*
* A copy is also included in the downloadable source code package
* containing JNA, in file "AL2.0".
*/
plugins {
java
application
alias(libs.plugins.graalvm)
}

application {
mainClass = "com.example.JnaNative"
}

java {
toolchain {
languageVersion = JavaLanguageVersion.of(22)
vendor = JvmVendorSpec.GRAAL_VM
}
}

dependencies {
implementation(libs.bundles.jna)
implementation(libs.bundles.graalvm.api)
nativeImageClasspath(libs.jna.graalvm)
}

val nativeImageDebug: String by properties

graalvmNative {
testSupport = true
toolchainDetection = false

binaries {
named("main") {
buildArgs.addAll(listOf(
"--features=com.sun.jna.SubstrateStaticJNA",
).plus(if (nativeImageDebug != "true") emptyList() else listOf(
"--verbose",
"--debug-attach",
"-J-Xlog:library=info",
"-H:+UnlockExperimentalVMOptions",
"-H:+JNIEnhancedErrorCodes",
"-H:+SourceLevelDebug",
"-H:-DeleteLocalSymbols",
"-H:-RemoveUnusedSymbols",
"-H:+PreserveFramePointer",
"-H:+ReportExceptionStackTraces",
"-H:CCompilerOption=-v",
"-H:NativeLinkerOption=-v",
)))
}
}
}

// Allow the outer Ant build to override the version of JNA or GraalVM.
// These properties are used in JNA's CI and don't need to be in projects that use JNA.

val jnaVersion: String by properties
val graalvmVersion: String by properties
val overrides = jnaVersion.isNotBlank() || graalvmVersion.isNotBlank()

if (overrides) configurations.all {
resolutionStrategy.eachDependency {
if (requested.group == "net.java.dev.jna") {
useVersion(jnaVersion)
because("overridden by ant build")
}
if (requested.group == "org.graalvm") {
useVersion(graalvmVersion)
because("overridden by ant build")
}
}
}
Loading

0 comments on commit 7fe4da6

Please sign in to comment.