ted@tedneward.com | Blog: http://blogs.tedneward.com | Twitter: tedneward | 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 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
// Native method, no body. public native void sayHello(int length); public static void main (String args[]) { String str = "Hello, world!"; (new JNIExample()).sayHello(str.length()); }
(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!)
#include <stdio.h> #include <jni.h> #include "JNIExample.h" JNIEXPORT void JNICALL Java_JNIExample_sayHello(JNIEnv *env, jobject object, jint len) { printf("Hello from C/C++!\nThe length of your string is %d.\n\n", len); } JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void* reserved) { printf(">>> JNIExample shared library loaded!\n"); return JNI_VERSION_1_2; } JNIEXPORT void JNICALL JNI_OnUnload(JavaVM* vm, void* reserved) { printf("<<< JNIExample shared library unloaded!\n"); }
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
, ...)
static { System.out.println(System.mapLibraryName("jniexample")); System.loadLibrary("jniexample"); }
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
// JNIEXPORT void JNICALL Java_JNIExample_sayHello(JNIEnv *env, jobject object, jint len) @_cdecl("Java_JNIExample_sayHello") public func JNIExample_sayHello(_ env: OpaquePointer, _ this: OpaquePointer, _ len: CInt) { print("Hello from Swift!") print("Your string was \(len) characters long") }
@_cdecl("JNI_OnLoad") public func onLoad() -> CInt { print(">>> JNIExample native library loaded") return 65538 // JNI_VERSION_1_2 (from jni.h) }
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
[package] name = "JNIExample" version = "0.1.0" edition = "2021" [dependencies] jni = "0.19.0" [lib] name = "JNIExample" crate-type = ["rlib", "cdylib"]
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
use jni::JNIEnv; use jni::objects::{JObject}; // JNIEXPORT void JNICALL Java_JNIExample_sayHello(JNIEnv *env, jobject object, jint len) #[no_mangle] pub extern "C" fn Java_JNIExample_sayHello(_env: JNIEnv, _object: JObject, len: u32) { println!("Hello from Rust!\nThe length of your string is {}.\n\n", len); } #[cfg(test)] mod tests { #[test] fn it_works() { let result = 2 + 2; assert_eq!(result, 4); } }
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
{.hint[XDeclaredButNotUsed]:off.} type jobject_base {.inheritable, pure.} = object jobject* = ptr jobject_base proc Java_JNIExample_sayHello(env: pointer, obj: jobject, len: int32): void {.exportc, cdecl, dynlib.} = echo "Hello world from Nim! Your string is length ", len, " !"
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
#include <jni.h> #include "JNIExample.h" #include <stdio.h> JNIEXPORT void JNICALL Java_JNIExample_sayHello(JNIEnv *env, jobject object, jstring message) { const char* native_message = env->GetStringUTFChars(message, NULL); int len = 0; // Call String.length() method on message jclass clsString = env->GetObjectClass(message); jmethodID midLength = env->GetMethodID(clsString, "length", "()I"); if (midLength == 0) printf("Could not find length() method\n"); len = env->CallIntMethod(message, midLength); printf("%s\nThe length of your string is %d.\n\n", native_message, len); env->ReleaseStringUTFChars(message, native_message); }
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
public class Main { public static void test() { System.out.println("You build a custom launcher!"); } }
#include <jni.h> /* where everything is defined */ #include <jni_md.h> #include <assert.h>
#define MAX_OPTS 4 int main(int argc, char* argv[]) { JavaVM *jvm; /* denotes a Java VM */ JNIEnv *env; /* pointer to native method interface */ JavaVMInitArgs vm_args; /* JDK/JRE 6 VM initialization arguments */ JavaVMOption options[MAX_OPTS]; int n = 0; options[n++].optionString = "-Djava.class.path=."; // gcc warning: conversion from string literal to 'char *' is deprecated [-Wc++11-compat-deprecated-writable-strings] assert(n < MAX_OPTS);
vm_args.version = JNI_VERSION_1_6; vm_args.nOptions = n; vm_args.options = (JavaVMOption*)&options; vm_args.ignoreUnrecognized = true; /* load and initialize a Java VM, return a JNI interface pointer in env */ JNI_CreateJavaVM(&jvm, (void**)&env, &vm_args);
/* invoke the Main.test method using the JNI */ jclass cls = env->FindClass("Main"); jmethodID mid = env->GetStaticMethodID(cls, "test", "()V"); env->CallStaticVoidMethod(cls, mid);
/* We are done. */ jvm->DestroyJavaVM();
Build
make sure INCLUDE path references JVM include
make sure LIB path references JVM libs
@ECHO JAVA_HOME is currently set to %JAVA_HOME% @ECHO Compiling Main.java javac -d . ..\Main.java cl -I%JAVA_HOME%\include -I%JAVA_HOME%\include\win32 -MD ..\Launcher.cpp %JAVA_HOME%\lib\jvm.lib @ECHO Make sure jvm.dll is on the PATH when running!
#! /bin/bash echo JAVA_HOME currently set to $JAVA_HOME echo Compiling Main.java ... javac -d . ../Main.java echo Compiling Launcher... g++ -I"$JAVA_HOME/include" -o Launcher ../Launcher.cpp $JAVA_HOME/libexec/lib/server/libjvm.so
#!/bin/bash echo JAVA_HOME currently set to $JAVA_HOME echo Compiling Main.java ... javac -d . ../Main.java echo Compiling Launcher... gcc -I"$JAVA_HOME/include" -I"$JAVA_HOME/include/darwin/" -o Launcher ../Launcher.cpp $JAVA_HOME/lib/server/libjvm.dylib
Run
make sure jvm.dll / libjvm.so is on the library path
ClassLoader paths work as normal
@ECHO OFF @ECHO Making sure jvm.dll is nearby... copy %JAVA_HOME%/bin/server/jvm.dll . @ECHO Running... Launcher.exe
echo Running Launcher... LD_LIBRARY_PATH=${JAVA_HOME}/libexec/lib/server:${LD_LIBRARY_PATH} ./Launcher
echo Running Launcher... DYLD_LIBRARY_PATH=${JAVA_HOME}/lib/server:${DYLD_LIBRARY_PATH} ./Launcher
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
import com.sun.jna.Library; import com.sun.jna.Native; import com.sun.jna.Platform; public class JNAHelloWorld { public interface CLibrary extends Library { CLibrary INSTANCE = (CLibrary) Native.loadLibrary( (Platform.isWindows() ? "msvcrt" : "c"), CLibrary.class); void printf(String format, Object... args); } public static void main(String[] args) { CLibrary.INSTANCE.printf("Hello, World"); for (int i=0;i < args.length;i++) { CLibrary.INSTANCE.printf("Argument %d: %s\n", i, args[i]); } } }
Using Win32 MessageBox -- Windows only
import com.sun.jna.Library; import com.sun.jna.Native; import com.sun.jna.Platform; public class Win32Hello { public interface User32Library extends Library { User32Library INSTANCE = (User32Library) Native.loadLibrary( (Platform.isWindows() ? "user32" : "cant-run-anywhere-else"), User32Library.class); // MessageBoxA([in, optional] HWND hWnd, [in, optional] LPCTSTR lpText, [in, optional] LPCTSTR lpCaption, [in] UINT uType) int MessageBoxA(int hWnd, String lpText, String lpCaption, int uType); } public static void main(String... args) { User32Library.INSTANCE.MessageBoxA(0, "From Java!", "Hello, World!", 0); } }
Using SDL2 -- any platform (theoretically)
import com.sun.jna.Library; import com.sun.jna.Native; import com.sun.jna.Platform; public class SDLHello { public interface SDLLibrary extends Library { SDLLibrary INSTANCE = (SDLLibrary)Native.loadLibrary( "SDL2", SDLLibrary.class); final int SDL_INIT_VIDEO = 0x00000020; int SDL_Init(int flags); // int SDL_ShowSimpleMessageBox(UInt32 flags, const char* title, const char* message, SDL_Window* window) int SDL_ShowSimpleMessageBox(int flags, String title, String message, Object window); final int SDL_MESSAGEBOX_INFORMATION = 0x00000040; } public static void main(String... args) { SDLLibrary sdl = SDLLibrary.INSTANCE; int result = sdl.SDL_Init(SDLHello.SDLLibrary.SDL_INIT_VIDEO); if (result != 0) System.out.println("Some kind of SDL error in init: " + result); result = sdl.SDL_ShowSimpleMessageBox( SDLLibrary.SDL_MESSAGEBOX_INFORMATION, "From Java!", "Hello, World!", 0); if (result != 0) System.out.println("Some kind of SDL error on message box: " + result); } }
"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
Who is this guy?
Architect, Engineering Manager/Leader, "force multiplier"
Co-founder, Solidify US
http://www.solidify.dev
Principal -- Neward & Associates
Author
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)
See http://www.newardassociates.com