Java interoperation with a Native DLL using JNA

Imagine you get a static link library (.lib file) whose functions need to be invoked from a Java application. You cannot link the library to your java program, but what you can do is to call functions from a dynamic link library (DLL).

The term “DLL” does not imply a specific format and thus the tooling you should use from Java to call the functions depends on what kind of DLL it is. It could be a .NET library that contains managed code or it could be a native dll with unmanged code (both concerns the Windows platform).

lib-dll-java

This blog post is about interopertion of Java with a native DLL. It could have been created with Microsoft Visual Studio C++. The DLL links the static link library and is a “Multi-threaded DLL” with “No Common Language Runtime support”.

From the Java side you have the choice between Java Native Interface (JNI) and Java Native Access (JNA).

The usual low-level way is to use JNI to access the DLL, but this is much more error prone and less productive than using JNA.

The JNA framework consists of a single jna.jar and can be downloaded here: Java Native Access (JNA) – downloads and documentation. If you use maven to build the project, just add the dependency:

 
   net.java.dev.jna
   jna
   3.2.7
   compile

You basically create a Java interface to wrap the DLL functions to be invoked, but…

Here are some technical details of interesting integration problems, depending on the signature of the DLL functions, that need to be invoked:

Consider the calling convention

Depending on the calling convention that the DLL expects, the wrapper interface needs to inherit from “com.sun.jna.Library” or “com.sun.jna.win32.StdCallLibrary”.

C++:
#define EXAMPLE_DLL extern "C" __declspec(dllexport)
EXAMPLE_DLL char* returnDLLVersion();
Java:
public interface ExampleDLL extends Library {
  ExampleDLL INSTANCE = (ExampleDLL) Native.loadLibrary("example.dll", ExampleDLL.class);
  String returnDLLVersion();
}

The Microsoft-specifc directive __declspec(dllexport) exports the functions for the DLL so that a client can invoke them. Usually you create a macro definition: #define EXAMPLE_DLL to make the code more readable.

Data type mapping between C and Java

The function “returnDLLVersion” returns a char pointer (C-String). The JNA-framework converts this to a Java String (Unicode). It offers automatic conversions for most C-data types to/from Java. Detailed documentation is here: “Mapping between Java and Native“.

Writing output into shared memory

C++:
EXAMPLE_DLL bool readBufferContent(unsigned char* outbuf);

Java:
boolean readBufferContent(Memory memory);

The C-function “readBufferContent()” writes an output string into memory, that Java needs to provide and read. If the maximum output length is known before, you can use this procedure:

  1. Java reserves a fixed amount of bytes:
    Memory memory = new Memory(100);
  2. Java passes a pointer to the memory to the DLL:
    ExampleDLL.INSTANCE.readBufferContent(memory);
  3. C++ writes the String and the DLL call returns.
  4. Java reads the memory content and creates a String object with:
    String bufferContent = memory.getString(0);
  5. The Java Garbage Collector cares about freeing the Memory-Object (somewhen), as soon as there are no references to it. What a luxury!

An output parameter of type “int”: call-by-reference

C++:
EXAMPLE_DLL ResultStruct* consolidate(const short *cardTypes, int *numberOfResults);

Java:
Pointer consolidate(int[] cardTypes, IntByReference numberOfResults);

The consolitate()-function demonstrates multiple problems:

  1. First parameter is an int-Array. In C you pass a pointer to the array. Fortunately, JNA automatically converts primitive arrays, see JNA-documentation chapter “Pointers and Arrays“.
  2. Second parameter is an “out”-parameter. The DLL puts the number of results into the variable “numberOfResults”. It is used to determine the length of the array that the function returns. Thus you need to pass the int-variable by reference. JNA provides various ByReference-classes, e.g. “com.sun.jna.ptr.IntByReference”.
    Java can get the int-value after the call with:
    int value = numberResults.getValue();
  3. The return value of the function is an array of dynamic length. Instead of using the IntByReference parameter, it could have been possible to determine the array length by terminating the array with a NULL-element. This implementation decision has not been taken here, thus the array-length is determined by the value of the 2nd parameter “numberOfResults”, so that the Java program does not need to compute it dynamically.

Dynamic memory reserved in the DLL: return an array of struct of dynamic length

The array elements are C-structs of type “ResultStruct”. Unfortunately, JNA cannot automatically convert an array of structs. It can convert a single struct into a Java object and arrays of primitives, so here the conversion must be implemented ourselves.

There are two questions:

  1. How to allocate and free the dynamic memory for the result array of structs? (To avoid a memory leak!)
  2. How to create the Java objects?

The length and thus the amount of memory is unknown before the function call, so we decide not use a fixed-length Memory object of JNA. Instead, the DLL dynamically reserves memory, but we must pay attention on the scope, so that the memory is still allocated when the function-call returns. The C++ program can use “malloc()” or “new” to allocate heap memory. To free it correctly the DLL provides a separate freeMemory() function:

C++:
void freeMemory(ResultStruct *arr) { if(arr) delete[] arr; }

Java:
void freeMemory(Pointer p);

The Java program cannot free the memory itself, because the DLL uses a separate heap-manager. You can find a discussion of this issue here: in a C++ forum

Create Java objects from array of C-structs

We map the return type of the function as a com.sun.jna.Pointer in Java. With the knowledge about the C-struct (sequence and data type of member variables), the java program can read and convert the memory content to create adequate objects. The number of elements in the array is also known (variable “numberOfResults”, see above). It is very important, to compute the offset correctly to avoid JVM-crashes for invalid memory access!

C++:
struct ResultStruct {
  unsigned short count;
  char description[40];
  unsigned short average;
};

Java:
public static class ResultStruct {
  public short count;
  public String description;
  public short average;
}

static ResultStruct[] fromArrayPointer(Pointer pointer, int numberResults) {
  ResultStruct[] arr = new ResultStruct[numberResults];
  int offset = 0;
  for (int i = 0; i < numberResults; i++) {
    arr[i] = fromPointer(pointer, offset);
    offset += 44;
  }
  return arr;
 }

static ResultStruct fromPointer(Pointer pointer, int offset) {
  ResultStruct inst = new ResultStruct();
  inst.count = pointer.getShort(offset);
  offset += 2;
  inst.description = pointer.getString(offset);
  offset += 40;
  inst.average = pointer.getShort(offset);
  return inst;
}

Callback functions (Call a Java method from within the C++ DLL)

In places, where C gets a function pointer to call a function, this can be used to call Java methods as well. JNA offers a Callback-interface, documented in chapter “Callbacks/Closures“. The Callback class inherits form the Callback interface and contains a single method of arbitrary name and with compatible signature.

C++:
EXAMPLE_DLL void setFunction(void (*func)(char **texts, int count));

Java:
void setFunction(ExampleCallback callback);

public interface ExampleCallback extends Callback {
    void receive(Pointer pointerToTexts, int count);
}

Convert a char** into a String-array

Finally, a look at the signature of the “receive” method in the ExampleCallback interface. It gets a string-array in form of  a pointer to a pointer, char **. Because of the differences between C-strings and Java-strings, you should use the method Pointer.getStringArray(int,int) provided by JNA to do the conversion. No care needs to be taken of memory usage, because the memory for the strings has been allocated by the C-DLL and the call happens from inside the DLL, so the C-program is responsible to free the memory when the callback returns. The java-strings copy the memory content and are completly decoupled from the c-strings.

public void receive(Pointer pointerToTexts, int count) {
  String[] texts = pointerToTexts.getStringArray(0, count);
  ...
}

We hope that the barrier to leave the Java world, through these examples is diminished! We look forward to your opinion.

Kommentare sind deaktiviert