JNA allows Java code to call native C/C++ libraries by mapping native functions and data structures to Java. It uses libffi to invoke native functions from Java. While easier than JNI, JNA still requires understanding native code and careful mapping of data types and memory management to avoid issues like memory corruption. JNA is generally suitable when no pure Java solution exists and native functionality is needed, but precision is required to implement it correctly.
Short Story: Unveiling the Reasoning Abilities of Large Language Models by Ke...
JNA - Let's C what it's worth
1. JNA - Let’s C what it’s worth
Author: Idan Sheinberg
2. whoami
Freelance software developer and all things open-source
Hobbyist software developer and all things open-source
Not much time for other stuff...
3. Let’s Start From The Begining
JNA - Java Native Access - A means to access C/C++ code from the JVM.
C/C++ (native) code is made accessible to the JNA by compiling it to a shared library.
Yep, that means DLLs (Windows), SOs (Linux) or Dylibs(OSX).
By using JNA, we are able to access native methods as if they were defined inside
standard JVM jars.
4. Why Should I Even Do That?
Be a Java programmer.
Not (want to) be a C/C++ programmer
Need a functionality/API provided only by native code.
Need to interface with specific hardware only accessible through native code.
Be a masochist/adventurous (maybe the same).
5. Don’t we already have JNI for that?
JNI is the standard approach for interfacing with code written in C/C++ from the JVM.
With JNI, the programmer must write C/C++ glue code to bridge the access to the actual
native code you’re trying to interface with.
That’s harder than your average Java related programming task,
and also requires proficiency in at least the C.
One might argue that a high-level language with a native
interface should rid its practician from writing native code.
6. A few more JNI facts
Lots of the low level mechanics in the JVM are implemented using JNI.
⏣ FileOutputStream, SocketInputStream, etc...
JNI allows for full, bi-directional interaction with JVM heap objects.
⏣ It was designed for the purpose of serving as an extension/foundation to the JVM.
⏣ That’s probably the main cause for its sluggish/clumsy design.
JNA, on the other hand, was created to solely ease the access to native code.
⏣ As there’s no(t supposed to be any) C/C++ code to write, there’s no point in dealing with bi-directional
access.
7. How does JNA work?
JNA uses libffi (Foriegn Function Interface Library) to invoke functions found in compiled
C/C++ code from the JVM:
⏣ JNA uses a JNI (no kidding) library named libjnidispatch to glue libffi and handle type mapping & method
proxying.
⏣ JNA (via libjnidispatch) also provides access to some useful low-level functions - malloc,memset,free,
etc...
⏣ libffi supports many, many CPU architectures, operating systems and compilers, allowing JNA to function
seamlessly regardless of the execution environment.
9. A quick start
Given the following sample program:
sample-lib.h:
void printer(const char * input);
sample-lib.c:
#include <stdio.h>
#include <sample-lib.h>
void printer(const char * input)
{
printf("Input - %sn",input );
}
10. A quick start
A matching JNA endpoint class might look like this:
SampleLib.java:
package org.sheinbergon.jna.sample;
import com.sun.jna.Native;
public class SampleLib {
private final static String SAMPLE = "sample-lib";
static {
Native.register(SAMPLE);
}
public static native void printer(String input);
}
11. Working with structures
Let’s define a C structure and a method to manipulate it:
sample-structure-lib.h:
typedef struct sample_structure {
unsigned long int random;
} SAMPLE_STRUCT;
void random(const SAMPLE_STRUCTURE *);
sample-structure-lib.c:
#include <time.h>
#include <stdlib.h>
#include <sample-structure-lib.h>
void random(const SAMPLE_STRUCT * strct)
{
srand(time(NULL));
strct->number = rand();
}
12. Working with structures
First, let’s define a matching Structure class in Java:
SampleStruct.java:
package org.sheinbergon.jna.sample;
import com.sun.jna.Structure;
public class SampleStruct extends Structure {
public int number ;
@Override
protected List<String> getFieldOrder() {
return List.of("number");
}
}
13. Working with structures
Now, for the matching JNA endpoint class:
SampleStructureLib.java:
package org.sheinbergon.jna.sample;
import com.sun.jna.Native;
public class SampleStructureLib {
private final static String SAMPLE_STRUCTURE = "sample-structure-lib";
static {
Native.register(SAMPLE_STRUCTURE);
}
public static native void random(SampleStruct struct);
}
14. Making DLLs available to the JVM.
The JVM uses the OS library search path to begin with.
⏣ $LD_LIBRARY_PATH and ldconfig on Linux
⏣ %PATH% and Windows/System Directory on Windows.
The local resource classpath should also work.
⏣ Be sure to place library file inside a directory matching the proper CPU architecture - linux-x86-64 for
linux 64 bit, win32-x86-64 for windows 64 bit, etc.
⏣ File suffix/prefix is determined per OS - Loading a DLL named java-jna implies searching for a file named
libjava-jna.so on Linux and java-jna.dll on Windows.
And of course, there’s jna.library.path or java.library.path system properties
16. Where’s that manual
Standard JNA Type Mapping - https://github.com/java-native-access/jna/blob/master/www/Mappings.md.
Unfortunately, the list above is lacking big time.
⏣ C/C++ coding conventions encourage passing pointers/references to maintain an efficient and flexible stack.
⏣ As opposed to JAVA, native function declaration does not differentiate between pointers to allocated memory
or garbage.
⏣ Following such rules & guidelines with JNA is a bit trickier, and a lot more confusing. For the very least, it’s
going to take some getting used to.
17. Pointer
JNA provides us with the Pointer class. As its name implies, it proxies a native pointer.
⏣ Remember, A pointer does not necessarily refer to allocated memory!
Common use cases for mapping types to a Pointer:
⏣ Pointers to types you don’t care about.
⏣ void * method/function argument/return type.
⏣ Instantiating a Structure sub-class.
18. Pointer
Native code allows you to read any type of data at any offset using a pointer. A JNA
Pointer provide the same functionality:
⏣ Let’s say we have int * cp = ...
⏣ For Pointer jp , calling jp.getInt(0L) is the same as *cp (the int value itself).
⏣ jp.getInt(4L) is the same *(cp+1). Pointer offset in native code matches sizeof(type)
You can also copy entire arrays from native memory.
⏣ jp.getIntArray(0L,5) copies 20 bytes (5 * int32) from the location pointed by p to the JVM heap.
⏣ Useful for reading structure fields populated inside the native code.
19. ByReference
char * is automatically translated to(and from) String.
But what about other types, like int * for example ?
⏣ Are we dealing with a reference to pre-allocated or unallocated memory ? are we referencing an array or a
single value?
For single value references, JNA provides us with smart(ish) types like IntByReference
⏣ From the Java developer’s point-of-view, they are convenient containers that allow us to pass pointers to a
JNA mapped method, and read the value set in a clearer way.
What about void ** ?
⏣ PointerByReference is your friend.
20. Memory
A sub class of Pointer, representing allocated native memory
⏣ That’s memory allocated on the native heap, of course.
⏣ Allocation takes place on instance construction (i.e. new Memory(20L)).
Memory is one of the most useful classes when working with JNA:
⏣ Use whenever you need to pre-allocate a buffer to be populated by the innards of native method.
⏣ That’s a pretty common use-case.
23. Structures limitations
Bit-Fields are not supported.
⏣ Aggregate all relevant bit fields onto the nearest type and parse possible bit values using bitwise operators.
C:
struct bitfields
{
unsigned short f1 : 7;
unsigned short f2 : 9;
};
JAVA:
public class BitFields extends Structure {
public short fshort;
}
Multi-Dimensional arrays are not supported.
⏣ Uni-dimensional arrays are just fine, so just be sure to encompass all of the dimensions size specifications -
a field of type char buffer[3][4][5] in a C struct should be mapped as byte[] buffer = byte[60] in a
matching JAVA Structure sub-class.
Be accurate with your type selection/inclusion - Core dumps are no fun!
24. Passing allocated memory
Let’s say a native void function expects an allocated byte buffer (char buffer[size]).
⏣ Function declaration would be in the form of void func(char * buffer).
One might guess that the matching JNA setup should look like this:
⏣ Define a native method static native void func(ByteByReference buffer).
⏣ Create a buffer byte [] buffer = new byte[size], put it inside a pointer new ByteByReference(byte[0]) and
pass it on to the method.
25. Passing allocated memory - Doing it Right
JVM heap allocated arrays are NOT the same as native allocated memory:
⏣ Define a native method static native void func(Pointer buffer).
⏣ Create a Memory(size) buffer instance and pass it on. Memory is a sub-class of Pointer class, so no
problem there.
Structure sub-classes array fields are an exception, and are considered valid.
⏣ The JNA layer takes care of copying from/to native code and memory allocation
C:
struct yarrr
{
byte by[20];
};
JAVA:
public class Yarrr extends Structure {
public byte [] by = new byte[20];
}
26. memory allocation
Consider the following native code sample:
// Struct definition
typedef struct {
char * f1;
int f2;
} STRUCT;
// Some function
void func(STRUCT * S){...}
// Main code loop
for(int i=0;; i++) {
STRUCT s = { .f1 = "text", .f2 = i };
...
func(s);
}
27. memory allocation
Matching JNA mapping might look like this:
// Struct defintion
public class Struct extends Structure {
public static Struct of(String f1,int f2){Struct s = new Struct();s.f1=f1;s.f2=f2;return s;}
public String f1;
public int f2;
...
}
// JNA function mapping
static native void func(Struct s);
// Main code loop
for(int i=0;; i++) {
Struct s = Struct.of("text",i);
...
func(s);
}
28. memory allocation - pressure
In C, a local variable’s memory is allocated in the stack on declaration and gets
deallocated once it goes out of scope:
⏣ Out of scope means end of block - loop iteration, function end, etc.
⏣ Local means every variable inside the bounds of a block aside from memory explicitly allocated.
In the JVM, class instances are created inside the heap and would eventually get GC’d
once they are no longer referenced.
Eventually being the keyword - it’s not always fast-enough…
⏣ This behavior could lead to memory exhaustion, native memory contention, sluggishness, and ugly crashes.
29. Memory allocation - pressure - Solution
In order to simulate native C stack memory allocation, we could simply reuse structure
instances instead of re-allocating them in each loop iteration.
⏣ Please don’t forget to “clear” the structure before reusing (“cheaper” than additional memory allocation).
// Main code loop
Struct s = new Struct();
for(int i=0;; int++) {
s.clear();
s1.f1 = “text”;
s1.f2 = i;
...
func(s);
}
30. Weak-References
Weak references refer to object instances that might be GC’d during contention, even
though the instances themselves might still be in-use/referenced.
⏣ Say we have a method void callee(SomeObject obj){...}
⏣ Calling it with callee(new SomeObject()) will create a weak-reference to that SomeObject instance.
Doing the same with JNA mapped structures are no different, of course. The
consequences, however, are far more impactful:
⏣ Structure GC also frees native memory. That memory might still be currently accessed by native code.
⏣ In fact, sometimes structures allocated are not meant to be freed as part of the ongoing flow.
31. Weak-References - Solution
Make sure everything passed down through JNA proxied methods is strongly referenced.
⏣ Static fields or members of singletons/objects the only go out of scope as part of lifecycle management
⏣ This applies to Structure , Pointer , Callback , *ByReference subclasses/instances.
32. Memory Pressure - Structure Synchronization
When passing Structure sub-classes through JNA mapped methods, Object fields defined
in the JVM need to be written to native memory and made available to the native code.
⏣ The same goes for fields being read after a JNA method call.
The problem starts when you’re dealing with nested structure definition (consider
structure array fields, re-instantiating on each read).
This could cause heap memory exhaustion and slow down you’re overall application
performance.
⏣ A Full GC might also be triggered, causing weak-reference (and other) issues to arise earlier.
33. Memory Pressure - Structure Synchronization - Solution
Luckily for us, we can choose what gets synced and what does not:
⏣ We can disable or enable read/write synchronization separately or both altogether.
public class Sync extends Strucutre {public int f1;...}
Sync s = new Sync();
s.setAutoWrite(false); // Disable only pre JNA method write sync
s.setAutoSynch(true); // Enable both read and write sync;
⏣ For maximum efficiency, we can also synchronize specific fields manually.
s.readField("f1"); // Post JNA method call
Only read/write fields that are required to properly fulfill the code’s functionality.
34. Memory Corruption - Pragma pack
Native code compilers might pad structure fields, so they’ll match the default byte
alignment of the target CPU architecture.
⏣ Given a structure that’s supposed to require 5 bytes:
struct structure {
char f1; // 1 bytes
in32_t f2; // 4 bytes
};
⏣ Memory allocated for the structure might be “rounded” up to 8 bytes:
struct structure {
char f1; // 1 bytes
/* 3 bytes padding */
in32_t f2; // 4 bytes
};
35. Memory Corruption - Pragma pack
That’s cool, butttttttttttttttttt
⏣ JNA needs to allocate and set memory in the same manner the compiled code behaves.
⏣ Mismatching in this setting might and probably will cause structure fields to be written beyond the bounds of
its allocated memory.
⏣ This would result in vary nesty heap-corruptions, the kind that’ll send you searching the Glibc’s source code.
36. Memory Corruption - Pragma pack - Solution
JNA Provides us with the means of setting the alignment type.
⏣ You can set it on a per Structure basis :
public class SampleStruct extends Structure {
public SampleStruct(){
setAlignType(ALIGN_NONE);
}
}
⏣ Or globally (on library load):
public class SampleLib {
static {
Native.register(NativeLibrary.getInstance(SAMPLE,
Map.of(Library.OPTION_STRUCTURE_ALIGNMENT,Structure.ALIGN_NONE)));
}
}
37. Getting proffesionall help
JNA Github repository is actively maintained
⏣ https://github.com/java-native-access/jna
⏣ Browsing through the readme, you can see it’s in active use by several major projects
The google group is also alive & kicking
⏣ https://groups.google.com/forum/#!forum/jna-users
⏣ A good place to search for answers or just rant and let some steam out.
40. Pros
JNA is a mature & stable solution.
⏣ After all, JNA is a just proxy/bridge between Java & Native code.
⏣ If you’re doing everything right, no special treatment/race-condition handling is required.
Fully supported on Linux, OSX and Windows
⏣ That includes the nuisance of dealin with Windows __stdcall convention and special types.
When keeping your eco-system pure JVM is of high importance, JNA is probably your go-to
solution.
⏣ It really narrows down the list of things you can’t easily achieve with a JVM programming language
41. Cons
JNA requires the developer to be precise and accurate
⏣ C/C++ programming has the same requirement.
⏣ Java, as high level programming language, has gotten us used to getting by with a much lower degree of
accuracy.
JNA requires the developer for the very least to be able to read and understand C/C++
code.
⏣ You need to understand the pointer types populated/passed on and their scope.
⏣ And what about thread-safety ? Documentation provided with native code is sometimes lacking.
⏣ Going JNA without the source code and/or a native reference implementation might be considred suicide.
42. My 2 cents
Build upon JNA when no other standard pure-JVM solution seem viable for a requirement.
⏣ The lack of existence of, or unmaintained/poor-quality code.
Don’t doubt JNA - doubt your own implementation instead.
⏣ JNA is harder until you get the hang of it - there’s no hiding from that truth.
RTFM, seriously
⏣ https://github.com/java-native-access/jna/blob/master/www/FrequentlyAskedQuestions.md
⏣ http://java-native-access.github.io/jna/4.2.1/overview-summary.html#overview.description
43. Use Case #1
Needed to convert live raw (wav) audio feed to AAC-encoded segments.
⏣ Segments need to be sequentually concatable.
⏣ The program need to to know epoch start timestamp of each segment (real world clock).
⏣ The JVM can read audio and determine timestamp of read data at any given point, but cannot encode raw
audio to AAC.
⏣ FFmpeg can segment audio-only stream, but cannot imprint epoch timestamps.
44. Use Case #1
Solution - Created a bridge between libfdk-aac and the JVM using JNA
⏣ Project repository https://github.com/sheinbergon/jna-aac-encoder
⏣ Raw (wav) audio is read from a capture interface in real-time using the Java (old but working) AudioSystem.
⏣ Each x bytes read are encoded to AAC (I’m passing byte buffers back and forth).
⏣ Timetstamp is recorded prior to reading the next chunk of bytes.
45. Use Case #2
Needed to find a microsecond-fast way to send messages from a C program to a
Java(Kotlin) program, on Windows.
⏣ Socket I/O or Memory mapping was too “low-level” in C.
⏣ Brokerless MQs seemed like a viable solution.
⏣ ZeroMQ/Czmq are very standard solutions, but windows builds are a complete mess.
⏣ JeroMQ might have been considered a good pure JVM alternative, but it wasn’t fast enough.
⏣ Nanomsg is a viable alternative (faster than ZeroMQ - 150-400 us per message), but the Java client requires
MSVC and ANT...Ya’ak
46. Use Case #2
Solution - Load the Nanomsg DLL from the JVM via JNA
⏣ Nanomsg DLL compiled using CLion/MinGW under Windows within 5 minutes.
⏣ Just took the DLL and made it loadable to the JVM application
⏣ Followed the samples here http://nanomsg.org/gettingstarted/pipeline.html and mapped minimal required
methods for consumer code
⏣ Implemented a consumer server thread in Java with JNA.
⏣ This took around 2.5 hours to wind up with a fully working sample.