ted.neward@newardassociates.com | Blog: http://blogs.newardassociates.com | Github: tedneward | LinkedIn: tedneward
How does native code work with the JVM?
What is JNI?
Is there an easier API than JNI?
The JVM wants/needs to interact with the platform beneath it
filesystem
networking
peripherals
graphics
sound
...
FFI: foreign function interface
The interface by which control transitions between VM-controlled code and "native" code outside the VM's sight or control
FFIs usually offer two things; sometimes three
hosted code "calling out" to native code
native code "calling in" to hosted code
code to boostrap the VM into existence
NOTE: Using any FFI usually requires native knowledge
native code compilation & linking
runtime code resolution & loading
runtime function resolution by name
calling conventions
unmanaged memory (heap allocation/deallocation)
native debugging skills
JVM FFI is documented in Java Native Interface (JNI) spec
describes all three scenarios
in existence since Java 1.1
has changed very little since 1.2
very low-level; almost assembly-language-like
Recent (circa 2018) efforts
Project Panama (OpenJDK)
GraalVM (via LLVM and Sulong)
JNA
JNR (jnr-ffi)
History
formally defined in JDK 1.1
incrementally refined in each release since
Love-hate relationship
JNI plays havoc with "WORA"
but over time, became more accepted
for certain things
over time, also became less necessary
more things gained standard JVM access libraries
Three forms
Java calling native code
Native code calling into Java
Hosting the JVM (JNI Invocation)
JNIEnv
this is a struct-of-function-pointers
first three slots "reserved"/empty (historical reasons)
provides open-ended flexibility and encapsulation
C++-friendly
Declare a native method in Java class
native
modifier, no implementation body
Compile the Java code
native method will still have no body
verify with javap
if you want to see
running now will generate UnsatisfiedLinkError
<</home/runner/work/Slides/Slides/Content/JVM/FFI/JNI/code/CallingNative/JNIExample.java NOT FOUND>>
(Optional) Create a C/C++ header
javah
generates C-style header from Java bytecode
later JDKs actually embed this in javac
(see -h
parameter)
Implement the expected function endpoint
C/C++: copy the function signature from the header
(others: match the function signature from the header)
first parameter is a JNIEnv*
; ignore this for now
second parameter will be a jobject
("this") if non-static
remaining parameters are method params, "JNI-ized"
(Optional) Implement load/unload entry points
These are invoked by JNI to give you initialization/cleanup
Must be exported as JNI_OnLoad
/JNI_OnUnload
parameters are JavaVM*
and "reserved" void*
return minimum JNI version from OnLoad (or will fail to load!)
<</home/runner/work/Slides/Slides/Content/JVM/FFI/JNI/code/CallingNative/linux/JNIExample.c NOT FOUND>>
dumpbin /exports
on Windows
objdump -T
or nm -D
on Linux/macOS
remember, names must match precisely
(parameters won't be displayed/verifiable)
System.loadLibrary
passing in library name
typically done in static initializer so it loads at classload time
leave off the extension or prefix (.dll
or libXXX.jnilib
)
Make sure the library can be found!
pass in -Djava.library.path
at JVM startup
put it in PATH
put it in other built-in dynamic library resolution (DYLD_LIBRARY_PATH
, ...)
<</home/runner/work/Slides/Slides/Content/JVM/FFI/JNI/code/CallingNative/JNIExample.java NOT FOUND>>
any C-binding language can be used here
C++, D, assembler, Delphi, so long as it can "cdecl"-bind
you will need to figure out how to adapt the native/primitive types
public func foo(...)
creates a standalone function
Use @_cdecl
to control name
Close eye on Swift/C interoperability
https://developer.apple.com/documentation/swift/swift_standard_library/c_interoperability
<</home/runner/work/Slides/Slides/Content/JVM/FFI/JNI/code/CallingNative/macos/JNIExample.swift NOT FOUND>>
<</home/runner/work/Slides/Slides/Content/JVM/FFI/JNI/code/CallingNative/macos/JNIExample.swift NOT FOUND>>
Rust is a system-level language designed to replace C
https://www.rust-lang.org/
The Rust Book: https://www.rust-lang.org/learn
so, in theory, this should be pretty easy (if you know Rust)
Rust makes it easier--it has a JNI crate already defined
cargo new
dir_name --lib
add a [dependencies]
for jni = "0.19.0"
only needed if you call back into the JVM...
... but it does provide definitions for JNIEnv
and jobject
and friends
<</home/runner/work/Slides/Slides/Content/JVM/FFI/JNI/code/CallingNative/linux-rust/Cargo.toml NOT FOUND>>
reference the JNI types
use jni::JNIEnv;
use jni::objects::{JObject};
might also need JClass
(for static methods)
turn off name-mangling #[no_mangle]
define the public, C-exported function with name and parameters
<</home/runner/work/Slides/Slides/Content/JVM/FFI/JNI/code/CallingNative/linux-rust/src/lib.rs NOT FOUND>>
cargo build
generates lib in target/debug
make sure .so
file is accessible via -Djava.library.path
or other means
Nim is another system-level language
https://nim-lang.org/
The Nim Manual: https://nim-lang.org/docs/manual.html
draws concepts from Ada, Modula, Python
deterministic memory management
support for different backends (C, C++, JS)
AST-manipulating macros
Map the Nim types to the JNI types
either by ignoring JNIEnv and jobject
or by building those types in Nim
Nim/JNI library (recommended)
https://github.com/yglukhov/jnim
not part of core Nim
<</home/runner/work/Slides/Slides/Content/JVM/FFI/JNI/code/CallingNative/linux-nim/JNIExample.nim NOT FOUND>>
nim c --header --app:lib
file.nim
put the library on the java.library.path
Native code calling Java takes two forms
platform considerations don't matter as much
#include
the java.h
and java_md.h
headers
calling into the JVM in a native method
everything goes through the JNIEnv*
C or C++ API, whichever you prefer
API looks similar to Reflection
<</home/runner/work/Slides/Slides/Content/JVM/FFI/JNI/code/CallingJava/win32/JNIExample.cpp NOT FOUND>>
Be careful of platform considerations
calling GUI methods may require an event loop
calling OS methods may block further execution on that thread
thread affinity is not guaranteed
JVM is really "just" a set of libraries
Java launcher loads the JVM, loads your command-line class, then executes its main()
any native process can do this, if it wants
JNI_CreateJavaVM()
creates the JVM instance
JVM arguments come in JavaVMInitArgs
structure and JavaVMOption
blocks
"returns" a JNIEnv*
DestroyJavaVM
closes down the VM
requires "jvm.dll", either implicitly or explicitly
Java launcher finds it & resolves symbols explicitly
Steps
Make sure the Launcher executable can find the shared lib
Bootstrap the JVM (with "command-line" options passing in)
Load the right JVM class
Find the method you want to use as entry point
Invoke it
Clean up when you're done
<</home/runner/work/Slides/Slides/Content/JVM/FFI/JNI/code/Launcher/Main.java NOT FOUND>>
<</home/runner/work/Slides/Slides/Content/JVM/FFI/JNI/code/Launcher/Launcher.cpp NOT FOUND>>
<</home/runner/work/Slides/Slides/Content/JVM/FFI/JNI/code/Launcher/Launcher.cpp NOT FOUND>>
<</home/runner/work/Slides/Slides/Content/JVM/FFI/JNI/code/Launcher/Launcher.cpp NOT FOUND>>
<</home/runner/work/Slides/Slides/Content/JVM/FFI/JNI/code/Launcher/Launcher.cpp NOT FOUND>>
<</home/runner/work/Slides/Slides/Content/JVM/FFI/JNI/code/Launcher/Launcher.cpp NOT FOUND>>
Build
make sure INCLUDE path references JVM include
make sure LIB path references JVM libs
<</home/runner/work/Slides/Slides/Content/JVM/FFI/JNI/code/Launcher/win32/build.bat NOT FOUND>>
<</home/runner/work/Slides/Slides/Content/JVM/FFI/JNI/code/Launcher/linux/build.sh NOT FOUND>>
<</home/runner/work/Slides/Slides/Content/JVM/FFI/JNI/code/Launcher/macos/build.sh NOT FOUND>>
Run
make sure jvm.dll / libjvm.so is on the library path
ClassLoader paths work as normal
<</home/runner/work/Slides/Slides/Content/JVM/FFI/JNI/code/Launcher/win32/run.bat NOT FOUND>>
<</home/runner/work/Slides/Slides/Content/JVM/FFI/JNI/code/Launcher/linux/run.sh NOT FOUND>>
<</home/runner/work/Slides/Slides/Content/JVM/FFI/JNI/code/Launcher/macos/run.sh NOT FOUND>>
Online
http://java.sun.com/javase/6/docs/technotes/guides/jni/index.html
The core JNI documentation—always good to read
http://java.sun.com/products/jdk/faq/jnifaq.html
JNA: OSS package to simplify JNI access
https://github.com/java-native-access/jna
Java Native Access
A Sun project now open-source
https://github.com/java-native-access/jna
"Provides simplified access to native library methods without requiring any additional JNI or native code"
Uses some native stub magic to generalize access to JNI FFI/native endpoints
Helps solely with Java-to-native invocation
Download
https://github.com/java-native-access/jna: Links to Maven Central
as of May 2022, v5.11.0
"Core" jar
jna-
X.
XX.
X..jar
supporting native library (jnidispatch
) is included in the jar file
"Platform" jar
jna-platform-
X.
XX.
X.jar
cross-platform mappings
per-platform commonly-used mappings
Maven install
<dependency> <groupId>net.java.dev.jna</groupId> <artifactId>jna-platform</artifactId> <version>5.11.0</version> </dependency>
Loading JNA
platform-specific shared library loaded when Native
is loaded
jna.boot.library.path
(which you set via -D)
load from system library paths
Windows PATH
macOS DYLD_LIBRARY_PATH
Linux LD_LIBRARY_PATH
attempt to extract the stub from jna.jar
Using Standard C library -- any platform
<</home/runner/work/Slides/Slides/Content/JVM/FFI/JNA/code/JNAHelloWorld.java NOT FOUND>>
Using Win32 MessageBox -- Windows only
<</home/runner/work/Slides/Slides/Content/JVM/FFI/JNA/code/Win32Hello.java NOT FOUND>>
Using SDL2 -- any platform (theoretically)
<</home/runner/work/Slides/Slides/Content/JVM/FFI/JNA/code/SDLHello.java NOT FOUND>>
"a high-performance JDK distribution designed to accelerate the execution of applications written in Java and other JVM languages along with support for JavaScript, Ruby, Python, and a number of other popular languages. GraalVM’s polyglot capabilities make it possible to mix multiple programming languages in a single application while eliminating foreign language call costs."
https://www.graalvm.org/docs/introduction/
virtual machine execution engine
drop-in replacement for Oracle Java 8/11
as well as drop-ins for other languages/runtimes
ahead-of-time compiler for Java
create standalone binaries
polyglot virtual machine
language implementation framework (Truffle)
Java (JVM), JavaScript/NodeJS, Python, Ruby, R, WASM, LLVM
custom language/DSL implementations
Which Java do you want?
Java8-compatible
Java11-compatible
(experimental) Java16-compatible
Which license do you want?
Community Edition (CE)
Enterprise Edition
anything compiled to bitcode
C, C++
others: Fortran, Ada, D, Delphi, Haskell, Julia, Obj-C, Rust, ...
write LLVM function
take careful note of its exported member names
extern "C"
helps here (for C++)
other languages will need to follow suit
or you will need to know mangled names
extern "C" void sayHello() { cout << "Hello, from LLVM!" << endl; }
Java setup
org.graalvm.polyglot
create a Context
create a Source
(from File
or String filepath)
use Context.eval
to evaluate the .so file; returns a Value
Context polyglot = Context.newBuilder(). allowAllAccess(true). build(); File file = new File("./hello.so"); Source source = Source.newBuilder("llvm", file).build(); Value clib = polyglot.eval(source);
Java call to LLVM
use Value.getMember
to obtain exported members; returns a Value
execute
those executable members
Value sayHello = clib.getMember("sayHello"); sayHello.execute();
Make sure to use the GraalVM LLVM toolchain
Compile with clang
(or other LLVM compiler) to shared library bitcode
CPP = $(LLVM_TOOLCHAIN)/clang++ CPPFLAGS = -shared -c -O -emit-llvm LIBS=-lgraalvm-llvm .PHONY: all clean toolchain-check all: toolchain-check hello.so LLVMExample.class toolchain-check: echo LLVM_TOOLCHAIN = $(LLVM_TOOLCHAIN) hello.so: hello.cpp $(CPP) $(CPPFLAGS) hello.cpp -o hello.so $(LIBS) LLVMExample.class: LLVMExample.java javac LLVMExample.java clean: rm *.so rm *.class
uses the LLVM "polyglot" API
for C/C++, this is in graalvm/llvm/polyglot.h
collection of C APIs that manipulate polyglot_value
s
a Polyglot Value
/polyglot_value
is an opaque object/pointer
use C/C++ API to determine characteristics
polyglot_is_string
polyglot_can_execute
use C/C++ API to obtain or transform values
polyglot_as_i8
polyglot_get_array_element
extern "C" void printMessage(polyglot_value message) { if (::polyglot_is_string(message)) { uint64_t size = ::polyglot_get_string_size(message) + 1; // Make sure we account for a NULL char buffer[size]; memset(buffer, 0, size); uint64_t written = ::polyglot_as_string(message, buffer, size, "utf-8"); cout << "Your message was " << buffer << endl; } else { cout << "Not sure what we've got here" << endl; } }
polyglot_java_type
returns Class object
polyglot_get_member
returns method
call it on object for instance method
call it on Class object for static method
polyglot_invoke
to invoke method
passing in polyglot_value
objects as parameters
extern "C" void sysout(polyglot_value message) { bool result = ::polyglot_is_string(message); assert(result); uint64_t size = ::polyglot_get_string_size(message) + 1; // Make sure we account for a NULL char buffer[size]; memset(buffer, 0, size); uint64_t written = ::polyglot_as_string(message, buffer, size, "utf-8"); polyglot_value java_lang_System_class = ::polyglot_java_type("java.lang.System"); polyglot_value java_lang_System_out = ::polyglot_get_member(java_lang_System_class, "out"); ::polyglot_invoke(java_lang_System_out, "println", message); }
Graal website
https://www.graalvm.org/
Graal docs
https://www.graalvm.org/docs/introduction/
"Ten Things You Can Do With GraalVM"
https://chrisseaton.com/truffleruby/codeone18/ten-things-graal.pdf
(Old) presentation by Chris Seaton
GraalVM Publications
https://www.graalvm.org/community/publications/
"Supercharge Your Applications with GraalVM"
A B Vijay Kumar (Packt)
GraalVM videos list
https://www.graalvm.org/community/video-library/
Proposed enhancement to JVM
https://openjdk.java.net/projects/panama/
Features:
Foreign Function and Memory Access APIs (JEP 412, 419, 424)
Foreign-Memory Access API (JEP 383, 393)
Foreign Linker API (JEP 370)
Vector API (JEP 338)
Goal: simplify/empower native access from JVM
Native code represents a powerful extension to Java code, but with risks
protect native code as much as possible
C++ exception handlers
native OS signal/exception handlers
for best performance, minimize boundary crossings
given the wide range of C/C++-compiling tools, lots of options are possible
given the wide range of C/C++-bound FFIs, lots more options available
Architect, Engineering Manager/Leader, "force multiplier"
http://www.newardassociates.com
http://blogs.newardassociates.com
Sr Distinguished Engineer, Capital One
Educative (http://educative.io) Author
Performance Management for Engineering Managers
Books
Developer Relations Activity Patterns (w/Woodruff, et al; APress, forthcoming)
Professional F# 2.0 (w/Erickson, et al; Wrox, 2010)
Effective Enterprise Java (Addison-Wesley, 2004)
SSCLI Essentials (w/Stutz, et al; OReilly, 2003)
Server-Based Java Programming (Manning, 2000)