Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

JNA: Support GraalVM #1608

Open
wants to merge 4 commits into
base: master
Choose a base branch
from

Conversation

sgammon
Copy link

@sgammon sgammon commented Jun 10, 2024

Summary

Adds a JAR publication at jna-graalvm.jar, with accompanying build infrastructure, that provides support for JNA within the context of the Substrate Virtual Machine (SVM).

JNA is already possible on SVM today, but requires extensive (and app-specific) configuration, which can end up being brittle. If methods aren't caught for configuration at build-time, dispatch at runtime can throw. This PR ships automatic configuration support for GVM to JNA itself, as an optional add-on.

Features

  • Automatic configuration of JNA under GraalVM native image
  • Detection and configuration of user types
  • CI and sample project for GraalVM+JNA
  • Experimental feature for building JNA code directly into the image

Usage

  1. Developer adds new jna-graalvm.jar to their native-image classpath
  2. Developer uses JNA
  3. That's it
  4. Optionally, the developer enables static JNA with --features=com.sun.jna.SubstrateStaticJNA

In addition to baseline configurations required for any use at all of JNA, the base feature leverages GraalVM's analysis to detect when a developer is using JNA features, and then registers configurations appropriately for their classes, too.

For example, when the developer writes:

    public interface CLibrary extends Library {
        CLibrary INSTANCE = (CLibrary)
            Native.load((Platform.isWindows() ? "msvcrt" : "c"),
                                CLibrary.class);

        void printf(String format, Object... args);
    }

... then CLibrary is registered as a dynamic proxy with GraalVM automatically.

Trying it out

Tip

See this comment to try this out in an existing codebase without building from source.

# make sure you have a recent version of graalvm set at GRAALVM_HOME
git clone [email protected]:elide-dev/jna.git -b feat/static-graalvm-jna
cd jna && ant && ant dist && ant install && ant nativeImage && ant nativeRun

You should see:

...
     [exec] ========================================================================================================================
     [exec] GraalVM Native Image: Generating 'graalvm-native-jna' (executable)...
     [exec] ========================================================================================================================
     [exec] For detailed information and explanations on the build output, visit:
     [exec] https://github.com/oracle/graal/blob/master/docs/reference-manual/native-image/BuildOutput.md
     [exec] ------------------------------------------------------------------------------------------------------------------------
     [exec] [1/8] Initializing...                                                                                    (4.8s @ 0.12GB)
     [exec]  Java version: 22.0.1+8, vendor version: Oracle GraalVM 22.0.1+8.1
     [exec]  Graal compiler: optimization level: 2, target machine: x86-64-v3, PGO: ML-inferred
     [exec]  C compiler: gcc (linux, x86_64, 12.2.0)
     [exec]  Garbage collector: Serial GC (max heap size: 80% of RAM)
     [exec]  2 user-specific feature(s):
     [exec]  - com.oracle.svm.thirdparty.gson.GsonFeature
→    [exec]  - com.sun.jna.JavaNativeAccess: Enables access to JNA at runtime on SubstrateVM
...
nativeRun:
     [exec] Hello, JNA!
     [exec] Argument 0: testing-123

BUILD SUCCESSFUL
Total time: 1 second

Rationale

GraalVM Native Image targets use SVM instead of JVM at runtime. JNA's current strategy of unpacking libraries at runtime works under SVM, but is suboptimal; the binary is native, so it can simply include JNA object code directly. A configuration-only approach also leaves JNA code brittle, because configuration must be specified for all JNA touchpoints used by the app, and must stay in sync over time.

To accomplish automatic configuration, several GraalVM "feature" implementations are provided in this new publication. By default, regular JNA access is enabled through the JavaNativeAccess feature; this class enables reflection and runtime JNI configurations for downstream projects which use JNA.

Another feature, SubstrateStaticJNA, is experimental because it relies on unstable GraalVM APIs, but instead of loading JNA at runtime from a dynamic library, it builds JNA into the final native image with a static object.

These features are enabled through a resource within META-INF, called native-image.properties, which is picked up by the native image compiler at build time. The new artifact only needs to be present for GraalVM native targets at build time; otherwise, the classes and libraries in jna-graalvm.jar are inert.

Approach

---
title: "SVM + JNA"
---
classDiagram
    note for AbstractJNAFeature "Base Feature"
    note for JavaNativeAccess "Baseline Configurations"
    note for StaticJNAFeature "Static JNI Linkage"
    AbstractJNAFeature --|> JavaNativeAccess
    AbstractJNAFeature --|> StaticJNAFeature
    JavaNativeAccess --> UserCode: Detects

    class AbstractJNAFeature {
      Common Logic*
      --
      JNI/Proxy Registration
      Library Resolution + Unpacking
    }
    class JavaNativeAccess {
      Always Active*
      No change to current behavior*
      --
      Runtime JNI Registration
      Runtime Proxy Registration
      Subtype Reachability Handler
    }
    class StaticJNAFeature {
      Active On-Demand*
      --
      Unpacks Static Library at Build Time
      Enables Static JNI Linkage
    }
    class UserCode {
        class X extends Library ...
    }
Loading

Abstract Base

This new class is package-private and provides protected utilities for use exclusively by GraalVM Feature implementations; for unfamiliar readers, Features are build-time classes that contribute to compiler configuration.

Common logic provided:

  • Finding the static native library within the jna-graalvm.jar resource
  • Unpacking the static library to a location on-disk, as is normally done at runtime
  • Common routines for registration of runtime JNI and proxy access

JavaNativeAccess feature

sequenceDiagram
    Native Image->>+Feature: Detects `native-image.properties`, feature self-mounts
    Feature->>+Native Image: Registers reachability handler and common configurations
    Native Image->>User Code: Analysis phase begins
    User Code->>Native Image: class X extends Library { ... }
    Native Image->>-Feature: User extended Library with class X
    Feature->>-Native Image: Register class X as proxy
Loading

This feature is designed to be registered unconditionally in a downstream native-image build; it is found automatically via the native-image.properties resource, which declares it via --features=...JavaNativeAccess. Thus, (1) having the jna-graalvm.jar on your build-time classpath and (2) using JNA is enough to activate the feature.

Configurations are contributed by this feature which always apply to JNA in the context of Substrate, native image's equivalent to JVM: these include runtime JNI access and proxy access, both of which must be declared ahead of time in GraalVM's AOT mode.

What it does:

  • Registers classes necessary for runtime JNI access to JNA
  • Registers necessary proxy interface access for proper operation of JNA
  • Registers native library resources which should be persisted within the image

How to use it:

  • Add jna-graalvm.jar to your build-time classpath for native-image

Note

Many projects register these same configurations within [proxy,jni,reflect]-config.json files in their project; these configuration files can be cleaned up downstream once this feature becomes available. Users no longer have to generate this configuration themselves. Extra configurations are inert.


SubstrateStaticJNA feature

JNIArch

This feature is experimental, because it relies on unstable APIs within GraalVM's native image SDK1. Through a technique known as Static JNI2, the precursor library unpacking step normally needed for JNA's operation can be eliminated entirely. Instead of steps taken at runtime, a static library is unpacked at build time, and built directly into the user's native image.

This has many advantages: the library unpack step is no longer needed and so startup time in JNA-consuming apps is reduced; artifact size is reduced, since native libraries are no longer bundles as resources, and potentially compressed without benefit. Since the binary targets native code, unused variants of libjnidispatch.[so,dylib,dll] can be omitted, resulting in smaller image sizes.

The StaticJNAFeature is designed to interoperate with the JavaNativeAccess feature. The two can be used in a build together, or StaticJNAFeature can be omitted to preserve the current library behavior. This feature is opt-in and is not injected by the native-image.properties file.

What it does:

  • Unpacks the new static library, [lib]jnidispatch.[a,lib], at build time, according to current JNA behavior
  • Configures Native Image to understand JNA's jnidispatch as a static JNI "built-in"

How to use it:

  • Add jna-graalvm.jar to your build-time classpath for native-image
  • Specify the argument native-image ... --features=com.sun.jna.SubstrateStaticJNA

Caution

Obligatory warning that this is an experimental and unstable technique. Please don't rely on it for production use. Once oracle/graal#3359 is fixed, this feature can ship as default.

Footnotes

  1. https://github.com/oracle/graal/issues/3359

  2. https://www.blog.akhil.cc/static-jni

@sgammon

This comment was marked as outdated.

@dbwiddis
Copy link
Contributor

Hey @sgammon thanks for this! Understand this is still a work in progress, but it's exciting (for me, at least) to see expansion to other operating systems (and VMs).

@sgammon

This comment was marked as outdated.

@sgammon sgammon mentioned this pull request Jun 11, 2024
@sgammon sgammon force-pushed the feat/static-graalvm-jni branch from 8b323da to 8978b9b Compare June 11, 2024 03:01
@sgammon
Copy link
Author

sgammon commented Jun 11, 2024

Okay, I've cleaned up the stack of commits. It should be much more ready for review now. The full stack of commits is still available and can be restored if we need to unwind certain pieces.

@dbwiddis
Copy link
Contributor

I see you are the author of OSHI. ... Oh, I see you are a maintainer on JNA too

Well, developing a JNA-based cross-platform library necessitates a lot of upstream contributions. ;)

I did an initial review of the code but before digging into details, I think this is more of a philosophical discussion on whether another artifact vs. the usual JNA-packaged binary is the right fit for this repo. You've acknowledged that this is different and explained why, but I'm still trying to understand the usage from a downstream dependency's standpoint.

For example, in OSHI, I simply import the JNA dependency and expect it to work on any (supported) operating system. Using a different JAR doesn't seem to fit into this model; I'm either packaging both JARs and picking one of them at runtime, or "building" at runtime.

Are there other ways we can solve this problem?

@sgammon

This comment was marked as outdated.

@dbwiddis
Copy link
Contributor

Thanks for the more full explanation. I'm still not sure about all the technical details. I'm sure @matthiasblaesing will have some comments here and I'll wait for him to chime in before commenting more.

I do think it's great to support this capability, I just don't quite understand (yet) enough about any possible alternative approaches to conclude (as you have) this is the best one, but I'm sure we'll get that sorted out.

@sgammon

This comment was marked as outdated.

Adds a JAR publication at `jna-graalvm.jar`, with accompanying
build infrastructure, which provides support for JNA within the
context of the Substrate Virtual Machine (SVM).

GraalVM Native Image targets use SVM instead of JVM at runtime.
JNA's current strategy of unpacking libraries at runtime works
under SVM, but is suboptimal; the binary is native, so it can
simply include JNA object code for the current platform directly.

To accomplish this, several GraalVM "feature" implementations are
provided in this new publication. By default, regular JNA access
is enabled through the `JavaNativeAccess` feature; this class
enables reflection and runtime JNI configurations for downstream
projects which use JNA.

Another feature, `SubstrateStaticJNA`, is experimental because it
relies on unstable GraalVM APIs, but instead of loading JNA at
runtime from a dynamic library, it builds JNA into the final
native image with a static object.

These features are enabled through a resource within `META-INF`,
called `native-image.properties`, which is picked up by the native
image compiler at build time. The new artifact only needs to be
present for GraalVM native targets at build time; otherwise, the
classes and libraries in `jna-graalvm.jar` are inert.

Includes tested support for:
- macOS aarch64
- Linux amd64

- feat: add `jna-graalvm.jar` publication
- feat: add base `JavaNativeAccess` feature for auto-config of JNA
- feat: add initial implementation of `SubstrateStaticJNA` feature
- test: sample/test gradle build for native image
- chore: ci config to run native sample
- chore: add readme to `lib/gvm`

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]>
@sgammon sgammon force-pushed the feat/static-graalvm-jni branch 2 times, most recently from 6d07b23 to 874c272 Compare June 13, 2024 05:59
@sgammon

This comment was marked as outdated.

native/dispatch.c Show resolved Hide resolved
native/dispatch.c Outdated Show resolved Hide resolved
@sgammon sgammon force-pushed the feat/static-graalvm-jni branch from 874c272 to 62b1878 Compare June 13, 2024 06:57
native/dispatch.c Outdated Show resolved Hide resolved
JNIEXPORT jint JNICALL
JNI_OnLoad_jnidispatch(JavaVM *jvm, void *UNUSED(reserved)) {
setupJna(jvm);
return JNI_VERSION_1_8; // upgrade to JNI_VERSION_1_8; required for static JNI
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is required by spec for static JNI support. JNI_VERSION_1_8 was introduced in JDK8, so this is safe to apply now that JNA has a minimum that matches.

@sgammon sgammon force-pushed the feat/static-graalvm-jni branch from 62b1878 to f7b3d3d Compare June 13, 2024 07:19
src/com/sun/jna/Native.java Outdated Show resolved Hide resolved
@sgammon sgammon force-pushed the feat/static-graalvm-jni branch from f7b3d3d to 4fb7944 Compare June 13, 2024 07:23
sgammon and others added 3 commits June 13, 2024 00:27
When operating under static linkage in SVM (Native Image), JNA's
`JNI_OnLoad` hooks are not run. We need to sanity-check at the
first JNI border and run static initialization manually.

Additionally, `JNI_OnLoad` should be provided in static contexts
as `JNI_OnLoad_jnidispatch`. This changeset fixes both issues.

Signed-off-by: Sam Gammon <[email protected]>
Signed-off-by: Dario Valdespino <[email protected]>
When linked statically on GraalVM, JNI symbols declared in the
overloaded form cannot be resolved. Luckily, all of `Native`'s
callsites are in `Pointer` or itself, and all `native` methods
of `Native` are non-public.

This PR adjusts the JNA C API to avoid using overloaded `read`,
`write`, or `getDirectByteBuffer`. Callsites are amended in
`Pointer` accordingly.

Signed-off-by: Sam Gammon <[email protected]>
Signed-off-by: Dario Valdespino <[email protected]>
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]>
@sgammon sgammon force-pushed the feat/static-graalvm-jni branch from 4fb7944 to 548ebec Compare June 13, 2024 07:27
@sgammon

This comment was marked as outdated.

sgammon added a commit to elide-dev/elide that referenced this pull request Jun 13, 2024
- chore: switch to jna snapshot
- chore: add new jna graalvm artifact
- chore: opt-in to static jna feature
- chore: cleanup superfluous configs

Adopts java-native-access/jna#1608

Signed-off-by: Sam Gammon <[email protected]>
sgammon added a commit to elide-dev/elide that referenced this pull request Jun 13, 2024
- chore: switch to jna snapshot
- chore: add new jna graalvm artifact
- chore: opt-in to static jna feature
- chore: cleanup superfluous configs

Adopts java-native-access/jna#1608

Signed-off-by: Sam Gammon <[email protected]>
sgammon added a commit to elide-dev/elide that referenced this pull request Jun 13, 2024
- chore: switch to jna snapshot
- chore: add new jna graalvm artifact
- chore: opt-in to static jna feature
- chore: cleanup superfluous configs

Adopts java-native-access/jna#1608

Signed-off-by: Sam Gammon <[email protected]>
sgammon added a commit to elide-dev/elide that referenced this pull request Jun 13, 2024
- chore: switch to jna snapshot
- chore: add new jna graalvm artifact
- chore: opt-in to static jna feature
- chore: cleanup superfluous configs

Adopts java-native-access/jna#1608

Signed-off-by: Sam Gammon <[email protected]>
@matthiasblaesing
Copy link
Member

My current take on GraalVM: GraalVM still feels massively experimental and I fail to see a road for GraalVM project. At some point it was part of the JDK, then it got split of, then realignment with the JDK was planned (project galahad), but questions regarding the state basicly state "we will see, when or even if graalvm technology will be reintegrated into the JDK" (my takeaway from this short conversation: https://mail.openjdk.org/pipermail/galahad-dev/2024-June/thread.html#6 (Thread "What's the progress of Project Galahad")). So from my POV GraalVM has a good chance to die the same death that nashorn died.

At this point I only skimmed this PR, but the biggest part the jumped out was, that the whole native interface is changed. This alone introduces about 3 days of work to rebuild all libraries. So I have to ask why?

I also notice that the example project is placed in a new samples folder. That does not fit the current structure where sample/demo code reside in contrib.

@sgammon

This comment was marked as outdated.

@sgammon

This comment was marked as outdated.

@sgammon

This comment was marked as outdated.

sgammon added a commit to elide-dev/elide that referenced this pull request Jun 24, 2024
- chore: switch to jna snapshot
- chore: add new jna graalvm artifact
- chore: opt-in to static jna feature
- chore: cleanup superfluous configs

Adopts java-native-access/jna#1608

Signed-off-by: Sam Gammon <[email protected]>
@workcheng
Copy link

When is this version likely to be released?

@sgammon
Copy link
Author

sgammon commented Nov 26, 2024

@workcheng At the moment I think there are no plans, but it would not be hard to complete.

@matthiasblaesing What if I cleaned this up, and only included the very stable parts, and left the "static JNI" work until later? At least then JNA would "just work" for GraalVM native builds, without needing to register symbols. I am happy to rebase, clean it up, and offer it for review in a timely manner if there is interest in shipping this feature.

@sgammon
Copy link
Author

sgammon commented Nov 26, 2024

(Removing the Static JNI portion removes any experimental parts, and would obviate the need to issue an update to native libraries entirely, as no native code needs to change.)

@workcheng
Copy link

@workcheng At the moment I think there are no plans, but it would not be hard to complete.

@matthiasblaesing What if I cleaned this up, and only included the very stable parts, and left the "static JNI" work until later? At least then JNA would "just work" for GraalVM native builds, without needing to register symbols. I am happy to rebase, clean it up, and offer it for review in a timely manner if there is interest in shipping this feature.

Before this release, are there any alternative methods if I want to operate JNI in GraalVM?

@workcheng
Copy link

@workcheng At the moment I think there are no plans, but it would not be hard to complete.
@matthiasblaesing What if I cleaned this up, and only included the very stable parts, and left the "static JNI" work until later? At least then JNA would "just work" for GraalVM native builds, without needing to register symbols. I am happy to rebase, clean it up, and offer it for review in a timely manner if there is interest in shipping this feature.

Before this release, are there any alternative methods if I want to operate JNI in GraalVM?

My current requirement is to use Java to operate on the Windows registry to control some behaviors of the Windows system, such as setting proxy addresses and so on.

@sgammon

This comment was marked as off-topic.

@workcheng
Copy link

@workcheng You don't need this PR to use JNI with native GraalVM. This PR just makes it so that configuration is automatic. But you can specify the requisite configuration manually.

Take a look here. In a GraalVM Native Image, you are running in native code ("SubstrateVM"), not the JVM; so reflection and other JVM features are not available "automatically."

Thus, any class you use reflectively, or access reflectively from JNI, must be registered ahead of time. The same is true for symbols you use via JNI and also classpath resources, which, for JNA, often include native libs (.so, .dylib, etc).

If you run your program with the GraalVM Agent enabled, it will create the configurations for you. Be careful to keep these configurations up to date over time, as your code changes.

When this PR is merged, the process will become automatic, and any JNA usage will automatically configure itself for GraalVM when building for Native Image.

Oh, okay. I use this tool to configure the Windows proxy. It works fine directly on the JVM, but it doesn't work when packaged into GraalVM, and there are no error messages either. I thought this feature was required for it to take effect. Thanks. I'll see if there are any alternative methods or look into what's causing this.

@sgammon
Copy link
Author

sgammon commented Jan 19, 2025

I plan to take a second look at this PR soon, with the following amendments in mind:

  • Ship the Native Image integration as its own standalone PR, with an additional artifact (but without any change needed to native libraries or anything else)

  • Ship the Static JNI feature as a fork of this repo, so that the maintainers do not need to rebuild static libs; if this fork proves popular, maybe integration upstream can be considered (but it's experimental anyway, so it doesn't belong here, arguably)

I also haven't yet benchmarked JNA on the static JNI configuration, but I bet it is significantly faster. JNA today is much slower than JNI or FFM and I think that gap could potentially be closed for Native Image users.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants