Random Dev Articles

latest | archive | lights on

Java and Native Code in OpenJDK 13

Tuesday, 11 June 2019

Contents

Intro

Last week I watched a pretty cool GOTO Conference talk [1] with Mikael Vidstedt, director of Software Engineering at Oracle, where he presents many of the upcoming and currently being worked on Java features. One of those, Project Panama, stood out as particularly interesting to me.

#Java has changed more in the last year than in the previous five years.

Learn about these changes and what Java's future holds in this #GOTOchgo talk with @gsaab and @MikaelVidstedthttps://t.co/QcX8MnEQZL

— GOTO (@GOTOcon) June 5, 2019

Disclaimer: Project Panama is still in its early stages of development, so the contents of this article may not reflect its current state.

Project Panama, available in early-access JDK 13 builds, is meant as a bridge between Java and native code. How is this useful? There are certain use cases where a piece of functionality is available as a library written in C or C++ and the requirement is to integrate it into a Java application. The current solution is to use the Java Native Interface (JNI) [2] which could require a vast amount of work and is also quite error-prone. You need to have a good grasp of the native library’s insides and then write all the required implementations to bridge the Java interface with the native code. From my experience, calling native library functions that may change at some later point in time and also managing heap memory allocations could be a real challenge with JNI. To quote Mikael as he says it on the video:

How many people here have written JNI or, you know, done JNI? We’re so sorry for you!

This is where Panama comes into play. It provides new tools and API to simplify the bridging between Java and native code. It basically boils down to the following steps:

  1. Generate Java bindings (interfaces) from existing C header file(s) using the jextract tool.
  2. Invoke C functions via the extracted Java interface using the java.foreign API.

This allows you to concentrate on writing the core logic of your application, rather than fiddling with glue code and integration details.

Hands-on

Project Panama’s documentation pages [3] already provide a solid number of examples to start with. I’ll just take a quick peek at how to bridge and run a libCurl Java app and then I’d like to present a more detailed example - a simple SSH client that I wrote based on libSSH2.

I’m running these examples on a macOS, but with a few tweaks you should also be able to run them on a Linux installation.

The libCurl App

How to download a web page using the native Curl library’s implementation? Well, first we need to get and extract a Panama OpenJDK build archive.

Let’s open up a shell and set the JAVA_HOME environment variable to where the OpenJDK build archive is extracted.

export JAVA_HOME=/opt/jdk-13.jdk

Now we need to generate the Java interfaces, the glue code that will bind Java code to the native library. This will produce a curl.jar file:

$JAVA_HOME/bin/jextract -t org.unix -L /usr/lib -lcurl \
  --record-library-path /usr/include/curl/curl.h \
  -o curl.jar

When we inspect the JAR file, we can see all the Curl API calls, as well as dependency bindings. The Curl API is available through the new java.foreign Java API.

curl.jar

Now for a quick example. Here’s a Java piece of code that fetches a web page and displays its contents on screen.

import java.lang.invoke.*;
import java.foreign.*;
import java.foreign.memory.*;
import org.unix.curl.*;
import org.unix.curl_h;
import static org.unix.curl_h.*;
import static org.unix.easy_h.*;


public class Main {
   public static void main(String[] args) {
       try (Scope s = curl_h.scope().fork()) { 
           curl_global_init(CURL_GLOBAL_DEFAULT);
           Pointer<Void> curl = curl_easy_init();
           if(!curl.isNull()) {
               Pointer<Byte> url = s.allocateCString(args[0]);
               curl_easy_setopt(curl, CURLOPT_URL, url);
               int res = curl_easy_perform(curl);
               if (res != CURLE_OK) {
                 System.err.println("Error fetching from: " + args[0] + " ERR: " + Pointer.toString(curl_easy_strerror(res)));
                 curl_easy_cleanup(curl);
               }
           }
           curl_global_cleanup();
       }
    }
}

A couple of things to point out here. Notice how we cannot directly pass a Java String to the curl_easy_setopt() call. This call accepts a memory address pointer as the url parameter, so we first need to do a dynamic memory allocation operation using the Scope and pass a Pointer interface instance instead. As you may find in the Panama tech docs [5], the Pointer interface helps a lot when it comes to complex C-alike pointer operations like pointer arithmetic, casts, memory dereference, etc. The Scope manages the runtime life-cycle of dynamic allocated memory.

Alright, now armed with this knowledge, can you extend this code to write the contents of a Curl fetched web page to a file stream?

The libSSH2 App

Here’s a more complete example application that utilizes libSSH2 [4] to implement a simple SSH client.

If you’d like to have a go and adjust it to run on Windows, I’ll greatly appreciate a PR.

Final Thoughts

A few points from my side that I learned or have been thinking about while working with Panama.

Thanks for reading!🍻

References

  1. GOTO 2019 • Project Panama part
  2. Java Native Interface
  3. Panama Foreign Docs
  4. libSSH2 - a client-side C library implementing the SSH2 protocol
  5. Panama Binder Docs v3
  6. Most efficient way to pass Java socket file descriptor to C binary file

Help Me Delete Your Tweets!