java_interop 1.1.1
java_interop: ^1.1.1 copied to clipboard
Call Java from pure Dart over JNI, using only dart:ffi and no Flutter SDK. Boots or attaches to a JVM in-process, loads jars, and calls constructors, methods, fields and arrays.
Changelog #
1.1.1 #
First release on pub.dev. Repository, examples and CI — no library changes:
the only edit under lib/ is a corrected doc comment, so upgrading from 1.1.0
changes nothing at runtime.
Compatibility #
- The SDK floor drops from
^3.12.2to^3.10.0. Nothing in the package needed 3.12, and CI now 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 of which needs a jar or a class path:
jdk_apis.dart(SHA-256 throughMessageDigest, locale-aware currency throughNumberFormatandLocale, a deflate/inflate round trip using a Java array as a buffer Java writes into),collections.dart(ArrayListandHashMapdriven with plain Dart values, ending in reusabledartListFromanddartMapFromconverters), andperformance.dart(holding aJavaClassfor its member-id cache, and scoping references withlocalFrame, with timings). example/example.mdindexes all five, says which needs a jar, 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.String.valueOf's per-primitive overloads make a better demonstration than the old fixtures did: the descriptor alone picks the overload, and the text that comes back proves the value landed in the right bytes of itsjvalueslot.Arrays.sortshows an array Java mutates in place, which the previous example could not. - The greeter moved to
example/greeter/, a standalone project with a path dependency on this package — so it consumes the public API the way a real consumer does, and a gap in the exports fails there first.
Project layout #
- The test suite and the greeter example each own everything they build and run
from:
test/java/→test/build/fixtures.jarviatest/build.sh, andexample/greeter/java/→example/greeter/build/greeter.jarvia its ownbuild.sh, with ajava_home.shapiece. Previously they shared onejava/directory and one jar, so each compiled and loaded the other's classes. - Root
build.shdelegates to both.test.shandrun.shuse the one belonging to what they run. - Two tests that used the greeter's class moved to the fixtures, for shapes the fixtures already had.
Repository #
- Apache 2.0
LICENSE. - GitHub Actions CI: format, analyze (
--fatal-infos --fatal-warnings) anddart docon Linux; the full suite and every example on Linux and macOS, since locating and loadinglibjvmis the per-platform part of this package. - Coverage collected on both platforms and uploaded to Codecov, gated on the token being present so a fork or a clone without it still builds green.
- Badges for CI, coverage, last commit, open pull requests and code size.
Fixed #
- The package's own usage snippet told readers to load
build/fixtures.jar, a filename that never existed anywhere in the repository.
1.1.0 #
Arrays, boxed primitives and member-id caching at the ergonomic layer. The three gaps that forced a drop to raw JNI for ordinary Java code.
Arrays #
JavaArray, aJavaObjectthat carries its element descriptor. An array result arrives as one, andtoList()reads it as the matching Dart type: a typed list for primitives (Int32List,Float64List, …),List<String?>forString[], unboxed values for a wrapper array, and nestedJavaArrays forint[][].- A Dart
Listmay now be passed for any array parameter or field, nested lists included; the temporary array is released after the call. JavaArray.of,.sized, and typed factories (ofInts,ofStrings, …), with[]/[]=element access.- New raw helpers on
JvmArrays:newArray,arrayToList,getArrayElement,setArrayElement,getStringArray,getObjectArray,elementClass.
Boxed primitives #
- A Dart
bool,intordoublepassed where a wrapper (Ljava/lang/Integer;…) or an erased type (Object,Number,Comparable,Serializable) is declared is boxed automatically and released afterwards. Generic APIs —List.add,Map.put— take plain Dart values now. - For an erased parameter the wrapper is inferred:
bool→Boolean,double→Double,int→Integerwhen it fits in 32 bits, elseLong.jvm.boxLongand friends override it when the width is observable. JvmBoxingonJvm:box,boxAs,boxInt/boxLong/…,unbox,unboxAs,wrapperOf,canBox,isJavaString;JavaWrappernames the eight wrapper classes. Wrapper classes and their ids are resolved once per VM.JavaObject.toDart()converts a value whose declared type was onlyObject, by asking the VM what it actually is.
Performance #
JavaClasscaches everyjmethodID/jfieldIDit resolves — ids stay valid while the class is loaded, which the class reference guarantees — so a call in a loop pays forGetMethodIDonce rather than once per invocation.JavaObjectholds the runtime class it resolves instead of looking it up and releasing it on every call, field read and field write.JavaClass.nameis resolved lazily; wrapping ajclassno longer costs three JNI calls for a name that is usually only used in error messages.
Also #
JavaObject.isInstanceOf,JavaClass.methodId/staticMethodId/fieldId/staticFieldId(cached), andJavaClass.cachedMemberCount.- Calling a method or touching a field on a Java
nullnow throws aJniErrorinstead of handing a nulljobjectto JNI, where it is undefined behaviour.
Breaking #
- A method or field declared to return a primitive wrapper is now unboxed:
callStatic('valueOf', '(I)Ljava/lang/Integer;', [42])returns42, not aJavaObject. Usejvm.boxInt(42)to get the object. - A result declared as an array type is now a
JavaArrayrather than a plainJavaObject.JavaArrayis aJavaObject, so anas JavaObjectcast still holds;as JavaObjectfollowed by raw array calls on.refstill works too. JavaClass's third constructor argument is optional, andrelease()clears the member cache.
1.0.0 #
Initial release: a complete, tested Dart↔Java bridge built on dart:ffi alone.
JVM lifecycle #
Jvm.startOrAttachboots a JVM, or attaches to the one the process already has (viaJNI_GetCreatedJavaVMs), so multiple isolates can share one VM.- Survives the startup race: when two threads call
JNI_CreateJavaVMat once, the loser waits for the winner's VM to be published instead of failing withJNI_EEXIST. JNIEnv*is re-resolved per call throughGetEnv, attaching the current thread when needed — a Dart isolate is not pinned to an OS thread, so a cached env would be a latent crash.Jvm.destroy,detachCurrentThread,version, andfnSlotas an escape hatch for unwrapped JNI functions.
Calls and members #
- Classes by dotted or JNI name, including nested (
Outer$Inner) classes;getObjectClass,getSuperclass,isInstanceOf,isAssignableFrom. - Constructors and instance/static methods for all ten JNI return types
(
void, the eight primitives, and references). - Instance and static fields, read and written, for all types.
- Argument coercion driven by the parsed method signature, which is what makes
a Dart
doubleland as eitherjfloatorjdoublecorrectly.
Data #
- Strings via the UTF-16 (
jchar) family, so astral characters and embedded NULs survive a round trip — JNI's "UTF-8" is modified UTF-8 and does not. - Arrays: creation, length, bulk region read/write for all eight primitive types, plus object arrays with null elements.
Errors and references #
- Java throwables become
JavaExceptioncarrying the class name, message and Java stack trace, with the pending exception always cleared first. throwJavaraises a Java exception from Dart.- Explicit
JavaRefownership with local/global references,toGlobal,isSameObject, scopedlocalFrame, and a clear error on use-after-release instead of a dangling pointer.
Ergonomics #
JavaClass/JavaObjectdispatch from the signature string, convert Dart arguments (includingString), and release temporaries automatically.JniSignatureparses and builds descriptors, rejecting malformed ones at the call site rather than as a confusingNoSuchMethodErrorfrom the VM.searchLibjvm/defaultLibjvmPathlocate a JDK and report every path tried.