java_interop 1.4.0
java_interop: ^1.4.0 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.
java_interop #
Call Java from pure Dart, over JNI, with nothing but dart:ffi.
Boots a JVM inside your Dart process (or attaches to the one already running), loads jars from a class path, and calls constructors, methods, fields and arrays across the full JNI type surface. Java throwables arrive as Dart exceptions carrying the class name, message and Java stack trace.
$ ./run.sh
Hello, Dart! (from Java 21.0.12)
Greeter.add(2, 40) = 42
Greeter.divide(1, 0) threw: JavaException: java.lang.ArithmeticException: / by zero
⚠️ One JVM per process #
JNI_CreateJavaVMmay be called once per process, and HotSpot cannot start another afterJvm.destroy().Jvm.startOrAttachtherefore reuses an existing VM when it finds one, which is what makes this work underdart test— every suite is a separate isolate inside one shared process. A consequence: the class path is fixed by whoever creates the VM first; JNI offers no way to extend it later.
Why not package:jni? #
Every published version of package:jni — 0.1.1 through 1.0.3 — declares a
Flutter requirement:
environment:
sdk: '>=3.3.0 <4.0.0'
flutter: '>=3.35.6'
dart pub add jni therefore fails outright on a Flutter-free SDK with "jni
requires the Flutter SDK", even though nothing about loading a jar needs
Flutter. The neighbouring packages do not fill the gap either: jnigen has a
Dart-only environment but only generates bindings that import 'package:jni/jni.dart'; jni_util is a path helper with no JNI in it; and
apollovm interprets a subset of Java source and cannot load a compiled jar.
So this package binds the JNI Invocation API directly with dart:ffi — the same
C API package:jni wraps. The only dependency is package:ffi, which is pure
Dart (it supplies the native allocator and UTF-8 helpers that dart:ffi itself
does not expose).
Features #
- Boot or attach.
Jvm.startOrAttachcreates a JVM, or finds the one this process already has viaJNI_GetCreatedJavaVMs. Multiple isolates share one VM safely, including when several of them start at the same instant. - Thread-correct by construction.
JNIEnv*is thread-affine and a Dart isolate is not pinned to an OS thread — it can resume on a different one after anawait. Every call re-resolves the env throughGetEnv, attaching the current thread when needed, so a cached pointer can never go stale. - Complete call surface. Constructors, instance methods and static methods
for all ten JNI return types (
void, the eight primitives, references). - Fields. Instance and static, read and written, for every type.
- Signature-driven dispatch.
JavaClass/JavaObjectparse the descriptor and pick the right JNI accessor for you — callingCallIntMethodon a method that returnslongis undefined behaviour in raw JNI, not an error. - Signatures written as Java.
callJava('int add(int, int)')instead ofcall('add', '(II)I'). Paste the declaration from the Java source or fromjavap: modifiers, parameter names, generics andthrowsare ignored. A typed builder (JSig,JType) covers the cases you wantconstor programmatic. - Correct strings. The UTF-16 (
jchar) family, so emoji, CJK and embedded NULs survive a round trip. JNI's "UTF-8" is modified UTF-8 and does not. - Arrays. A
JavaArraythat carries its element type: a DartListpasses as an array argument, an array result reads back as a typed list (Int32List,List<String?>, nestedJavaArrays), and elements are indexable. Underneath, creation, length, and bulk region read/write for all eight primitive types, plus object arrays including null elements. - Boxing. A Dart number,
boolorStringpassed where a wrapper or an erasedObjectis declared is boxed intoInteger/Long/Double/… and released afterwards; a declared wrapper result is unboxed back to anint,doubleorbool.toDart()does the same for a value whose declared type was onlyObject— which, after erasure, is every generic API. - Member ids are cached.
JavaClasscaches eachjmethodID/jfieldIDit resolves, and aJavaObjectholds the runtime class it looked up, so a call in a loop pays forGetMethodIDonce instead of once per invocation.jvm.classFor(name)keeps one instance per class for the process, so the cache pays off by default rather than by remembering to hold the class yourself. - Explicit reference ownership. Local and global references,
toGlobal,isSameObject, scopedlocalFrame, and a clear Dart error on use-after- release instead of handing the VM a dangling pointer.localFrameReturningcovers the case a scope cannot: building one reference worth keeping, without the dangling handle that returning fromlocalFramesilently produces. - Exceptions both ways. Java throwables become
JavaException(class name, message, Java stack trace, and thegetCause()chain — which is where the class that explains the failure usually is, since libraries rewrap), always cleared before returning so the next call is not poisoned;throwJavaraises one from Dart. - Binary data.
JavaArray.toBytes()reads abyte[]straight into aUint8List, because Java'sbyteis signed and everything in Dart that consumes bytes is not. - Answers about the environment.
isAttached(did this call create the VM, or attach to one whose class path you cannot see?),systemProperty,resourceExists/resourceUrls, andrequireClasses— which fails at startup naming the class and the artifact, instead of leaving aNoClassDefFoundErrorto surface from inside a library later. - Java interfaces implemented in Dart.
jvm.implementInterface(...)returns a real Java object: listeners, visitors,Comparator, SPI. Reentrant calls (the usual kind — Java is only running because Dart called it) are dispatched synchronously and return a value; calls from a thread the JVM owns are queued when they arevoidand refused with a clear Java exception when they are not, so nothing can deadlock. - Monitors.
jvm.synchronized(object, body)takes the same lock Java'ssynchronizeddoes, which matters as soon as a Dart callback can mutate state Java's other threads read. - An escape hatch.
Jvm.fnSlot(index)reaches any JNI function this binding does not wrap. - Tested. 467 tests across unit and integration suites, covering every primitive type, both directions of every conversion, arrays and boxing, signature parsing, the reference lifecycle, the exception paths including cause chains and cycles, proxies across threads and isolates, and the multi-isolate startup race.
Architecture #
Your Dart code
│
JavaClass / JavaObject / JavaArray ← signature-driven, converts + releases
│
extensions on Jvm (JvmCalls, JvmFields, JvmArrays, JvmStrings, JvmClasses)
│ ← 1:1 with the JNI C API
Jvm.fnSlot(i) ← escape hatch: any unwrapped JNI function
│
JNIEnv* function table ← resolved per call via GetEnv
│
libjvm (JNI Invocation API)
│
HotSpot
lib/
├── java_interop.dart # public library (exports)
└── src/
├── jvm.dart # lifecycle, per-call env resolution, exceptions
├── jni_slots.dart # function-table indices, read out of jni.h
├── native_types.dart # dart:ffi typedefs and structs
├── jvm_classes.dart # class / method / field resolution
├── jvm_calls.dart # constructors + calls, every return type
├── jvm_fields.dart # instance and static field access
├── jvm_strings.dart # UTF-16 string conversion
├── jvm_arrays.dart # primitive and object arrays
├── java_ref.dart # owned references, local frames
├── java_class.dart # JavaClass / JavaObject ergonomic layer
├── signatures.dart # JniType + JniSignature parse/build
├── jvalue.dart # the 8-byte jvalue union
├── java_home.dart # libjvm discovery
└── errors.dart # JniError, JniLookupError, JavaException
Getting started #
You need a JDK — a JRE is not enough, since libjvm ships with the JDK.
brew install openjdk@21
That formula is keg-only, so /usr/libexec/java_home will not find it unless
you symlink it. The java_home.sh each project carries checks the Homebrew
prefixes first, which is why the scripts below work with no shell setup:
./build.sh # both jars: the test fixtures and the greeter's own
./run.sh # builds if needed, then runs the greeter example
./test.sh # builds if needed, then runs the test suite
To run dart directly, export JAVA_HOME yourself:
export JAVA_HOME="$(brew --prefix openjdk@21)/libexec/openjdk.jdk/Contents/Home"
dart run example/main.dart
Usage #
Call a class from a jar #
import 'package:java_interop/java_interop.dart';
final jvm = Jvm.startOrAttach(classPath: ['example/greeter/build/greeter.jar']);
final greeter = JavaClass.forName(jvm, 'com.nfeflash.example.Greeter');
final instance = greeter.newInstance('(Ljava/lang/String;)V', ['Dart']);
print(instance.call('greet', '()Ljava/lang/String;')); // Hello, Dart! ...
print(greeter.callStatic('add', '(II)I', [2, 40])); // 42
instance.release();
greeter.release();
Class names take either form — java.lang.String or java/lang/String — and
nested classes keep their $: com.example.Outer$Inner.
Call the JDK #
final math = JavaClass.forName(jvm, 'java.lang.Math');
math.callStatic('abs', '(I)I', [-5]); // 5 (int)
math.callStatic('abs', '(D)D', [-5.5]); // 5.5 (double)
math.getStaticField('PI', JniType.double_); // 3.14159...
final list = JavaClass.forName(jvm, 'java.util.ArrayList');
final instance = list.newInstance();
instance.call('add', '(Ljava/lang/Object;)Z', ['first']);
instance.javaToString(); // [first]
Math.abs is overloaded for int, long, float and double. The signature
you pass selects both the Java overload and the JNI accessor used to read the
result, so (I)I returns a Dart int and (D)D a Dart double.
Writing signatures as Java, not as descriptors #
A descriptor is easy to get subtly wrong, and it fails in the least helpful way:
J is long and I is int, Z is boolean, a class needs L, a trailing
; and slashes rather than dots — and a mistake surfaces as a
NoSuchMethodError from inside the VM. Write the Java declaration instead:
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');
The declaration is what you would read in the Java source or in javap output,
so it can be pasted in unedited — modifiers, annotations, parameter names,
generic arguments and a throws clause are all ignored, java.lang is implicit,
and varargs count as an array:
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;
jtype('int[][]') // [[I
jsig and jtype return the descriptor string, so they drop into the existing
call / callStatic / getField unchanged. For a signature built
programmatically — or one you want const — there is a typed builder that
cannot produce a malformed descriptor:
const add = JSig.of([JType.int_, JType.int_], returns: JType.int_); // (II)I
JSig.ctor([JType.string]); // (Ljava/lang/String;)V
JType.of('java.util.List'); // Ljava/util/List;
JType.int_.array.array; // [[I
Both routes produce the same JSig. Declarations are parsed once and cached, so
a call in a loop re-parses nothing.
A malformed declaration is a
JniErrorat the call, naming the token that failed — not aNoSuchMethodErrorfrom the VM later. It is still a runtime check; the typed builder is the compile-time one.
Results and arguments #
| Java | Dart argument | Dart result |
|---|---|---|
boolean |
bool |
bool |
byte char short int long |
int |
int |
float double |
double (or int) |
double |
void |
— | null |
java.lang.String |
String or null |
String? |
Integer Long Double … |
int / double / bool |
int? / double? / bool? |
Object Number Comparable |
int / double / bool (boxed, see below) |
JavaObject |
| any array | List, JavaArray, or null |
JavaArray |
| any other reference | JavaObject, JavaRef, or null |
JavaObject |
The rule is that a declared type Dart has a direct equivalent for is converted
in both directions, and everything else is a JavaObject you own and must
release(). Conversions that allocate in the VM — a jstring for a String, a
box for a number, an array for a List — are released for you afterwards, even
if the call throws.
Use callAs<T> / callStaticAs<T> when you want the cast done for you:
final sum = fixtures.callStaticAs<int>('staticSum', '(II)I', [2, 40]);
Fields #
object.getJavaField('int intField'); // read
object.setJavaField('String stringField', 'text'); // write (auto-converted)
fixtures.getJavaStaticField('int staticIntField');
fixtures.setJavaStaticField('String staticStringField', 'x');
Or with the descriptor spelled out, which is what the above resolves to:
object.getField('intField', JniType.int_);
object.setField('stringField', JniType.string, 'text');
Arrays #
An array result is a JavaArray that carries its element descriptor, so one
type covers all nine element kinds:
final fromJava = fixtures.callStatic('intArray', '()[I') as JavaArray;
fromJava.toList(); // Int32List [-2147483648, 0, 2147483647]
fromJava[0]; // -2147483648
fromJava.release();
// A String[] reads back as Dart strings, nulls preserved; an Integer[] unboxes.
(fixtures.callStatic('stringArray', '()[Ljava/lang/String;') as JavaArray)
.toList(); // ['one', null, 'três']
A Dart List passed for an array parameter is converted and released for you:
fixtures.callStatic('sumInts', '([I)I', [[1, 2, 3, 4]]); // 10
fixtures.callStatic('sumNested', '([[I)I', [[[1, 2], [3]]]); // 6
fixtures.callStatic('joinStrings', sig, [['a', 'b', 'c']]); // a,b,c
Or build one to keep across calls, and mutate it in place:
final ints = JavaArray.ofInts(jvm, [10, 20, 30]);
ints[0] = 100;
fixtures.callStatic('sumInts', '([I)I', [ints]); // 150
ints.release();
JavaArray.sized(jvm, JniType.string, 3); // String[3], null-filled
toList accepts a start/length window, and an out-of-range one is rejected
in Dart with the array's real length in the message rather than reaching the VM.
The JvmArrays extension underneath is unchanged if you want the raw
newIntArray / getIntArray / region calls.
Boxed primitives #
Java's autoboxing is a compiler feature, so JNI never does it. This binding does, driven by the declared descriptor:
// A declared wrapper is exact in both directions.
fixtures.callStatic('unboxInteger', '(Ljava/lang/Integer;)I', [42]); // 42
fixtures.callStatic('boxInteger', '(I)Ljava/lang/Integer;', [7]); // 7 (an int)
// Which makes generic APIs — everything erased to Object — usable directly.
list.call('add', '(Ljava/lang/Object;)Z', [1]);
map.call('put', '(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;',
['answer', 42]);
For an erased parameter (Object, Number, Comparable, Serializable)
the wrapper has to be inferred, which is the one place this binding guesses what
javac would have known statically: bool → Boolean, double → Double, and
int → Integer when it fits in 32 bits, otherwise Long. That split matters
for equals, so pass an explicit box when the Java side stores a specific width:
final long = jvm.boxLong(42); // ... boxInt, boxDouble, boxChar, boxAs(...)
fixtures.callStatic('classOf', classOfSig, [long]); // java.lang.Long
long.release();
Going the other way, a method declared to return Object still gives a
JavaObject — only the VM knows what is inside it. toDart() asks:
final value = map.call('get', '(Ljava/lang/Object;)Ljava/lang/Object;', ['answer'])
as JavaObject;
value.toDart(); // 42 — an int, a double, a bool, a String, or the object itself
value.release();
Exceptions #
try {
fixtures.callStatic('divide', '(II)I', [1, 0]);
} on JavaException catch (e) {
e.className; // java.lang.ArithmeticException
e.message; // / by zero
e.isA('ArithmeticException'); // true — matches simple or qualified names
e.stackTraceText; // the Java stack trace
}
// The VM is immediately usable again.
jvm.throwJava('java.lang.IllegalStateException', 'from Dart');
Read causes before deciding what a failure means. Libraries rewrap
constantly — catch (Exception e) { throw new Wrapper(e.getMessage(), e); } — so
the class that identifies the problem is often not className but a link
further down the chain:
try {
api.callJava('void send()');
} on JavaException catch (e) {
e.className; // com.example.ApiException — the wrapper
e.causes; // [java.net.SocketTimeoutException: connect timed out, …]
if (e.isCausedBy('java.net.SocketTimeoutException')) {
// Retryable. The same wrapper around a validation failure is not, and only
// the chain tells the two apart.
}
e.causeOf('IOException')?.message;
}
Caller-sensitive JDK APIs #
Some JDK methods look at who called them — Reflection.getCallerClass() — to
pick a class loader, a module, or a resource bundle. Called over JNI there is no
Java frame above them, so the caller comes back null and the method fails in a
way that has nothing to do with what you asked for:
NullPointerException: Cannot invoke "java.lang.Class.getModule()"
because "caller" is null
This is a property of those APIs, not a gap in this binding: the same call from a
main works fine. Each has an overload that takes explicitly what it would
otherwise have inferred.
| Instead of | Call |
|---|---|
Logger.getLogger(name) |
LogManager.getLogManager().readConfiguration(stream) — and it applies to loggers created later, too |
Class.forName(name) |
Class.forName(name, true, loader), or jvm.findClass |
ResourceBundle.getBundle(name) |
ResourceBundle.getBundle(name, locale, loader) |
DriverManager.getConnection(url) |
a DataSource obtained explicitly |
MethodHandles.lookup() |
MethodHandles.privateLookupIn(someClass, …) |
Configuring java.util.logging is the one most projects hit, because a library
that logs at INFO will otherwise flood stderr:
// Not Logger.getLogger(...).setLevel(...) — that throws under JNI.
final manager = jvm.classFor('java.util.logging.LogManager');
final properties = utf8.encode('org.noisy.library.level=WARNING\n');
// ... hand `properties` to readConfiguration via a ByteArrayInputStream ...
References and frames #
// Scoped: everything created inside is freed on the way out.
final total = jvm.localFrame(() {
var length = 0;
for (var i = 0; i < 1000; i++) {
length += jvm.stringLength(jvm.newString('value $i'));
}
return length;
});
// A global reference survives the frame — and the thread.
late JavaRef kept;
jvm.localFrame(() => kept = jvm.newString('kept').toGlobal());
jvm.stringFrom(kept);
kept.release();
Implementing a Java interface in Dart #
A library that wants a listener, a visitor or a Comparator wants an object
implementing an interface. implementInterface builds one:
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 {
jvm.classFor('java.util.Arrays').callJavaStatic(
'void sort(Object[], java.util.Comparator)',
[values, ordering.instance],
);
} finally {
ordering.release();
}
Arguments arrive as Dart values — null, bool, int, double, String, or a
JavaObject. The return value is converted for the method's declared type, so
returning 1 from a method declared byte is fine: Dart has one integer type and
the narrowing happens on the Java side, where the declared type is known. Several
interfaces at once (implementInterfaces), per-method handlers
(handlers: {'visitStaticText': …}), and call.method for the underlying
java.lang.reflect.Method are all there.
toString, equals and hashCode are answered in Java by default — identity
equality, identity hash, a name listing the interfaces — because Proxy routes
them to the handler like anything else, and a proxy without them breaks the moment
it reaches a HashMap or a log line. Pass forwardObjectMethods: true to take
them yourself.
Which thread calls matters. A reentrant call — Java calling back during a call
Dart made, which is nearly all of them — runs synchronously on the isolate's own
thread and returns a value. A call from a thread the JVM owns cannot: Dart aborts
the process if an isolateLocal callback is entered from another thread, and
blocking that thread until the isolate answers deadlocks as soon as the isolate is
itself inside a JNI call. So a void method is queued and runs on the next turn
of the event loop (call.isQueued is true), and a method with a return value
throws IllegalStateException in Java naming both threads. Neither can hang.
An isolate is not pinned to an OS thread, so which thread is "the isolate's" is
re-checked whenever you read proxy.instance. If you cache the JavaObject and
hand it to Java after an await that moved the isolate, call
proxy.bindCurrentThread() first — otherwise the call is refused, with a message
saying so.
There is no jar. The one Java class this needs
(java/dart/jni/DartInvocationHandler.java) is compiled by
tool/gen_proxy_class.dart into checked-in bytes and loaded at runtime with JNI
DefineClass, into a class loader created per isolate. That last part is not
cosmetic: RegisterNatives binds to a class, so a shared class would let the
last isolate to bind steal every other isolate's callbacks — and a callback
entered from the wrong isolate's thread aborts the process.
Dropping to raw JNI #
The extensions on Jvm mirror the C API one-to-one when you want the control:
final clazz = jvm.findClass('com.nfeflash.example.Fixtures');
final method = jvm.staticMethodId(clazz, 'staticSum', '(II)I');
jvm.callStaticIntMethod(clazz, method, [JValue.fromInt(20), JValue.fromInt(22)]);
And for a JNI function this binding does not wrap at all, fnSlot gives you the
raw function pointer to cast and call yourself.
How it works #
The function tables #
JNIEnv* and JavaVM* are each a pointer to a pointer to a table of function
pointers. There are no exported symbols to look up — apart from
JNI_CreateJavaVM and JNI_GetCreatedJavaVMs — so every call means
dereferencing to the table, indexing the right slot, and casting that slot to
the right signature:
Pointer<Void> fnSlot(int index) =>
env.cast<Pointer<Pointer<Void>>>().value[index];
final fn = Jvm.fnSlotOf(env, JniFn.findClass)
.cast<NativeFunction<FindClassC>>()
.asFunction<FindClassDart>();
The indices in JniFn were extracted from $JAVA_HOME/include/jni.h of
OpenJDK 21 by numbering the members of struct JNINativeInterface_ (235 slots,
including the four leading reserved entries), not transcribed by hand. The
table is append-only across JNI versions, so they hold for any JNI ≥ 1.6 VM.
Only the …A call variants #
The bare CallObjectMethod family is C variadic, and arm64 (macOS and Linux)
passes variadic arguments under a different calling convention than fixed ones —
calling them through a non-variadic FFI signature would silently corrupt
arguments. The A variants take a jvalue* array instead and are correct
everywhere.
The jvalue union #
jvalue is an 8-byte union, and the A variants read whichever member the
declared parameter type names. On a little-endian 64-bit target every integral
and reference member lives in the low bytes, so they can all be staged through
an int. Floats are the exception: jfloat is a 4-byte IEEE-754 value, so its
bit pattern has to land in the low 4 bytes — which is why coercing a Dart
double needs the signature to know whether it is a jfloat or a jdouble.
Threads and JNIEnv* #
A JNIEnv* belongs to one thread. A Dart isolate is not pinned to an OS
thread — it can resume on a different one after an await — so this binding
never caches it. Jvm.env calls GetEnv on every access (a thread-local read
in the VM) and falls back to AttachCurrentThread when the thread is new.
The startup race #
HotSpot rejects a second JNI_CreateJavaVM with JNI_EEXIST as soon as the
first one starts, but only publishes the VM to JNI_GetCreatedJavaVMs once it
has finished booting. A thread that loses the race therefore sees neither a
usable VM nor a creatable one. startOrAttach waits for the winner to finish
instead of failing.
This is not hypothetical: dart test runs suites concurrently as isolates of
one process, and without this the suite fails intermittently. There is a
regression test (test/integration/startup_race_test.dart) that runs the race
in a fresh process, since it cannot recur once any VM exists.
Strings #
NewStringUTF / GetStringUTFChars speak modified UTF-8: U+0000 is encoded
as two bytes, and characters outside the BMP as a surrogate pair of three bytes
each. A Dart string containing an emoji or a NUL does not survive that round
trip. Dart strings are already UTF-16, so this binding uses NewString /
GetStringChars (the jchar family), which is both exact and cheaper.
Limitations #
- One JVM per process, fixed class path, no restart after
destroy(). These are HotSpot limitations, not limitations of this binding. - No
GetArrayElements. Only the region-copy API is bound. The elements family may hand back a direct pointer into the Java heap and pin it, which is easy to leak and blocks the GC while held. - No weak references. Global and local only.
- Overloads are not resolved from argument types. Every call names its descriptor, or a Java declaration that yields one.
- A proxy cannot answer a JVM-owned thread that wants a value.
voidmethods from such threads are queued; anything else is refused. See above for why blocking there cannot be made safe. - A live proxy keeps its isolate alive, since a JVM thread may still queue a call to it. Release it (or the whole runtime) and the isolate ends normally.
- Boxing into an erased
Objectis inferred, not known. A DartintpicksIntegerorLongby width, which is not always what javac would have chosen; box explicitly when the difference is observable. - The member-id cache lives on a
JavaClass. TwoJavaClass.forNamecalls for the same class do not share one — usejvm.classFor(name), which keeps a single instance per class. An object that resolved its own runtime class holds it until released. - Some JDK APIs are caller-sensitive and fail over JNI because there is no Java frame to inspect. See the section above for the ones that matter and what to call instead.
- No custom class loaders. Classes come from the class path the VM was created with. (The proxy machinery creates one for itself, to give each isolate's handler class a distinct identity, but it loads nothing from it.)
JavaObjectdoes not auto-release. Ownership is explicit; userelease()orlocalFrame. There is no finalizer, because aJNIEnv*is thread-affine and a Dart finalizer offers no guarantee about which thread it runs on.
Running the example and tests #
./build.sh # both jars, each beside the sources it comes from
./run.sh # the small greeter example (example/greeter/)
dart run example/main.dart # a tour of the whole feature set — no jar needed
./test.sh # the full suite (builds the fixtures first)
dart test test/unit # unit tests only — no JVM needed
Each project builds its own jar from its own Java sources, so nothing is shared between the suite and the examples:
| Project | Sources | Jar |
|---|---|---|
| test suite | test/java/ |
test/build/fixtures.jar |
| greeter example | example/greeter/java/ |
example/greeter/build/greeter.jar |
Every example except the greeter needs no jar and no class path, since they call classes the JVM already has:
dart run example/main.dart # the whole API surface, against the JDK
dart run example/jdk_apis.dart # SHA-256, locale currency, deflate/inflate
dart run example/collections.dart # ArrayList and HashMap from Dart values
dart run example/performance.dart # member-id caching and local frames
example/example.md indexes all five and says which to
read for a given task. The greeter is
its own project with a path dependency on this one, so it
exercises the package through its public API exactly as a consumer would.
The suite skips itself with an explanation when no JDK is installed, rather than
failing with a native error from inside DynamicLibrary.open.
Source #
github.com/gmpassos/java_interop — issues and pull requests welcome.
Author #
Graciliano M. Passos: gmpassos@GitHub.