But what about Java
Step 9 - But what about Java
Now the swan song. We used and stuck with Python bindings from the start, partly because it's just so simple to prototype/test/debug/develop with against a C library.... but the whole point of this idea is to be language agnostic, we have a C API so we should be able to interface with any language that provies a C calling mechanism. Java is an attractive target. How hard can it be? :)
Python built its binding dynamically at runtime, query the registry, create callable stub objects, profit. Java however it statically typed and compiled, so although there are various dynamic call methods we could use, for our current goal what we're going to do it build a java code generator. The aim is to be able to point at our C++ library, crank a handle, and get a java .jar file that we can then build against. We want to end up similar to python being able to write native Java code that, again, looks just like our python code creating native Java objects we can call methods on.
JNA
We're going to use JNA (Java Native Access) to call into out C API, it has a nice interface to use and provides a nicer abstraction rather than building a JNI glue layer, all we need to do is define an annotated interface to the xplat C API:
public interface XPlatNative extends Library {
Pointer registry_get();
long XPLAT_getNumClasses(Pointer registry);
String XPLAT_getClassName(Pointer registry, long index);
int XPLAT_invoke(String className, Pointer instance,
String methodName, Pointer args,
int argCount, XPlatValue.ByReference returnValue);
// ... and every other exported function
}
JNA loads the library and wires the interface up at runtime, the XPlatValue type is a JNA Structure matching the C variant union:
public class XPlatValue extends Structure {
public int type;
public XPlatData data; // JNA Union
}
The code generator
We'll write our java generator in java itself: javagen.java, this loads the targer library (libdemo.so) via JNA, reads the registry exactly as the Python binding does, and then generates Java source files:
Pointer registry = lib.registry_get();
Map<String, EnumInfo> enums = loadEnums(lib, registry);
Map<String, StructInfo> structs = loadStructs(lib, registry);
Map<String, ClassInfo> classes = loadClasses(lib, registry);
generateEnums(enums, outPath);
generateStructs(structs, outPath);
generateClasses(classes, enums, structs, outPath, libName, typeInfoNames);
For our MyEnum it generates a proper Java enum:
public enum MyEnum {
Red(0), Green(1), Blue(2);
public final int value;
MyEnum(int v) { this.value = v; }
}
For TestStruct it generates a record-like class with matching field types and a constructor. For Demo it generates a class with a real getInt() method, a putInt(int) method, and so on - complete with the correct Java types for parameters and return values.
The marshalling layer
XPlatMarshal provides static helpers that generated code calls to pack Java values into XPlatValue arrays before invoking:
public static void marshalIntArg(XPlatValue[] args, int index, long value) {
args[index].type = XPLAT_TYPE_INT;
args[index].data.intValue = value;
args[index].data.setType(Long.TYPE);
}
The generated Demo.getInt() method ends up looking roughly like:
public long getInt() {
XPlatValue[] args = new XPlatValue[0];
XPlatValue.ByReference ret = new XPlatValue.ByReference();
lib.XPLAT_invoke("Demo", instancePtr, "getInt", null, 0, ret);
return ret.data.intValue;
}
(again, we have many options for optimising this in future, but for now this keeps it really simple and easy to follow, debug, and understand!)
Dates map to java.time.LocalDate (days-since-epoch arithmetic, same as Python). Vectors become List<T>, maps become Map<K,V>, using the same ArrayBuilder C API from Step 5.
The runtime files
The java/xplat/ directory contains the files shared across all generated bindings:
XPlatNative.java- JNA interface to the full C APIXPlatValue.java/XPlatData.java- JNA structures matching the C typesXPlatMarshal.java- argument marshalling helpersXPlatRuntime.java- runtime helpers (array/struct builders, date conversion)
These are library-agnostic. The generated files (the Demo class, MyEnum, TestStruct, etc.) are library-specific and live alongside after generation.
Usage
The toolchain for Java is now: Build the native layer, run the generator, compile and run.
cd step9
make
# Generate Java bindings
java -cp java:java/jna-5.13.0.jar javagen demo java/
# Build the demo app
javac -cp java:java/jna-5.13.0.jar java/DemoApp.java java/*.java
# Execute
LD_LIBRARY_PATH=../build java -cp java:java/jna-5.13.0.jar DemoApp
The usage in our java code now looks just like our python, i.e straight native calls as if it were a Java library:
try (Demo demo = new Demo()) {
System.out.println(demo.getInt()); // 42
System.out.println(demo.getString()); // "hello"
System.out.println(demo.getDate()); // 1971-11-26
System.out.println(demo.getVector()); // [1.0, 2.0, 3.5]
demo.putInt(999);
demo.setAll(true, 42, 3.14, "hello", MyEnum.Blue);
TestStruct s = demo.getStruct();
System.out.println(s.m_string);
}
I'm using a try-with-resources block above, this takes advantage of the AutoCloseable feature and lets us have more control over object lifetimes in Java.
At scope exit this will call close() and cause XPLAT_destroyInstance to run deleting the C++ native object.
We can, and could, just rely on the Java garbage collector to work, but we have more control here. e.g. if our C++ object is huge, Java has no knowledge of this and wouldn't try to garbage collect even on low memory.
We can add other intelligence to our API if we wanted, we could provide calls to interrogate memory used and/or the list of handles etc.
So long and thanks for all the fish
That's all for now, I'd intended purely to write a simple example of using C++26 at first, but got slightly carried away. With the metadata infrastructure this wires into a library it should be possible to use this for multiple purposes, an API validation system could be build to check for and guarantee library compatability, markup could be used to drive documentation generation actually from the code rather than using doxygen type annotations, then rust/lua/scala etc? all should be relatively straightforward to knit together.