Busy Java Developer's Guide to Native Code

ted@tedneward.com | Blog: http://blogs.tedneward.com | Twitter: tedneward | Github: tedneward | LinkedIn: tedneward

Objectives

JVM FFI

The Foreign Function Interface

JVM FFI

The JVM wants/needs to interact with the platform beneath it

JVM FFI

Definitions

FFI: foreign function interface

The interface by which control transitions between VM-controlled code and "native" code outside the VM's sight or control

JVM FFI

FFIs usually offer two things; sometimes three

JVM FFI

NOTE: Using any FFI usually requires native knowledge

JVM FFI

JVM FFI is documented in Java Native Interface spec

JVM FFI

Recent (circa 2018) efforts

Java Native Interface

The Foreign Function Interface for the JVM

JNI

History

JNI

Love-hate relationship

JNI

Three forms

JNI

JNIEnv

Java-calling-native

Going from Java code to native code

Steps

Write the Java

    Declare a native method in Java class

    Compile the Java code

Steps

  // 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());
  }

Steps

Write the native implementation

    (Optional) Create a C/C++ header

    Implement the expected function endpoint

Steps

Write the native implementation

    (Optional) Implement load/unload entry points

Steps

#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");
}

Steps

When in doubt, verify exported symbols

Steps

Load the native library in Java code

Steps

  static {
    System.out.println(System.mapLibraryName("jniexample"));
    System.loadLibrary("jniexample");
  }

Java-calling-native

Native note

Java-calling-native

Swift (macOS)

Swift/macOS

// 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")
}

Swift/macOS

@_cdecl("JNI_OnLoad")
public func onLoad() -> CInt {
    print(">>> JNIExample native library loaded")
    return 65538  // JNI_VERSION_1_2 (from jni.h)
}

Java-calling-native

Rust (Linux)

Java-calling-native

Create Rust project

Rust/linux

[package]
name = "JNIExample"
version = "0.1.0"
edition = "2021"

[dependencies]
jni = "0.19.0"

[lib]
name         = "JNIExample"
crate-type   = ["rlib", "cdylib"]

Java-calling-native

Write the exported function

Rust/linux

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);
    }
}

Java-calling-native

Build! Run!

Java-calling-native

Nim

Java-calling-native

Write the exported function

Java-calling-native

{.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, " !"

Java-calling-native

Build! Run!

Native-calling-Java

Going back into the JVM from native code

Native-calling-Java

Native code calling Java takes two forms

Native-calling-Java

#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);
}

Native-calling-Java

Be careful of platform considerations

JNI Invocation

Bringing up the JVM in your native process

JNI Invocation

JVM is really "just" a set of libraries

JNI Invocation

Steps

JNI Invocation

public class Main {
    public static void test() {
        System.out.println("You build a custom launcher!");
    }
}

JNI Invocation

#include <jni.h>       /* where everything is defined */
#include <jni_md.h>
    
#include <assert.h>

JNI Invocation

#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);

JNI Invocation

    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);

JNI Invocation

    /* invoke the Main.test method using the JNI */
    jclass cls = env->FindClass("Main");
    jmethodID mid = env->GetStaticMethodID(cls, "test", "()V");
    env->CallStaticVoidMethod(cls, mid);

JNI Invocation

    /* We are done. */
    jvm->DestroyJavaVM();

JNI Invocation

Build

JNI Invocation

@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!

JNI Invocation

#! /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

JNI Invocation

#!/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

JNI Invocation

Run

JNI Invocation

@ECHO OFF

@ECHO Making sure jvm.dll is nearby...
copy %JAVA_HOME%/bin/server/jvm.dll .

@ECHO Running...
Launcher.exe

JNI Invocation

echo Running Launcher...
LD_LIBRARY_PATH=${JAVA_HOME}/libexec/lib/server:${LD_LIBRARY_PATH} ./Launcher

JNI Invocation

echo Running Launcher...
DYLD_LIBRARY_PATH=${JAVA_HOME}/lib/server:${DYLD_LIBRARY_PATH} ./Launcher

JNI Resources

Resources

Online

Java Native Access (JNA)

A convenience layer atop JNI

Java Native Access (JNA)

Java Native Access

Installing JNA

Hooking it all up

Installing JNA

Download

Installing JNA

Maven install


<dependency>
    <groupId>net.java.dev.jna</groupId>
    <artifactId>jna-platform</artifactId>
    <version>5.11.0</version>
</dependency>


Installing JNA

Loading JNA

JNA Examples

Exploring with code

JNA Examples

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]);
    }
  }
}

JNA Examples

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);
    }
}

JNA Examples

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);
    }
}

GraalVM

Overview

GraalVM

What is it?

GraalVM

What is it?

GraalVM

Several distinct "flavors"

Graal LLVM

Interoperability via LLVM bitcode

Graal LLVM

GraalVM can run LLVM bitcode

Graal LLVM

Simple native function, LLVM side

Graal LLVM

extern "C" void sayHello() {
    cout << "Hello, from LLVM!" << endl;
}

Graal LLVM

Simple native function, Java side

Graal LLVM

        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);

Graal LLVM

Simple native function, Java side

Graal LLVM

        Value sayHello = clib.getMember("sayHello");
        sayHello.execute();

Graal LLVM

Build! Run!

Graal LLVM

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

Graal LLVM

Working with Java values

Graal LLVM

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;
    }
}

Graal LLVM

Calling back into the JVM

Graal LLVM

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 Resources

Where to go to get more

Graal Resources

Official

Graal Resources

Books

Videos

Project Panama

Interconnecting JVM and native code

Project Panama

Proposed enhancement to JVM

Summary

Native code represents a powerful extension to Java code, but with risks

Credentials

Who is this guy?