Proxy2Java
Consumes the proxy IR and generates idiomatic Java bindings, so that Java code can call into the bound library across the C ABI through the Java Native Interface (JNI).
Using the tool
The Proxy2java tool is invoked as a subcommand of the GNATpolyglot program. It must be provided with at least a proxy file and an output path:
$> gnatpolyglot proxy2java <proxy-file> -o<output-path> {<switch>}
-h|--helpDisplay Help.
-qBe quiet.
-vVerbose output.
-o|--output=<outputPath>Path to the directory in which the Java interface should be generated. Directories along the path are created if they do not exist yet.
--with-runtime=<path>Location of the GNATpolyglot runtime to use. Defaults to
<outputPath>/runtimes. When omitted, the runtime is installed automatically into that location.--group-id=<groupId>groupIdof the generated Maven project file, given as a dotted name. Defaults tocom.adacore. The proxy name is appended automatically as alib<proxyName>artifact, so a proxy namedtestis published undercom.adacore.libtest.--header-file=<file>Prepend the content of the given file to all sources generated by GNATpolyglot.
Modules
Modules in the JSON Proxy are translated to Java packages, rooted at the
--group-id and the lib<proxyName> artifact name. Nested modules become
nested packages. Free functions of a module become static methods of a
generated <Module>Package class.
Ada declarations |
Generated Java |
|---|---|
-- example.ads
package Example is
procedure Foo;
end Example;
|
// com/adacore/libexample/example/ExamplePackage.java
package com.adacore.libexample.example;
public final class ExamplePackage {
public static void foo();
}
|
-- example-child.ads
package Example.Child is
procedure Bar;
end Example.Child;
|
// com/adacore/libexample/example/child/ChildPackage.java
package com.adacore.libexample.example.child;
public final class ChildPackage {
public static void bar();
}
|
Functions
Function placement
By default, functions become static methods of their module’s
<Module>Package class. Functions with a method role become instance
methods of the class of the role; the first parameter (the controlling object)
becomes the implicit this and is dropped from the Java signature.
Identifiers are emitted in camelCase; class names in PascalCase.
Operators
Java does not support operator overloading. Functions carrying an operator
placeholder name (see Ada2Proxy) therefore keep that placeholder name in
the generated interface (e.g. operatorPlus).
Types
Scalars
Scalars are mapped to Java primitive types:
Proxy type |
Java type |
|---|---|
|
|
character |
|
signed / unsigned int 8 |
|
signed / unsigned int 16 |
|
signed / unsigned int 32 |
|
signed / unsigned int 64 |
|
float 32 |
|
float 64 |
|
|
|
Java has no unsigned integer types, so signed and unsigned proxy integers of a given size map to the same (signed) Java type. 128-bit scalars are not supported (see Limitations).
Enumerations
An enumeration in the proxy becomes a Java enum. Each constant carries its
underlying native integer (the representation value carried by the proxy) in a
public final value field, and a static fromValue map provides the
reverse lookup (native integer → enum constant) used to reconstruct the
constant when a value crosses the JNI boundary.
The type of value follows the enum’s representation — the smallest signed
Java integer that holds the largest enumerator value (byte, short,
int or long). A nested Ref class (extending
com.adacore.gnatpolyglot.runtime.ScalarRef) wraps the value in a
ByteBuffer so an enum can be passed as a mutable (in-out) parameter.
A proxy enumeration Color with enumerators Red, Green and Blue
generates:
public enum Color {
RED((byte) 0),
GREEN((byte) 1),
BLUE((byte) 2),
;
public final byte value;
Color(byte value) {
this.value = value;
}
// native int -> enum constant, used at the JNI boundary
public static final java.util.Map<Byte, Color> fromValue = /* ... */;
// wraps the value so the enum can be passed as a mutable parameter
public static class Ref
extends com.adacore.gnatpolyglot.runtime.ScalarRef {
/* ... */
}
}
Arrays
A proxy array becomes a Java class implementing java.util.List<E> (and
RandomAccess), so it can be indexed, iterated and streamed like any Java
list — but its size is fixed: add, remove and friends throw
UnsupportedOperationException.
For a scalar element type, a ready-made runtime class is used —
IntegerArray,BooleanArray,FloatArray,DoubleArray,CharacterArray,ByteArray,ShortArrayorLongArray.For a class element type, a nested
Arrayclass is generated on the element class (e.g.MyInt.Array).
Arrays of arrays (multidimensional arrays) are not supported.
Note
These classes are part of the runtime support specific to the input language. Unlike scalars, enumerations or classes — which map to neutral Java types — an array carries semantics that belong to the input language’s own array model: most visibly its index bounds, which need not start at zero, but also how its storage is allocated and freed. No single neutral type can capture that for every possible input language, so an array maps instead to runtime support provided per input language.
Both get(i) / set(i, e) (zero-based, the usual List convention) and
getUnslided(i) / setUnslided(i, e) (using the index bounds carried by
the proxy) are available; getBegin(), getEnd() and size() expose
the bounds. A new array is allocated by passing its bounds to the constructor;
arrays returned from a function are wrapped automatically, and the underlying
native storage is released automatically when the wrapper is garbage-collected.
// a function returning, then consuming, an array of int
IntegerArray arr = ExamplePackage.make();
for (int v : arr) {
System.out.println(v);
}
arr.set(0, 42); // zero-based
ExamplePackage.consume(arr);
// allocate a fresh array with bounds 1 .. 10
IntegerArray fresh = new IntegerArray(1, 10);
Strings
A proxy string maps to the runtime class PolyglotString. It is a dedicated
string type rather than one of the array classes, but it relies on the same
per-input-language runtime support, and for the same reason (see the note
above). PolyglotString implements java.lang.CharSequence, but it is not
a drop-in java.lang.String — convert explicitly at the boundary.
new PolyglotString(String)— build one from a JavaString.toString()— copy the contents back out into a JavaString.charAt(i)(zero-based) andcharAtUnslided(i)(using the index bounds carried by the proxy) return a character;length()gives the length.
Text crosses the JNI boundary as UTF-8. The underlying native storage is
released automatically when the PolyglotString is garbage-collected, like
any other binding object.
// a function returning, then consuming, a string
PolyglotString s = ExamplePackage.greeting();
System.out.println(s.toString());
ExamplePackage.greet(new PolyglotString("hello"));
Classes
A proxy class maps to a Java class whose inheritability drives the Java modifier:
Proxy class kind |
Generated Java class |
|---|---|
|
|
|
|
|
|
Inheritance
An inheritable type can be extended from Java like any other Java class. When you override one of its methods, a dispatching call made inside the bound library will call back into your Java override.
Unlike the C++ backend, no special constructor or extra argument is needed: you
simply call the generated constructor with super(...) and override the
methods you want.
Ada declaration |
Java code using and overriding the generated bindings |
|---|---|
package Example is
type Shape is tagged private;
function Area (S : Shape) return Float;
procedure Describe (S : Shape'Class);
-- Dispatching call on Area
end Example;
|
import com.adacore.libexample.example.Shape;
import com.adacore.libexample.example.ExamplePackage;
class Square extends Shape {
private final float side;
Square(float side) {
this.side = side;
}
@Override
public float area() {
return side * side;
}
}
Shape shape = new Square(3.0f);
// Describe runs in the bound library, but its dispatching call
// to Area calls back into Square.area().
ExamplePackage.describe(shape);
|
Pointers
Java has no separate smart-pointer wrapper: a proxy pointer to a class is
exposed as that class itself (the generated wrapper extending
PolyglotObject). Ownership — which decides whether the pointed-to object is
ever freed — is carried by the object instance and read or changed through
_getOwner() / _setOwner(...), using the
com.adacore.gnatpolyglot.runtime.PolyglotData.Owner enum:
USER— the object is owned by your Java code; it is freed automatically once it becomes unreachable (see below).LIBRARY— the object is owned by the bound library; the Java side never frees it.STATIC— the object is statically allocated and is never freed; its owner cannot be changed.UNKNOWN— ownership could not be determined.
An object you construct yourself starts as USER; an object returned from a
function takes the ownership the proxy annotated its return with (typically
LIBRARY for something the library keeps owning).
When you pass an object to a function, the function expects a minimum
ownership level: this makes sure that a pointer the call may escape is not
later freed from under the library, leaving a dangling reference.
Passing an object whose ownership is too weak throws an
IllegalArgumentException before the native call.
A pointer that may be null is returned as a java.util.Optional<T>
(Optional.empty() for null), and null may be passed where a pointer
parameter is expected. A mutable (in-out) pointer parameter is passed through a
nested T.Ref wrapper whose get() returns the current Optional<T>.
Freeing is automatic: a USER-owned object is released once the garbage
collector finds it unreachable (a LIBRARY- or STATIC-owned one is left
alone). Because GC timing is not deterministic, the object also implements
AutoCloseable if you want to release it promptly with try-with-resources.
Rec rec = new Rec(1); // owned by USER
rec.setOwner(Owner.LIBRARY); // hand ownership to the library
// a function returning a (possibly null) pointer
Optional<Rec> other = ExamplePackage.recF(rec);
other.ifPresent(r -> System.out.println(r.getI()));
ExamplePackage.recP(null); // null is accepted
Exceptions
Exceptions are bound as Java classes extending a runtime type specific
to each input language. For example, Ada exceptions are bound to exceptions
extending com.adacore.gnatpolyglot.runtime.ada2java.AdaException. When a
bound function raises, the exception is caught at the C ABI boundary,
identified, and re-thrown on the Java side as the matching class. Predefined
exceptions provided by the frontend are bound as well.
How the bindings reach the bound library
The generated Java interface talks to the bound library through the Java Native Interface (JNI). Three layers are produced:
A public Java API — the idiomatic classes, methods, enums and exceptions the user calls.
A set of
private static nativeJava methods backing that API.A generated JNI/C bridge that marshals Java values to and from the C ABI exposed by the proxy, and is compiled as a native library.
The public classes extend the runtime base type
com.adacore.gnatpolyglot.runtime.PolyglotObject, which holds the opaque
pointer to the internal object and releases it automatically (through a
Cleaner). The native library is loaded at class-initialization time via
System.loadLibrary.
Building
Generation produces a Maven project (pom.xml) for the Java sources and a
GNAT project file for the JNI/C bridge. Build them in two steps: compile the
Java bindings with Maven, then compile the JNI layer with gprbuild,
specifying the following scenario variables:
OS— targeted system (unixorwindows)PROXY_LIB_LOCATION— directory containing the proxy relocatable library.PROXY_LIB— name of the proxy library (withoutlibprefix or extension).
# Build the Java bindings
$> mvn package -f 2java/
# Build the JNI bridge (link against the proxy library)
$> gprbuild 2java/<proxy>_jni.gpr --gpr=2 \
-XOS=unix \
-XPROXY_LIB_LOCATION=<proxy-lib-dir> \
-XPROXY_LIB=<proxy-lib-name>
Supported platforms
Proxy2Java currently supports only 64-bit Linux and Windows platforms (see Limitations).