loading...
Cover image for Java and Native Code in OpenJDK 13

Java and Native Code in OpenJDK 13

petarov profile image Petar Petrov ・5 min read

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.

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 jextracted 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.

GitHub logo petarov / java-panama-ssh2

Java SSH client using libssh2 through project Panama

java-panama-ssh2

A simple Java SSH2 client using the native libssh2 library and JDK 13 Project Panama

NOTE: This is an experimental project. Stability is not guaranteed.

Install

The client has been tested on macOS and Linux. It should also be possible to run it on Windows, however, no work has been done in that direction. PRs are welcome!

Install libssh2.

  • macOS - brew install libssh2
  • CentOS - yum install libssh2.x86_64 libssh2-devel.x86_64

Get the latest Panama JDK build.

Open a console shell and configure the JAVA_HOME var to point to JDK 13.

Generate the required Java interfaces from the native libssh2 headers:

./gen_bindings.sh

Run

To compile and run use:

./run.sh [-p|-k] hostname port username [path to ssh keys]
  • -p uses a password login. You'll be prompted to enter your password.
  • -k uses a public key login. You'll need to specify the path to your keys, e.g., ~/.ssh.

License

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.

  • The Scope is pretty powerful. You can allocate callback pointers, structs, arrays. The concept of layouts and layout types deserves more time for me to fully explore and grasp.
  • It comes as no surprise that writing Java code using a native library is more extensive and requires extra care, especially when it comes to not forgetting to invoke cleanup API calls that the library requires.
  • I/O in most native libraries requires a file descriptor, which isn't easy to get in Java [6]. This, however, is not directly related to the java.foreign API.
  • Some libraries define C++ style function prototypes without argument as opposed to C-style Void argument types. The Foreign Docs [3] have an example about this case when using the TensorFlow C API.
  • I haven't explored if it would be possible to use Panama with Go or Rust created native libraries. That would be pretty cool.

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

Posted on by:

Discussion

markdown guide