Releases: gmpassos/java_interop
Release list
1.4.0 — Java interfaces implemented in Dart
Java interfaces can now be implemented in Dart. Both this release and the 1.3.0
section of the changelog land together — 1.3.0 was never published on its own, so
its additions ship here.
Everything came out of porting a real Java library to Dart: XMLDSig signing, SOAP
over mutual TLS, JasperReports. About 530 lines of that port were things any
consumer would have had to write, plus one capability that could not be worked
around at all.
Implementing a Java interface
A library that wants a listener, a visitor, a Comparator or an SPI wants an
object, and JNI cannot make one: Proxy.newProxyInstance needs an
InvocationHandler and RegisterNatives needs a class, and both are Java.
final ordering = jvm.implementInterface(
'java.util.Comparator',
onInvoke: (call) => switch (call.methodName) {
'compare' => (call.args[0] as int).compareTo(call.args[1] as int),
_ => null,
},
);
try {
arrays.callJavaStatic('void sort(Object[], java.util.Comparator)', [
values,
ordering.instance,
]);
} finally {
ordering.release();
}No jar ships. The one Java class this needs stays readable in the repository and
travels as compiled bytes in Dart source, loaded with JNI DefineClass. A JDK is
needed only by the maintainer who regenerates it.
Arguments arrive as Dart values, with boxed primitives unboxed. Return values are
narrowed to the method's declared type on the Java side, because Dart has one
integer type and cannot know whether 1 means a byte or a long. toString,
equals and hashCode are answered in Java by default — Proxy routes them to
the handler like any other method, and a proxy without them breaks the first time
it reaches a HashMap or a log line.
Threading
Three facts were measured rather than assumed, and each shaped the design:
- a Dart
isolateLocalcallback entered from another thread aborts the
process before any Dart code runs, so the thread check lives in Java, where
Thread.currentThread()is free and reliable; - a
listenercallback is safe from any thread but runs after the calling frame
is gone, when everyjobjectin its arguments is already dead; - an isolate is not pinned to an OS thread:
await Isolate.runresumes the
main isolate on the thread the child just freed.
So a reentrant call returns a value; a void call from a JVM-owned thread is
queued and runs on the next turn of the event loop; and a call from a JVM thread
that wants a value throws IllegalStateException naming both threads. Nothing
can deadlock, and nothing silently does nothing. A released proxy called from a
JVM thread — the one case with neither a handler nor an error callback left — is
dropped and counted in droppedProxyCalls, because throwing there would end the
isolate over something the caller could not prevent.
Also new
JavaException.causes, walkinggetCause(), withisCausedByandcauseOf.
Libraries rewrap relentlessly, so the class that explains a failure is usually
not the one thrown — and a timeout is retryable where the same wrapper around a
validation failure is not.JavaArray.toBytes()→Uint8List, because Java'sbyteis signed and every
Dart API that consumes bytes is not.Jvm.isAttached/created,systemProperty,resourceExists,
resourceUrls,requireClasses— so "something else booted the VM without my
jars" fails at startup rather than as aNoClassDefFoundErroran hour later.localFrameReturning, for the case a scope cannot serve: returning a reference
fromlocalFrameyields a dangling handle that nothing reports.jvm.classFor(name)andJavaClass.enumConstant, so the member-id cache pays
off by default instead of by discipline.jvm.synchronized(object, body)andjvm.refTypeOf(ref).newJava('(byte[])')accepting the declaration the way Java source writes it.- A README section on caller-sensitive JDK APIs —
Logger.getLogger,
Class.forName(String)and friends — which fail under JNI because
Reflection.getCallerClass()has no Java frame to inspect.
Verification
467 tests, up from 366. The ones that matter cover failures that abort the
process rather than throw, so they run as separate processes: several isolates
each binding their own handler class, and Jvm.isAttached proven in both states.
dart format, dart analyze --fatal-infos --fatal-warnings and dart doc --dry-run clean on Linux and macOS.
1.2.0
Signatures written as Java, so a JNI descriptor never has to be typed by hand.
A descriptor fails in the least helpful way there is: J is long and I is int, Z is boolean, a class needs L, a trailing ; and slashes instead of dots — and any of it wrong surfaces as a NoSuchMethodError from inside the VM, pointing at nothing.
Declarations
Name and types written once, as Java:
instance.callJava('String greet()');
fixtures.callJavaStatic('int add(int, int)', [2, 40]);
clazz.newJava('(String, int)', ['demo', 7]);
object.getJavaField('int intField');
object.setJavaField('String stringField', 'text');Also callJavaAs<T> / callJavaStaticAs<T>, and getJavaStaticField / setJavaStaticField.
The declaration is what javap prints, so it pastes in unedited — modifiers, annotations, parameter names, generic arguments and a throws clause are ignored, java.lang is implicit, varargs count as an array:
| Written | Descriptor |
|---|---|
jsig('public static int add(int a, int b)') |
(II)I |
jsig('java.util.List<String> subList(int, int)') |
(II)Ljava/util/List; |
jsig('void write(byte[]) throws java.io.IOException') |
([B)V |
jsig('String format(String, Object...)') |
(Ljava/lang/String;[Ljava/lang/Object;)Ljava/lang/String; |
jsig('(String, int)') |
(Ljava/lang/String;I)V (constructor) |
jtype('int[][]') |
[[I |
Typed builder
Impossible to malform, and const-constructible for a signature on a hot path:
const add = JSig.of([JType.int_, JType.int_], returns: JType.int_);
JType.of('java.util.List').array; // [Ljava/util/List;
JType.int_.array.array; // [[IJavaMethod.parse and JavaField.parse expose the parsed form when the name and the signature are wanted separately.
Notes
- Nothing is deprecated.
jsigandjtypereturn plain descriptor strings, so they drop into the existingcall/callStatic/getField, and every 1.1.1 call site keeps working. - Declarations are parsed once and cached by string, the same reasoning as the member-id cache: a call in a loop re-parses nothing.
- A malformed declaration raises a
JniErrorat the call site naming the token that failed, rather than aNoSuchMethodErrorfrom the VM later. That is still a runtime check — the typed builder is the compile-time one. - 366 tests (56 new), on Linux and macOS, and on the declared 3.10 SDK floor.
Full changelog: https://github.com/gmpassos/java_interop/blob/main/CHANGELOG.md
1.1.1
First release on pub.dev. Repository, examples and CI — no library changes: the only edit under lib/ since 1.1.0 is a corrected doc comment, so upgrading changes nothing at runtime.
Compatibility
- SDK floor drops from
^3.12.2to^3.10.0. CI runsdart analyzeand the full suite on a pinned 3.10.0 SDK as well as on stable, so the floor is exercised rather than asserted.
Examples
- Three task-oriented examples, none needing a jar or a class path:
jdk_apis.dart(SHA-256 viaMessageDigest, locale-aware currency viaNumberFormat/Locale, a deflate/inflate round trip using a Java array as a buffer Java writes into),collections.dart(ArrayList/HashMapdriven with plain Dart values, ending in reusabledartListFrom/dartMapFrom), andperformance.dart(holding aJavaClassfor its member-id cache, scoping references withlocalFrame, with timings). example/example.mdindexes all five and points at the right one for a given task.- The tour in
example/main.dartis rewritten against the JDK, so it needs no jar either. - The greeter moved to
example/greeter/, a standalone project with a path dependency on this package.
Project layout
- The test suite and the greeter example each own everything they build and run from —
test/java/→test/build/fixtures.jar,example/greeter/java/→example/greeter/build/greeter.jar— with ajava_home.shapiece. They previously shared onejava/directory and one jar.
Repository
- Apache 2.0
LICENSE. - GitHub Actions CI: format, analyze and
dart doc; the full suite and every example on Linux and macOS, since locating and loadinglibjvmis the per-platform part of this package. - Coverage on both platforms, uploaded to Codecov (95%).
Fixed
- The package's own usage snippet told readers to load
build/fixtures.jar, a filename that never existed in the repository.
Full changelog: https://github.com/gmpassos/java_interop/blob/main/CHANGELOG.md