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 git@github.com: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
Copy link
Author

sgammon commented Jun 11, 2024

@dbwiddis I see you are the author of OSHI. We are users of JNA through OSHI (thank you for all your hard work!), and I'm hopeful this can at least eliminate some of the configs we end up shipping (abridged):

  {
    "interfaces":["oshi.jna.platform.linux.LinuxLibc"]
  },
  {
    "interfaces":["oshi.jna.platform.mac.SystemB"]
  },

These configurations should be automatic now, with the base feature only, which is already ready to go and has seen some successful testing. I can provide you with a test JAR if you'd like to give it a shot with OSHI. The static JNI piece is a bit more complex, we're debugging it with the GraalVM team.

Edit: Oh, I see you are a maintainer on JNA too 😄 while this is a draft (due to the static bits), we are already a bit worried about review. We'd like to get as much of a jump on it as we can. Whatever review you are willing to provide, we can be responsive and aggressive about cleaning it up and removing superfluous changes.

The Micronaut team has also expressed some interest in using this downstream, so JNA and OSHI would not be the only consumers of this feature.

@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
Copy link
Author

sgammon commented Jun 11, 2024

@dbwiddis

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

Fair point. Likewise, developing a cross-platform and cross-language runtime necessitates a lot of native and OS code. We are using OSHI to power our implementation of Node's os module, which I thought you might get a kick out of.

vs. the usual JNA-packaged binary is the right fit for this repo

I'm not sure what you mean? Oh, I suppose you mean just amending the existing JARs? I did consider that approach, but let me explain why I chose against it initially:

  • Native libraries must be built as static objects for static JNI, of course, and remain OS specific. I was concerned about bloating the regular JNA JARs with a full second copy of libjnidispatch for each OS, but in static form, rather than as a .so|dylib|etc.

  • While the GraalVM Feature classes are inert at runtime (I believe they have no requirement to be public), they are still present in the bytecode of an app if packaged directly in an implementation JAR. SVM might detect that these are unused and drop them, but they'd increase the weight of user app JARs and technically the classes don't need to be there anyway.

  • The GraalVM JAR depends on the Native Image APIs; JNA nicely has no transitive dependencies (I think?), and I didn't want to broaden this surface area for existing users of JNA who are not on SVM.

I'm not sure there is much use for a libjnidispatch.a outside of Static JNI on GraalVM. Static JNI is a supported scheme in regular JDK, but such objects must be built with the JDK, of course, because they can't be loaded at runtime. As far as I know this use case is super rare as a result.

Anyway, if I've misunderstood your request, please let me know. I'm happy to amend any way you see fit; as both a user and maintainer of JNA, I feel like I'm in good hands with your advice.

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.

The base JNA is of course needed, and then jna-graalvm.jar can optionally be added, at native image build time only (i.e. withheld from the application's runtime classpath), in order to auto-configure the compiler. It's in this library that the static lib ships, so it can be unpacked and referenced by the linker at image link time.

@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
Copy link
Author

sgammon commented Jun 11, 2024

@dbwiddis No worries. Several aspects were very confusing for me here, too, especially coming from JVM. JNI is sort of a ball of mystery.

Thank you for your initial comments here and I'll stay tuned for @matthiasblaesing's thoughts.

I just don't quite understand (yet) enough about any possible alternative approaches

We've seen a consistent pattern of library authors releasing an extra artifact which holds the compile-time stuff for GraalVM. That doesn't mean it's the right way to go, per se, but it's a pattern which GVM users would find familiar.

I do think there are some ways this approach could be simplified, but it might transfer complexity rather than eliminate it. I would defer to you guys fully on this one.

Ship default feature in the JNA JAR; drop or move the experimental feature

This would have the benefit of covering everyone who uses JNA, and happens to then use SVM. There would be no additional step in adding another dependency, and every installation of JNA built by Native Image would enjoy the benefits of automatic configuration.

This may actually be a good idea on the basis of getting away from the JSON configs, which can be brittle and cause crashes. The downside, of course, is the added class to JNA's JAR, and the transitive dependency on GraalVM.

JNA users, when switching to Native Image, would still need to include the GraalVM API dependency themselves, unless it was provided via JNA transitively, and that wouldn't make sense for the vast majority of JNA's users (who are on JVM and would see the extra dep as superfluous).

The experimental feature ("Static JNI") could be moved to another PR and either kept in a separate JAR or even published outside of JNA, in its own Maven coordinates. Unlinking these two features keeps the static libraries separate, so JAR size / runtime bloat isn't a concern.

The default feature ("JavaNativeAccess") is very sensible and stable. It does not use experimental APIs, and merely replicates configurations already in the wild, which users before had to author manually. So it would probably simplify review to include only this in an initial PR.

(Will add ideas to simplify as they occur to me)

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 <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>
@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
Copy link
Author

sgammon commented Jun 13, 2024

@dbwiddis Good news! The static JNI bug has now been fixed after an incredible rabbit-hole debug session (that ultimately boiled down to a lot of fprintf).

The Static JNI sample is working reliably, and has been added to CI as well.

In essence, under static linkage, the JNA initializer functions (native-side, in C) must be re-run. Easy fix. The cost is exactly one extra NULL check at VM startup.

Would you mind allowing a re-run of CI at your convenience? 😄 It has already passed the same CI configuration on our fork.

We've separated things out into four commits, which isolate the changes clearly:

(1) Changes for initial JavaNativeAccess feature
(2) Initial changes to C code to allow static access
(3) Change to C code to avoid polymorphic JNI, which isn't supported under static mode
(4) Changes to introduce Static JNI support (the SubstrateStaticJNA feature implementation, sample, CI, etc)

... in that order.

Notes for review

  • No logic was changed in C. Exports were added and a sanity NULL check was put in place on getNativeVersion which re-runs the existing init code; that's it.

  • No dist update is enclosed. We noticed during our internal review that SPARC JARs were ending up empty, among others; so we intend to defer to the regular library release process to update those. We can enclose an update if requested, of course.

  • Static JNI is still opt-in. Although it is tested and working now on Linux AMD64 and macOS ARM64, this still depends on unstable APIs in GraalVM, so it is opt-in.

  • Each commit is buildable. They should be buildable and reviewable independently.

Artifacts for testing

Here are some zips, let me show you them.

Latest versions have these SHA256 fingerprints:

934567f99a5aee35d48108a830a0847ec4d5c05c617881d240b0750e8bd3dfc8  jna-maven-pr1608.zip
60dd41d1e3b2518f87df26fb73f6d331f575e5c3f16882dbe4efe917e7233ccb  jna-dist-pr1608.zip

Tip

To use the Maven zip: extract and then upload from the root to your personal/favorite Maven repository.
Then you can use JNA at this testing version, using the coordinate net.java.dev.jna:jna-graalvm:5.15.0-SNAPSHOT.

Or, you can use them from our repo:

Maven

  <repositories>
    <repository>
      <id>elide-dev</id>
      <name>Elide Runtime</name>
      <url>https://maven.elide.dev</url>
    </repository>
  </repositories>

...

  <dependencies>
    <dependency>
      <groupId>net.java.dev.jna</groupId>
      <artifactId>jna-graalvm</artifactId>
      <version>5.15.0-SNAPSHOT</version>
    </dependency>
  </dependencies>

Gradle

settings.gradle.kts

dependencyResolutionManagement {
  repositories {
    maven {
      name = "elide"
      url = uri("https://maven.elide.dev")
      content {
        includeGroup("net.java.dev.jna")
      }
    }
  }
}

build.gradle.kts

dependencies {
  // as strings:
  implementation("net.java.dev.jna:jna:5.15.0-SNAPSHOT")  // or `jna-jpms` if you want
  implementation("net.java.dev.jna:jna-graalvm:5.15.0-SNAPSHOT")

  // as a version catalog entry:
  implementation(libs.jna)  // or `libs.jna.jpms` if you want
  implementation(libs.jna.graalvm)
}

libs.versions.toml

[versions]
jna = "5.15.0-SNAPSHOT"

[libraries]
jna = { module = "net.java.dev.jna:jna", version.ref = "jna" }
jna-jpms = { module = "net.java.dev.jna:jna-jpms", version.ref = "jna" }
jna-graalvm = { module = "net.java.dev.jna:jna-graalvm", version.ref = "jna" }

native/dispatch.c Show resolved Hide resolved
native/dispatch.c Outdated Show resolved Hide resolved
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.

src/com/sun/jna/Native.java Outdated Show resolved Hide resolved
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 <sam@elide.ventures>
Signed-off-by: Dario Valdespino <dario@elide.ventures>
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 <sam@elide.ventures>
Signed-off-by: Dario Valdespino <dario@elide.ventures>
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>
@sgammon
Copy link
Author

sgammon commented Jun 13, 2024

Results from downstream testing of the Static JNI feature are favorable. GraalVM is treating JNA's symbols as "built-ins" (these are logs generated by attaching a debugger):

preregisterUninitializedLibrary: jnidispatch
addBuiltinPkgNativePrefix: com_sun_jna_Native
addStaticJniLibrary: jnidispatch
...
isBuiltinFunction: `Java_com_sun_jna_Native_createNativeCallback, true
isBuiltinFunction: `Java_com_sun_jna_Native_getNativeVersion, true
isBuiltinFunction: `Java_com_sun_jna_Native_sizeof, true
...

No word yet on performance difference, but I'll update this comment once I can run a quick JMH suite.

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 <sam@elide.ventures>
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 <sam@elide.ventures>
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 <sam@elide.ventures>
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 <sam@elide.ventures>
@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
Copy link
Author

sgammon commented Jun 17, 2024

@matthiasblaesing

My current take on GraalVM: GraalVM still feels massively experimental and I fail to see a road for GraalVM project.

Hm, I would have to respectfully disagree, considering GraalVM has been out in stable form for more than 10 years now. Oracle has folded them into the main Java team, and Project Galahad forges ahead (they just recently renamed the modules to make them part of the JDK proper). I believe Oracle has made a 10-year commitment to support GVM.

Even if GraalVM doesn't last forever, there are a lot of JNA developers out there (including us) who use it, and who want JNA to work smoothly on top of it. This integration would keep working on into the future, of course, and responds dynamically both to changes in JNA and user code (any code which extends Library is automatically registered properly).

JNA already works on GraalVM, but it requires extensive manual configuration for native-image use (registration of JNI calls, for example), which is handled automatically by this feature. Keeping the JARs separate of course means the jna-graalvm.jar could be deleted someday without interfering with regular JNA.

... biggest part the jumped out was, that the whole native interface is changed ...

Yes, I know the native interface was touched, and truthfully I don't even want to touch it. These changes were required for the "Static JNA" feature, so they are not required for simple Native Image support, only the optimized static mechanism.

The logic in the native layer didn't actually change; it is only that functions had to be renamed, so as not to use JNI's overloaded calling syntax. This syntax isn't supported for static JNI, because such symbols cannot be resolved until link time.

There is no API-visible change, because the renamed methods are non-public and only have callsites within Pointer.java.

The only other logic to change is that, in static JNI, the JNI_OnLoad method is known at the symbol JNI_OnLoad_<libname> instead, so as not to collide with other static libraries in the same code unit.

I could split up this PR into two PRs: (1) the base JNA Native Image support feature, which does not require any native changes and is much less invasive, and (2) this new experimental static JNI thing. That would probably be much less risky and easier to review. What do you think?

Static JNI would also need static libraries, so to ship that feature the libs will need to be rebuilt anyway :/

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.

Ah, I didn't see there were existing samples in contrib. I can move them there, that is no problem

@sgammon
Copy link
Author

sgammon commented Jun 17, 2024

Btw, static JNI is working downstream for us, and it is a very cool feature, because:

(1) JNI's libjnidispatch is built in to the native image, so it is simply part of the final app
(2) There is no library unpack-and-load step from resources
(3) Because it is a native target, libraries don't have to be shipped in resources at all in this case

We've observed a significant speedup in start time as a result of these conversions to static JNI, both with JNA and other libraries (Netty, etc). So we will probably keep it, even if we need to do so on a fork, because it is a pretty substantial win for us. Just providing background as to why the feature is useful.

It's also just... very cool 😄. We get the cushy cross-platform nature of Java, and testing is a breeze, because it uses all the libraries embedded in JNA to support each OS. But when we ship for prod, we get a nice, trim, final binary, with no cruft in the image (resources for non-targeted OS) and less cruft at runtime (the unpack step).

I care more about shipping the basic GVM feature, though, because that will result in better reliability and easy of use for JNA on Native Image for everyone all around, with basically no risk (at least, no risk of native changes, etc).

@sgammon
Copy link
Author

sgammon commented Jun 17, 2024

Oh, sorry, one more native API change took place: the JNI API version was raised from JNI_VERSION_1_6 to JNI_VERSION_1_8.

I figure this change was pretty inert because JNA already sets a minimum at JDK 8. This change is also only required for static JNI.

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 <sam@elide.ventures>
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.

None yet

3 participants