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.