Java Native Access (JNA) provides Java programs easy access to native shared libraries without writing anything but Java code - no JNI or native code is required.
—
This document covers JNA's structure mapping system, unions, custom type conversion, and callback functionality for seamless Java-to-native type interoperability.
import com.sun.jna.Structure;
import com.sun.jna.Union;
import com.sun.jna.IntegerType;
import com.sun.jna.NativeLong;
import com.sun.jna.NativeMapped;
import com.sun.jna.TypeMapper;
import com.sun.jna.Callback;The Structure class provides automatic mapping between Java classes and C structures:
/**
* Base class for mapping Java objects to native structures
*/
public abstract class Structure {
/**
* Default constructor - allocates new native memory
*/
public Structure();
/**
* Constructor using existing memory
* @param p Pointer to existing structure memory
*/
public Structure(Pointer p);
/**
* Read structure fields from native memory
*/
public void read();
/**
* Write structure fields to native memory
*/
public void write();
/**
* Get size of structure in bytes
* @return Structure size including padding
*/
public int size();
/**
* Get pointer to structure memory
* @return Pointer to native structure
*/
public Pointer getPointer();
/**
* Read specific field from native memory
* @param fieldName Name of field to read
*/
public void readField(String fieldName);
/**
* Write specific field to native memory
* @param fieldName Name of field to write
*/
public void writeField(String fieldName);
/**
* Enable automatic read after native calls
* @param auto true to enable auto-read
*/
public void setAutoRead(boolean auto);
/**
* Enable automatic write before native calls
* @param auto true to enable auto-write
*/
public void setAutoWrite(boolean auto);
/**
* Get field order for structure layout
* Must be implemented by subclasses
* @return List of field names in memory order
*/
protected abstract List<String> getFieldOrder();
// Static utility methods
/**
* Create new structure instance
* @param type Structure class
* @return New instance of structure
*/
public static Structure newInstance(Class<?> type);
/**
* Create structure instance at pointer
* @param type Structure class
* @param p Pointer to existing memory
* @return Structure instance using existing memory
*/
public static Structure newInstance(Class<?> type, Pointer p);
/**
* Auto-read array of structures
* @param structures Array of structures to read
*/
public static void autoRead(Structure[] structures);
/**
* Auto-write array of structures
* @param structures Array of structures to write
*/
public static void autoWrite(Structure[] structures);
// Inner marker interfaces for parameter passing
/**
* Marker interface for pass-by-value structures
*/
public interface ByValue { }
/**
* Marker interface for pass-by-reference structures
*/
public interface ByReference { }
}// Simple C structure mapping
public static class Point extends Structure {
public int x;
public int y;
public Point() {
super();
}
public Point(int x, int y) {
this.x = x;
this.y = y;
write(); // Write fields to native memory
}
public Point(Pointer p) {
super(p);
read(); // Read from existing memory
}
@Override
protected List<String> getFieldOrder() {
return Arrays.asList("x", "y");
}
@Override
public String toString() {
return String.format("Point{x=%d, y=%d}", x, y);
}
}
// Usage
Point p1 = new Point(10, 20); // Create and initialize
Point p2 = new Point(); // Create uninitialized
p2.x = 30; p2.y = 40; p2.write(); // Set fields and write// Structure with various field types
public static class ComplexStruct extends Structure {
public byte byteField;
public short shortField;
public int intField;
public long longField;
public float floatField;
public double doubleField;
public Pointer ptrField;
public String stringField; // Converted to/from native string
// Array fields
public int[] intArray = new int[10];
public byte[] bytes = new byte[256];
// Nested structure
public Point location = new Point();
@Override
protected List<String> getFieldOrder() {
return Arrays.asList(
"byteField", "shortField", "intField", "longField",
"floatField", "doubleField", "ptrField", "stringField",
"intArray", "bytes", "location"
);
}
}
// Structure with custom field handling
public static class CustomStruct extends Structure {
public int flags;
public String name;
@Override
protected List<String> getFieldOrder() {
return Arrays.asList("flags", "name");
}
@Override
public void read() {
super.read();
// Custom post-read processing
if (name != null) {
name = name.trim();
}
}
@Override
public void write() {
// Custom pre-write processing
if (name != null && name.length() > 255) {
name = name.substring(0, 255);
}
super.write();
}
}// Pass by value - structure copied to native stack
public static class PointByValue extends Structure implements Structure.ByValue {
public int x, y;
@Override
protected List<String> getFieldOrder() {
return Arrays.asList("x", "y");
}
}
// Pass by reference - pointer to structure passed
public static class PointByReference extends Structure implements Structure.ByReference {
public int x, y;
@Override
protected List<String> getFieldOrder() {
return Arrays.asList("x", "y");
}
}
// Native function declarations
public interface Graphics extends Library {
/**
* Function expecting structure by value
* void drawPoint(Point point);
*/
void drawPoint(PointByValue point);
/**
* Function expecting structure by reference
* void updatePoint(Point* point);
*/
void updatePoint(PointByReference point);
}
// Usage
PointByValue p1 = new PointByValue();
p1.x = 10; p1.y = 20;
Graphics.INSTANCE.drawPoint(p1); // Structure copied
PointByReference p2 = new PointByReference();
p2.x = 30; p2.y = 40; p2.write();
Graphics.INSTANCE.updatePoint(p2); // Pointer passed
p2.read(); // Read any changes made by native function/**
* Represents a native union where all fields share memory
*/
public abstract class Union extends Structure {
/**
* Set active field type by class
* @param type Class of field to make active
*/
public void setType(Class<?> type);
/**
* Set active field type by field name
* @param fieldName Name of field to make active
*/
public void setType(String fieldName);
/**
* Read value as specific type
* @param type Type to read value as
* @return Value cast to specified type
*/
public Object getTypedValue(Class<?> type);
/**
* Set value and active type
* @param object Value to set (determines active type)
*/
public void setTypedValue(Object object);
}
// Union example
public static class IntOrFloat extends Union {
public int intValue;
public float floatValue;
@Override
protected List<String> getFieldOrder() {
return Arrays.asList("intValue", "floatValue");
}
}
// Usage
IntOrFloat u = new IntOrFloat();
u.setType("intValue"); // Set active type
u.intValue = 42; // Set int value
u.write(); // Write to memory
u.setType("floatValue"); // Change active type
u.read(); // Read as float
float f = u.floatValue; // Same memory, different interpretation
// Typed access
u.setTypedValue(3.14f); // Sets floatValue and active type
Object value = u.getTypedValue(Float.class); // Gets as floatThe PointerType abstract class provides a foundation for creating type-safe native pointer wrappers:
/**
* Base class for creating type-safe native pointer types
*/
public abstract class PointerType implements NativeMapped {
/**
* Default constructor wraps a NULL pointer
*/
protected PointerType();
/**
* Constructor using existing pointer
* @param p Pointer to wrap
*/
protected PointerType(Pointer p);
/**
* Get the associated native pointer
* @return Native pointer representation
*/
public Pointer getPointer();
/**
* Set the associated native pointer
* @param p Pointer to set
*/
public void setPointer(Pointer p);
// NativeMapped implementation
public Class<?> nativeType();
public Object toNative();
public Object fromNative(Object nativeValue, FromNativeContext context);
// Object methods
public int hashCode();
public boolean equals(Object o);
public String toString();
}// Create custom pointer type for handles
public class WindowHandle extends PointerType {
public WindowHandle() { super(); }
public WindowHandle(Pointer p) { super(p); }
}
// Use in native library interface
public interface WindowsAPI extends Library {
WindowHandle CreateWindow(String className, String windowName, int style,
int x, int y, int width, int height,
WindowHandle parent, Pointer menu,
Pointer instance, Pointer param);
boolean DestroyWindow(WindowHandle window);
boolean ShowWindow(WindowHandle window, int cmdShow);
}
// Usage
WindowsAPI api = Native.loadLibrary("user32", WindowsAPI.class);
WindowHandle window = api.CreateWindow("STATIC", "Test", 0,
100, 100, 300, 200,
null, null, null, null);
api.ShowWindow(window, 1);/**
* Base class for platform-specific integer types
*/
public abstract class IntegerType extends Number implements NativeMapped {
/**
* Create signed zero-valued integer
* @param size Size in bytes (1, 2, 4, or 8)
*/
public IntegerType(int size);
/**
* Create optionally unsigned integer
* @param size Size in bytes
* @param unsigned true for unsigned type
*/
public IntegerType(int size, boolean unsigned);
/**
* Create with specific value
* @param size Size in bytes
* @param value Initial value
* @param unsigned true for unsigned type
*/
public IntegerType(int size, long value, boolean unsigned);
/**
* Set the integer value
* @param value New value
*/
public void setValue(long value);
@Override
public int intValue();
@Override
public long longValue();
@Override
public float floatValue();
@Override
public double doubleValue();
// Static utility methods
/**
* Compare two IntegerType values
* @param v1 First value
* @param v2 Second value
* @return Comparison result (-1, 0, 1)
*/
public static int compare(IntegerType v1, IntegerType v2);
}
/**
* Platform-specific long integer (long in C, not Java long)
*/
public class NativeLong extends IntegerType {
/** Size of native long in bytes */
public static final int SIZE;
public NativeLong();
public NativeLong(long value);
}
// Custom integer types
public static class UInt32 extends IntegerType {
public UInt32() { super(4, true); } // 4-byte unsigned
public UInt32(long value) { super(4, value, true); }
}
public static class Int64 extends IntegerType {
public Int64() { super(8, false); } // 8-byte signed
public Int64(long value) { super(8, value, false); }
}/**
* Interface for custom Java to native type conversion
*/
public interface NativeMapped {
/**
* Convert from native representation to Java object
* @param nativeValue Native value
* @param context Conversion context
* @return Java object
*/
Object fromNative(Object nativeValue, FromNativeContext context);
/**
* Convert Java object to native representation
* @return Native value
*/
Object toNative();
/**
* Get native type class
* @return Class representing native type
*/
Class<?> nativeType();
}
/**
* Provides type converters for automatic conversion
*/
public interface TypeMapper {
/**
* Get converter for native-to-Java conversion
* @param javaType Target Java type
* @return Converter or null if none available
*/
FromNativeConverter getFromNativeConverter(Class<?> javaType);
/**
* Get converter for Java-to-native conversion
* @param javaType Source Java type
* @return Converter or null if none available
*/
ToNativeConverter getToNativeConverter(Class<?> javaType);
}
/**
* Default TypeMapper implementation
*/
public class DefaultTypeMapper implements TypeMapper {
/**
* Add bidirectional type converter
* @param javaType Java type to convert
* @param converter Bidirectional converter
*/
public void addTypeConverter(Class<?> javaType, TypeConverter converter);
/**
* Add from-native converter
* @param javaType Target Java type
* @param converter From-native converter
*/
public void addFromNativeConverter(Class<?> javaType, FromNativeConverter converter);
/**
* Add to-native converter
* @param javaType Source Java type
* @param converter To-native converter
*/
public void addToNativeConverter(Class<?> javaType, ToNativeConverter converter);
}// Boolean type that maps to int (common in C)
public static class CBoolean implements NativeMapped {
private final boolean value;
public CBoolean(boolean value) {
this.value = value;
}
public boolean booleanValue() {
return value;
}
@Override
public Object fromNative(Object nativeValue, FromNativeContext context) {
return new CBoolean(((Integer) nativeValue) != 0);
}
@Override
public Object toNative() {
return value ? 1 : 0;
}
@Override
public Class<?> nativeType() {
return Integer.class;
}
}
// Enum mapping to native constants
public enum Status implements NativeMapped {
OK(0), ERROR(1), PENDING(2);
private final int value;
Status(int value) {
this.value = value;
}
public int getValue() {
return value;
}
@Override
public Object fromNative(Object nativeValue, FromNativeContext context) {
int val = (Integer) nativeValue;
for (Status status : values()) {
if (status.value == val) return status;
}
throw new IllegalArgumentException("Invalid status: " + val);
}
@Override
public Object toNative() {
return value;
}
@Override
public Class<?> nativeType() {
return Integer.class;
}
}
// Using custom types in library interface
public interface MyLibrary extends Library {
void setEnabled(CBoolean enabled); // Converts boolean <-> int
Status getStatus(); // Converts int <-> Status enum
}/**
* Base interface for all native callbacks
*/
public interface Callback {
/** Default method name for single-method callbacks */
String METHOD_NAME = "callback";
/** Forbidden method names that cannot be callback methods */
String[] FORBIDDEN_NAMES = {"hashCode", "equals", "toString"};
/**
* Handler for uncaught exceptions in callback threads
*/
public interface UncaughtExceptionHandler {
void uncaughtException(Callback c, Throwable e);
}
}
// Simple callback examples
public interface SimpleCallback extends Callback {
void callback(int value);
}
public interface ComparisonCallback extends Callback {
int compare(Pointer a, Pointer b);
}
// Multi-method callback (must use METHOD_NAME)
public interface FileCallback extends Callback {
boolean callback(String filename, int attributes);
}// Define library with callback functions
public interface CallbackLibrary extends Library {
CallbackLibrary INSTANCE = Native.loadLibrary("mylib", CallbackLibrary.class);
/**
* Register callback for notifications
* void setNotificationCallback(void (*callback)(int));
*/
void setNotificationCallback(SimpleCallback callback);
/**
* Sort array using comparison function
* void qsort(void* base, size_t num, size_t size,
* int (*compar)(const void*, const void*));
*/
void qsort(Pointer array, int count, int elementSize, ComparisonCallback comparator);
/**
* Enumerate files with callback
* int enumFiles(const char* path, bool (*callback)(const char*, int));
*/
int enumFiles(String path, FileCallback callback);
}
// Implement callbacks
SimpleCallback notifier = new SimpleCallback() {
@Override
public void callback(int value) {
System.out.println("Notification received: " + value);
}
};
ComparisonCallback intComparator = new ComparisonCallback() {
@Override
public int compare(Pointer a, Pointer b) {
int val1 = a.getInt(0);
int val2 = b.getInt(0);
return Integer.compare(val1, val2);
}
};
FileCallback fileHandler = new FileCallback() {
@Override
public boolean callback(String filename, int attributes) {
System.out.println("Found file: " + filename + " (attrs: " + attributes + ")");
return true; // Continue enumeration
}
};
// Register and use callbacks
CallbackLibrary.INSTANCE.setNotificationCallback(notifier);
// Sort integer array
Memory intArray = new Memory(40); // 10 integers
for (int i = 0; i < 10; i++) {
intArray.setInt(i * 4, (int)(Math.random() * 100));
}
CallbackLibrary.INSTANCE.qsort(intArray, 10, 4, intComparator);
// Enumerate files
CallbackLibrary.INSTANCE.enumFiles("/path/to/directory", fileHandler);/**
* Callback thread initialization
*/
public class CallbackThreadInitializer {
/**
* Get thread name for callback
* @param cb Callback instance
* @return Thread name
*/
public String getName(Callback cb);
/**
* Get thread group for callback
* @param cb Callback instance
* @return Thread group
*/
public ThreadGroup getGroup(Callback cb);
/**
* Check if callback thread should be daemon
* @param cb Callback instance
* @return true if daemon thread
*/
public boolean isDaemon(Callback cb);
/**
* Get uncaught exception handler
* @param cb Callback instance
* @return Exception handler
*/
public Thread.UncaughtExceptionHandler getUncaughtExceptionHandler(Callback cb);
}
// Callback with exception handling
SimpleCallback robustCallback = new SimpleCallback() {
@Override
public void callback(int value) {
try {
// Process value...
if (value < 0) {
throw new IllegalArgumentException("Negative value not allowed");
}
System.out.println("Processing: " + value);
} catch (Exception e) {
System.err.println("Callback error: " + e.getMessage());
// Don't let exceptions propagate to native code
}
}
};/**
* Manages callback references and function pointers
*/
public class CallbackReference extends WeakReference<Callback> {
/**
* Get callback instance from function pointer
* @param type Callback interface type
* @param p Function pointer
* @return Callback instance or null
*/
public static Callback getCallback(Class<?> type, Pointer p);
/**
* Get function pointer for callback
* @param callback Callback instance
* @return Native function pointer
*/
public static Pointer getFunctionPointer(Callback callback);
}
// Get function pointer for callback
Pointer funcPtr = CallbackReference.getFunctionPointer(myCallback);
// Pass function pointer directly to native code
someNativeFunction(funcPtr);
// Retrieve callback from pointer (if needed)
Callback retrieved = CallbackReference.getCallback(SimpleCallback.class, funcPtr);// Array of structures
Point[] pointArray = (Point[]) new Point().toArray(10);
for (int i = 0; i < pointArray.length; i++) {
pointArray[i].x = i * 10;
pointArray[i].y = i * 20;
}
Structure.autoWrite(pointArray); // Write all structures
// Pass to native function expecting Point*
void processPoints(Point[] points);
// After native call, read updated values
Structure.autoRead(pointArray);
// Nested structures
public static class Rectangle extends Structure {
public Point topLeft = new Point();
public Point bottomRight = new Point();
@Override
protected List<String> getFieldOrder() {
return Arrays.asList("topLeft", "bottomRight");
}
}
// Variable-length structure (common C pattern)
public static class VariableStruct extends Structure {
public int count;
public int[] data;
public VariableStruct(int count) {
this.count = count;
this.data = new int[count];
}
@Override
protected List<String> getFieldOrder() {
return Arrays.asList("count", "data");
}
}// Always implement getFieldOrder()
@Override
protected List<String> getFieldOrder() {
return Arrays.asList("field1", "field2", "field3");
// Order MUST match C structure layout
}
// Handle encoding for string fields
public static class TextStruct extends Structure {
public String text;
public TextStruct() {
super();
setStringEncoding("UTF-8"); // Explicit encoding
}
}
// Validate structure after read
@Override
public void read() {
super.read();
// Validate field values
if (count < 0 || count > MAX_COUNT) {
throw new IllegalStateException("Invalid count: " + count);
}
// Null-check string fields
if (name == null) {
name = "";
}
}
// Handle memory alignment issues
public static class AlignedStruct extends Structure {
public byte b; // 1 byte
// 3 bytes padding
public int i; // 4 bytes
public short s; // 2 bytes
// 2 bytes padding (to align to 4-byte boundary)
@Override
protected List<String> getFieldOrder() {
return Arrays.asList("b", "i", "s");
}
}The WString class provides Unicode wide character string support for native libraries:
/**
* Wrapper for wide character strings (wchar_t* in C)
*/
public final class WString implements CharSequence, Comparable {
/**
* Create wide string from regular string
* @param s String to wrap
*/
public WString(String s);
// CharSequence implementation
public int length();
public char charAt(int index);
public CharSequence subSequence(int start, int end);
// String operations
public String toString();
public boolean equals(Object o);
public int hashCode();
public int compareTo(Object o);
}// Use WString for wide character APIs
public interface UnicodeAPI extends Library {
UnicodeAPI INSTANCE = Native.loadLibrary("mylib", UnicodeAPI.class);
/**
* Process wide character string
* @param text Wide character string input
* @return Length of processed string
*/
int processWideString(WString text);
/**
* Create wide character string
* @param buffer Buffer to fill with wide string
* @param maxLen Maximum length
* @return Actual length
*/
int createWideString(WString buffer, int maxLen);
}
// Usage
WString wideText = new WString("Hello Unicode: ñáéíóú");
int result = UnicodeAPI.INSTANCE.processWideString(wideText);
// Wide strings work with Windows APIs
Function messageBoxW = Function.getFunction("user32", "MessageBoxW", Function.ALT_CONVENTION);
messageBoxW.invoke(Integer.class, new Object[]{
Pointer.NULL, new WString("Unicode Message"), new WString("Title"), 0
});The StringArray class manages native arrays of string pointers with automatic cleanup:
/**
* Handle native array of char* or wchar_t* with automatic memory management
*/
public class StringArray extends Memory implements Function.PostCallRead {
/**
* Create native array of strings
* @param strings Java string array
*/
public StringArray(String[] strings);
/**
* Create wide character string array
* @param strings Java string array
* @param wide true for wide character strings
*/
public StringArray(String[] strings, boolean wide);
/**
* Create string array with specific encoding
* @param strings Java string array
* @param encoding Character encoding
*/
public StringArray(String[] strings, String encoding);
/**
* Create from WString array
* @param strings WString array
*/
public StringArray(WString[] strings);
/**
* Get updated string values after native call
* @return String array with current values
*/
public String[] getStrings();
// PostCallRead implementation
public void read();
}// Native function expecting char** array
public interface SystemAPI extends Library {
SystemAPI INSTANCE = Native.loadLibrary("system", SystemAPI.class);
/**
* Execute command with arguments
* @param command Command name
* @param args Null-terminated argument array
* @return Exit code
*/
int execv(String command, StringArray args);
/**
* Process environment variables
* @param envVars Environment variable array
*/
void processEnvironment(StringArray envVars);
}
// Usage
String[] cmdArgs = {"/bin/ls", "-la", "/tmp", null}; // NULL-terminated
StringArray nativeArgs = new StringArray(cmdArgs);
int exitCode = SystemAPI.INSTANCE.execv("/bin/ls", nativeArgs);
// Wide character string arrays for Windows
String[] paths = {"C:\\Program Files", "C:\\Windows", "C:\\Users"};
StringArray widePaths = new StringArray(paths, true); // Wide strings
// String array with custom encoding
String[] utf8Strings = {"ñáéíóú", "中文", "العربية"};
StringArray encodedStrings = new StringArray(utf8Strings, "UTF-8");/**
* Provides converters for Java to native type conversion
*/
public interface TypeMapper {
/**
* Get converter from native to Java type
* @param javaType Target Java type
* @return Converter from native type
*/
FromNativeConverter getFromNativeConverter(Class<?> javaType);
/**
* Get converter from Java to native type
* @param javaType Source Java type
* @return Converter to native type
*/
ToNativeConverter getToNativeConverter(Class<?> javaType);
}/**
* Interface for objects that can convert between Java and native types
*/
public interface NativeMapped {
/**
* Convert from native value to Java object
* @param nativeValue Native value to convert
* @param context Conversion context
* @return Java object representation
*/
Object fromNative(Object nativeValue, FromNativeContext context);
/**
* Convert this object to native value
* @return Native representation
*/
Object toNative();
/**
* Get the native type used for conversion
* @return Native type class
*/
Class<?> nativeType();
}// Custom enum with native mapping
public enum Color implements NativeMapped {
RED(1), GREEN(2), BLUE(3);
private final int value;
Color(int value) {
this.value = value;
}
@Override
public Object toNative() {
return value;
}
@Override
public Object fromNative(Object nativeValue, FromNativeContext context) {
int val = (Integer) nativeValue;
for (Color color : values()) {
if (color.value == val) return color;
}
throw new IllegalArgumentException("Unknown color: " + val);
}
@Override
public Class<?> nativeType() {
return Integer.class;
}
}
// TypeMapper for custom conversions
public class CustomTypeMapper implements TypeMapper {
@Override
public FromNativeConverter getFromNativeConverter(Class<?> javaType) {
if (javaType == Color.class) {
return new FromNativeConverter() {
@Override
public Object fromNative(Object nativeValue, FromNativeContext context) {
return Color.values()[(Integer) nativeValue - 1];
}
@Override
public Class<?> nativeType() {
return Integer.class;
}
};
}
return null;
}
@Override
public ToNativeConverter getToNativeConverter(Class<?> javaType) {
if (javaType == Color.class) {
return new ToNativeConverter() {
@Override
public Object toNative(Object value, ToNativeContext context) {
return ((Color) value).value;
}
@Override
public Class<?> nativeType() {
return Integer.class;
}
};
}
return null;
}
}
// Use custom type mapper in library
Map<String, Object> options = new HashMap<>();
options.put(Library.OPTION_TYPE_MAPPER, new CustomTypeMapper());
MyLibrary lib = Native.loadLibrary("mylib", MyLibrary.class, options);
// Now Color enum works automatically
Color result = lib.getPreferredColor(); // Returns Color enum
lib.setBackgroundColor(Color.BLUE); // Accepts Color enumThis comprehensive type system enables seamless integration between Java objects and native data structures, providing automatic memory layout, type conversion, and callback support for complex native library integration.
Install with Tessl CLI
npx tessl i tessl/maven-net-java-dev-jna--jna