-
Notifications
You must be signed in to change notification settings - Fork 1.7k
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
base: master
Are you sure you want to change the base?
JNA: Support GraalVM #1608
Conversation
This comment was marked as outdated.
This comment was marked as outdated.
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). |
@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. |
8b323da
to
8978b9b
Compare
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. |
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? |
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
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:
I'm not sure there is much use for a 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.
The base JNA is of course needed, and then |
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. |
@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.
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 (" (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 <[email protected]> Signed-off-by: Dario Valdespino <[email protected]> Co-authored-by: Sam Gammon <[email protected]> Co-authored-by: Dario Valdespino <[email protected]>
6d07b23
to
874c272
Compare
@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 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 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 ... in that order. Notes for review
Artifacts for testingHere are some zips, let me show you them.
Latest versions have these SHA256 fingerprints:
Tip To use the Maven zip: extract and then upload from the root to your personal/favorite Maven repository. 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
dependencyResolutionManagement {
repositories {
maven {
name = "elide"
url = uri("https://maven.elide.dev")
content {
includeGroup("net.java.dev.jna")
}
}
}
}
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)
}
[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" } |
874c272
to
62b1878
Compare
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 |
There was a problem hiding this comment.
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.
62b1878
to
f7b3d3d
Compare
f7b3d3d
to
4fb7944
Compare
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]>
4fb7944
to
548ebec
Compare
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):
No word yet on performance difference, but I'll update this comment once I can run a quick JMH suite. |
- 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]>
- 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]>
- 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]>
- 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]>
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 |
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 JNA already works on GraalVM, but it requires extensive manual configuration for
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 The only other logic to change is that, in static JNI, the 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 :/
Ah, I didn't see there were existing samples in |
Btw, static JNI is working downstream for us, and it is a very cool feature, because: (1) JNI's 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). |
Oh, sorry, one more native API change took place: the JNI API version was raised from 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. |
- 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]>
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
Usage
jna-graalvm.jar
to theirnative-image
classpath--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:
... 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.
You should see:
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
, callednative-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 injna-graalvm.jar
are inert.Approach
Abstract Base
This new class is package-private and provides protected utilities for use exclusively by GraalVM
Feature
implementations; for unfamiliar readers,Feature
s are build-time classes that contribute to compiler configuration.Common logic provided:
jna-graalvm.jar
resourceJavaNativeAccess
featureThis feature is designed to be registered unconditionally in a downstream
native-image
build; it is found automatically via thenative-image.properties
resource, which declares it via--features=...JavaNativeAccess
. Thus, (1) having thejna-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:
How to use it:
jna-graalvm.jar
to your build-time classpath fornative-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
featureThis 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 theJavaNativeAccess
feature. The two can be used in a build together, orStaticJNAFeature
can be omitted to preserve the current library behavior. This feature is opt-in and is not injected by thenative-image.properties
file.What it does:
[lib]jnidispatch.[a,lib]
, at build time, according to current JNA behaviorjnidispatch
as a static JNI "built-in"How to use it:
jna-graalvm.jar
to your build-time classpath fornative-image
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
https://github.com/oracle/graal/issues/3359 ↩
https://www.blog.akhil.cc/static-jni ↩