From 16918aef9da60646e5ae0ad0307fe0e1a64355b2 Mon Sep 17 00:00:00 2001 From: Hossein Yousefi Date: Fri, 27 Sep 2024 04:20:19 +0200 Subject: [PATCH] [jnigen] Implement void interface methods as listeners (#1596) --- .../dart_lang/jni/PortProxyBuilder.java | 21 +++- pkgs/jni/lib/src/jimplementer.dart | 33 ++--- pkgs/jni/lib/src/jni.dart | 5 +- pkgs/jni/src/dartjni.c | 40 +++--- pkgs/jnigen/CHANGELOG.md | 1 + pkgs/jnigen/docs/interface_implementation.md | 48 +++++++- .../lib/src/bindings/dart_generator.dart | 116 +++++++++++++----- .../bindings/simple_package.dart | 48 +++++++- .../jnigen/interfaces/MyRunnableRunner.java | 6 + .../runtime_test_registrant.dart | 52 ++++++++ 10 files changed, 296 insertions(+), 74 deletions(-) diff --git a/pkgs/jni/java/src/main/java/com/github/dart_lang/jni/PortProxyBuilder.java b/pkgs/jni/java/src/main/java/com/github/dart_lang/jni/PortProxyBuilder.java index f4026189e..2f70b2b5d 100644 --- a/pkgs/jni/java/src/main/java/com/github/dart_lang/jni/PortProxyBuilder.java +++ b/pkgs/jni/java/src/main/java/com/github/dart_lang/jni/PortProxyBuilder.java @@ -5,8 +5,7 @@ package com.github.dart_lang.jni; import java.lang.reflect.*; -import java.util.ArrayList; -import java.util.HashMap; +import java.util.*; public class PortProxyBuilder implements InvocationHandler { private static final PortCleaner cleaner = new PortCleaner(); @@ -28,6 +27,7 @@ private static final class DartImplementation { private boolean built = false; private final long isolateId; private final HashMap implementations = new HashMap<>(); + private final HashSet asyncMethods = new HashSet<>(); public PortProxyBuilder(long isolateId) { this.isolateId = isolateId; @@ -72,8 +72,10 @@ private static void appendType(StringBuilder descriptor, Class type) { } } - public void addImplementation(String binaryName, long port, long functionPointer) { + public void addImplementation( + String binaryName, long port, long functionPointer, List asyncMethods) { implementations.put(binaryName, new DartImplementation(port, functionPointer)); + this.asyncMethods.addAll(asyncMethods); } public Object build() throws ClassNotFoundException { @@ -106,21 +108,28 @@ private static native Object[] _invoke( long functionPtr, Object proxy, String methodDescriptor, - Object[] args); + Object[] args, + boolean isBlocking); private static native void _cleanUp(long resultPtr); @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { DartImplementation implementation = implementations.get(method.getDeclaringClass().getName()); + String descriptor = getDescriptor(method); + boolean isBlocking = !asyncMethods.contains(descriptor); Object[] result = _invoke( implementation.port, isolateId, implementation.pointer, proxy, - getDescriptor(method), - args); + descriptor, + args, + isBlocking); + if (!isBlocking) { + return null; + } _cleanUp((Long) result[0]); if (result[1] instanceof DartException) { Throwable cause = ((DartException) result[1]).cause; diff --git a/pkgs/jni/lib/src/jimplementer.dart b/pkgs/jni/lib/src/jimplementer.dart index ea6784b7b..8b66b8a8f 100644 --- a/pkgs/jni/lib/src/jimplementer.dart +++ b/pkgs/jni/lib/src/jimplementer.dart @@ -5,15 +5,11 @@ import 'dart:ffi'; import 'dart:isolate'; -import 'package:ffi/ffi.dart'; import 'package:meta/meta.dart' show internal; +import '../jni.dart'; import 'accessors.dart'; import 'jni.dart'; -import 'jobject.dart'; -import 'jreference.dart'; -import 'lang/jstring.dart'; -import 'third_party/generated_bindings.dart'; import 'types.dart'; /// A builder that builds proxy objects that implement one or more interfaces. @@ -50,17 +46,17 @@ class JImplementer extends JObject { static final _addImplementationId = _class.instanceMethodId( r'addImplementation', - r'(Ljava/lang/String;JJ)V', + r'(Ljava/lang/String;JJLjava/util/List;)V', ); static final _addImplementation = ProtectedJniExtensions.lookup< NativeFunction< JThrowablePtr Function(Pointer, JMethodIDPtr, - VarArgs<(Pointer, Int64, Int64)>)>>( + VarArgs<(Pointer, Int64, Int64, Pointer)>)>>( 'globalEnv_CallVoidMethod') .asFunction< - JThrowablePtr Function( - Pointer, JMethodIDPtr, Pointer, int, int)>(); + JThrowablePtr Function(Pointer, JMethodIDPtr, Pointer, + int, int, Pointer)>(); /// Should not be used directly. /// @@ -71,15 +67,22 @@ class JImplementer extends JObject { RawReceivePort port, Pointer> pointer, + List asyncMethods, ) { using((arena) { _addImplementation( - reference.pointer, - _addImplementationId as JMethodIDPtr, - (binaryName.toJString()..releasedBy(arena)).reference.pointer, - port.sendPort.nativePort, - pointer.address) - .check(); + reference.pointer, + _addImplementationId as JMethodIDPtr, + (binaryName.toJString()..releasedBy(arena)).reference.pointer, + port.sendPort.nativePort, + pointer.address, + (asyncMethods + .map((m) => m.toJString()..releasedBy(arena)) + .toJList(JString.type) + ..releasedBy(arena)) + .reference + .pointer, + ).check(); }); } diff --git a/pkgs/jni/lib/src/jni.dart b/pkgs/jni/lib/src/jni.dart index 71967cd00..6d19e844d 100644 --- a/pkgs/jni/lib/src/jni.dart +++ b/pkgs/jni/lib/src/jni.dart @@ -273,7 +273,10 @@ extension ProtectedJniExtensions on Jni { /// Returns the result of a callback. static void returnResult( Pointer result, JObjectPtr object) async { - Jni._bindings.resultFor(result, object); + // The result is `nullptr` when the callback is a listener. + if (result != nullptr) { + Jni._bindings.resultFor(result, object); + } } static Dart_FinalizableHandle newJObjectFinalizableHandle( diff --git a/pkgs/jni/src/dartjni.c b/pkgs/jni/src/dartjni.c index 0ae465ef6..26b9cc351 100644 --- a/pkgs/jni/src/dartjni.c +++ b/pkgs/jni/src/dartjni.c @@ -418,14 +418,20 @@ Java_com_github_dart_1lang_jni_PortProxyBuilder__1invoke( jlong functionPtr, jobject proxy, jstring methodDescriptor, - jobjectArray args) { - CallbackResult* result = (CallbackResult*)malloc(sizeof(CallbackResult)); - if (isolateId != (jlong)Dart_CurrentIsolate_DL()) { - init_lock(&result->lock); - init_cond(&result->cond); - acquire_lock(&result->lock); - result->ready = 0; - result->object = NULL; + jobjectArray args, + jboolean isBlocking) { + CallbackResult* result = NULL; + if (isBlocking) { + result = (CallbackResult*)malloc(sizeof(CallbackResult)); + } + if (isolateId != (jlong)Dart_CurrentIsolate_DL() || !isBlocking) { + if (isBlocking) { + init_lock(&result->lock); + init_cond(&result->cond); + acquire_lock(&result->lock); + result->ready = 0; + result->object = NULL; + } Dart_CObject c_result; c_result.type = Dart_CObject_kInt64; @@ -448,18 +454,24 @@ Java_com_github_dart_1lang_jni_PortProxyBuilder__1invoke( Dart_PostCObject_DL(port, &c_post); - while (!result->ready) { - wait_for(&result->cond, &result->lock); - } + if (isBlocking) { + while (!result->ready) { + wait_for(&result->cond, &result->lock); + } - release_lock(&result->lock); - destroy_lock(&result->lock); - destroy_cond(&result->cond); + release_lock(&result->lock); + destroy_lock(&result->lock); + destroy_cond(&result->cond); + } } else { result->object = ((jobject(*)(uint64_t, jobject, jobject))functionPtr)( port, (*env)->NewGlobalRef(env, methodDescriptor), (*env)->NewGlobalRef(env, args)); } + if (!isBlocking) { + // No result is created in this case, there is nothing to clean up either. + return NULL; + } // Returning an array of length 2. // [0]: The result pointer, used for cleaning up the global reference, and // freeing the memory since we passed the ownership to Java. diff --git a/pkgs/jnigen/CHANGELOG.md b/pkgs/jnigen/CHANGELOG.md index 12b2f6120..dc71ab96f 100644 --- a/pkgs/jnigen/CHANGELOG.md +++ b/pkgs/jnigen/CHANGELOG.md @@ -19,6 +19,7 @@ - Added `JImplementer` which enables building an object that implements multiple Java interfaces. Each interface now has a static `implementIn` method that takes a `JImplementer` and the implementation object. +- Added the ability to implement void-returning interface methods as listeners. - Generating identifiers that start with an underscore (`_`) and making them public by prepending a dollar sign. - Fixed an issue where inheriting a generic class could generate incorrect code. diff --git a/pkgs/jnigen/docs/interface_implementation.md b/pkgs/jnigen/docs/interface_implementation.md index 01a25efce..f96c10642 100644 --- a/pkgs/jnigen/docs/interface_implementation.md +++ b/pkgs/jnigen/docs/interface_implementation.md @@ -36,20 +36,24 @@ class Runnable extends JObject { ) { /* ... */ } } -abstract interface class $Runnable { +abstract mixin class $Runnable { factory $Runnable({ required void Function() run, + bool run$async, }) = _$Runnable; + bool get run$async => false; void run(); } class _$Runnable implements $Runnable { _$Runnable({ required void Function() run, + this.run$async = false; }) : _run = run; final void Function() _run; + final bool run$async; void run() { return _run(); @@ -83,7 +87,7 @@ implementing the interface in Java instead of using the lambdas: ```java // Java -public class Printer implements Runnable { +public class Printer with Runnable { private final String text; public Printer(String text) { @@ -104,7 +108,7 @@ You can do the same in Dart by creating a subclass that implements `$Runnable`: ```dart // Dart -class Printer implements $Runnable { +class Printer with $Runnable { final String text; Printer(this.text); @@ -120,6 +124,42 @@ And similarly write `Runnable.implement(Printer('hello'))` and `Runnable.implement(Printer('world'))`, to create multiple Runnables and share common logic. +### Implement as a listener + +By default, when any of methods of the implemented interface gets called, the +caller will block until the callee returns a result. + +Void-returning functions don't have to return a result, so we can choose to not +block the caller when the method is just a listener. To signal this, pass `true` +to `$async` argument when implementing the interface inline: + +```dart +// Dart +final runnable = Runnable.implement($Runnable( + run: () => print('hello'), + run$async: true, // This makes the run method non-blocking. +)); +``` + +Similarly, when subclassing + +```dart +// Dart +class Printer with $Runnable { + final String text; + + Printer(this.text); + + @override + void run() { + print(text); + } + + @override + bool get run$async => true; // This makes the run method non-blocking. +``` + + ### Implement multiple interfaces To implement more than one interface, use a `JImplementer` from `package:jni`. @@ -141,5 +181,5 @@ possible to make it a `Closable` by passing in `Closable.type` to ```dart // Dart -final closable = object.castTo(Closable.type); +final closable = object.as(Closable.type); ``` diff --git a/pkgs/jnigen/lib/src/bindings/dart_generator.dart b/pkgs/jnigen/lib/src/bindings/dart_generator.dart index 1c7420c43..1fe8186d2 100644 --- a/pkgs/jnigen/lib/src/bindings/dart_generator.dart +++ b/pkgs/jnigen/lib/src/bindings/dart_generator.dart @@ -469,6 +469,18 @@ class $name$typeParamsDef extends $superName { r'${node.binaryName}', \$p, _\$invokePointer, + [ +'''); + final interfaceAsyncMethod = _InterfaceIfAsyncMethod( + resolver, + s, + implClassName: implClassName, + ); + for (final method in node.methods) { + method.accept(interfaceAsyncMethod); + } + s.write(''' + ], ); final \$a = \$p.sendPort.nativePort; _\$impls[\$a] = \$impl; @@ -516,12 +528,12 @@ class $name$typeParamsDef extends $superName { [ ...typeParams .map((typeParam) => 'required $_jType<\$$typeParam> $typeParam,'), - ...node.methods.accept(_ConcreteImplClosureCtorArg(resolver)), + ...node.methods.accept(_AbstractImplFactoryArg(resolver)), ].join(_newLine(depth: 2)), '}', ); s.write(''' -abstract interface class $implClassName$typeParamsDef { +abstract mixin class $implClassName$typeParamsDef { factory $implClassName( $abstractFactoryArgs ) = _$implClassName; @@ -745,22 +757,11 @@ class _TypeClass { /// Generates the type class. class _TypeClassGenerator extends TypeVisitor<_TypeClass> { final bool isConst; - - /// Whether or not to return the equivalent boxed type class for primitives. - /// Only for interface implemetation. - final bool boxPrimitives; - - /// Whether or not to find the correct type variable from the static map. - /// Only for interface implemetation. - final bool typeVarFromMap; - final Resolver resolver; _TypeClassGenerator( this.resolver, { this.isConst = true, - this.boxPrimitives = false, - this.typeVarFromMap = false, }); @override @@ -768,8 +769,6 @@ class _TypeClassGenerator extends TypeVisitor<_TypeClass> { final innerTypeClass = node.type.accept(_TypeClassGenerator( resolver, isConst: false, - boxPrimitives: false, - typeVarFromMap: typeVarFromMap, )); final ifConst = innerTypeClass.canBeConst && isConst ? 'const ' : ''; return _TypeClass( @@ -788,8 +787,6 @@ class _TypeClassGenerator extends TypeVisitor<_TypeClass> { final definedTypeClasses = node.params.accept(_TypeClassGenerator( resolver, isConst: false, - boxPrimitives: false, - typeVarFromMap: typeVarFromMap, )); // Can be const if all the type parameters are defined and each of them are @@ -831,15 +828,12 @@ class _TypeClassGenerator extends TypeVisitor<_TypeClass> { @override _TypeClass visitPrimitiveType(PrimitiveType node) { final ifConst = isConst ? 'const ' : ''; - final name = boxPrimitives ? 'J${node.boxedName}' : 'j${node.name}'; + final name = 'j${node.name}'; return _TypeClass('$ifConst$_jni.${name}Type()', true); } @override _TypeClass visitTypeVar(TypeVar node) { - if (typeVarFromMap) { - return _TypeClass('_\$impls[\$p]!.${node.name}', false); - } return _TypeClass(node.name, false); } @@ -856,6 +850,23 @@ class _TypeClassGenerator extends TypeVisitor<_TypeClass> { } } +class _ImplTypeClass extends _TypeClassGenerator { + _ImplTypeClass(super.resolver); + + @override + _TypeClass visitTypeVar(TypeVar node) { + // Get the concrete type variable from the static map. + return _TypeClass('_\$impls[\$p]!.${node.name}', false); + } + + @override + _TypeClass visitPrimitiveType(PrimitiveType node) { + final ifConst = isConst ? 'const ' : ''; + final name = 'J${node.boxedName}'; + return _TypeClass('$ifConst$_jni.${name}Type()', true); + } +} + class _TypeParamGenerator extends Visitor { final bool withExtends; @@ -1484,6 +1495,9 @@ class _AbstractImplMethod extends Visitor { final name = node.finalName; final args = node.params.accept(_ParamDef(resolver)).join(', '); s.writeln(' $returnType $name($args);'); + if (returnType == 'void') { + s.writeln(' bool get $name\$async => false;'); + } } } @@ -1500,6 +1514,29 @@ class _ConcreteImplClosureDef extends Visitor { final name = node.finalName; final args = node.params.accept(_ParamDef(resolver)).join(', '); s.writeln(' final $returnType Function($args) _$name;'); + if (returnType == 'void') { + s.writeln(' final bool $name\$async;'); + } + } +} + +/// Closure argument for the factory of the implementation's abstract class. +/// Used for interface implementation. +class _AbstractImplFactoryArg extends Visitor { + final Resolver resolver; + + _AbstractImplFactoryArg(this.resolver); + + @override + String visit(Method node) { + final returnType = node.returnType.accept(_TypeGenerator(resolver)); + final name = node.finalName; + final args = node.params.accept(_ParamDef(resolver)).join(', '); + final functionArg = 'required $returnType Function($args) $name,'; + if (node.returnType.name == 'void') { + return '$functionArg bool $name\$async,'; + } + return functionArg; } } @@ -1515,7 +1552,11 @@ class _ConcreteImplClosureCtorArg extends Visitor { final returnType = node.returnType.accept(_TypeGenerator(resolver)); final name = node.finalName; final args = node.params.accept(_ParamDef(resolver)).join(', '); - return 'required $returnType Function($args) $name,'; + final functionArg = 'required $returnType Function($args) $name,'; + if (node.returnType.name == 'void') { + return '$functionArg this.$name\$async = false,'; + } + return functionArg; } } @@ -1568,6 +1609,27 @@ class _InterfaceMethodIf extends Visitor { } } +/// The if statement within the async methods list to conditionally add methods. +class _InterfaceIfAsyncMethod extends Visitor { + final Resolver resolver; + final StringSink s; + final String implClassName; + + _InterfaceIfAsyncMethod(this.resolver, this.s, {required this.implClassName}); + + @override + void visit(Method node) { + if (node.returnType.name != 'void') { + return; + } + final signature = node.javaSig; + final name = node.finalName; + s.write(''' + if (\$impl.$name\$async) r'$signature', +'''); + } +} + /// Generates casting to the correct parameter type from the list of JObject /// arguments received from the call to the proxy class. class _InterfaceParamCast extends Visitor { @@ -1583,13 +1645,7 @@ class _InterfaceParamCast extends Visitor { @override void visit(Param node) { - final typeClass = node.type - .accept(_TypeClassGenerator( - resolver, - boxPrimitives: true, - typeVarFromMap: true, - )) - .name; + final typeClass = node.type.accept(_ImplTypeClass(resolver)).name; s.write('\$a[$paramIndex].as($typeClass, releaseOriginal: true)'); if (node.type.kind == Kind.primitive) { // Convert to Dart type. @@ -1606,7 +1662,7 @@ class _InterfaceParamCast extends Visitor { /// /// Since Dart doesn't know that this global reference is still used, it might /// garbage collect it via `NativeFinalizer` thus making it invalid. -/// This passes the ownership to Java using `setAsReleased`. +/// This passes the ownership to Java using `toPointer()`. /// /// `toPointer` detaches the object from the `NativeFinalizer` and Java /// will clean up the global reference afterwards. diff --git a/pkgs/jnigen/test/simple_package_test/bindings/simple_package.dart b/pkgs/jnigen/test/simple_package_test/bindings/simple_package.dart index 08303ee91..0707eb370 100644 --- a/pkgs/jnigen/test/simple_package_test/bindings/simple_package.dart +++ b/pkgs/jnigen/test/simple_package_test/bindings/simple_package.dart @@ -4865,6 +4865,9 @@ class MyInterface<$T extends _$jni.JObject> extends _$jni.JObject { r'com.github.dart_lang.jnigen.interfaces.MyInterface', $p, _$invokePointer, + [ + if ($impl.voidCallback$async) r'voidCallback(Ljava/lang/String;)V', + ], ); final $a = $p.sendPort.nativePort; _$impls[$a] = $impl; @@ -4883,10 +4886,11 @@ class MyInterface<$T extends _$jni.JObject> extends _$jni.JObject { static _$core.Map get $impls => _$impls; } -abstract interface class $MyInterface<$T extends _$jni.JObject> { +abstract mixin class $MyInterface<$T extends _$jni.JObject> { factory $MyInterface({ required _$jni.JObjType<$T> T, required void Function(_$jni.JString s) voidCallback, + bool voidCallback$async, required _$jni.JString Function(_$jni.JString s) stringCallback, required $T Function($T t) varCallback, required int Function(int a, bool b, int c, double d) manyPrimitives, @@ -4895,6 +4899,7 @@ abstract interface class $MyInterface<$T extends _$jni.JObject> { _$jni.JObjType<$T> get T; void voidCallback(_$jni.JString s); + bool get voidCallback$async => false; _$jni.JString stringCallback(_$jni.JString s); $T varCallback($T t); int manyPrimitives(int a, bool b, int c, double d); @@ -4904,6 +4909,7 @@ class _$MyInterface<$T extends _$jni.JObject> implements $MyInterface<$T> { _$MyInterface({ required this.T, required void Function(_$jni.JString s) voidCallback, + this.voidCallback$async = false, required _$jni.JString Function(_$jni.JString s) stringCallback, required $T Function($T t) varCallback, required int Function(int a, bool b, int c, double d) manyPrimitives, @@ -4916,6 +4922,7 @@ class _$MyInterface<$T extends _$jni.JObject> implements $MyInterface<$T> { final _$jni.JObjType<$T> T; final void Function(_$jni.JString s) _voidCallback; + final bool voidCallback$async; final _$jni.JString Function(_$jni.JString s) _stringCallback; final $T Function($T t) _varCallback; final int Function(int a, bool b, int c, double d) _manyPrimitives; @@ -5269,6 +5276,9 @@ class MyRunnable extends _$jni.JObject { r'com.github.dart_lang.jnigen.interfaces.MyRunnable', $p, _$invokePointer, + [ + if ($impl.run$async) r'run()V', + ], ); final $a = $p.sendPort.nativePort; _$impls[$a] = $impl; @@ -5286,20 +5296,24 @@ class MyRunnable extends _$jni.JObject { static _$core.Map get $impls => _$impls; } -abstract interface class $MyRunnable { +abstract mixin class $MyRunnable { factory $MyRunnable({ required void Function() run, + bool run$async, }) = _$MyRunnable; void run(); + bool get run$async => false; } class _$MyRunnable implements $MyRunnable { _$MyRunnable({ required void Function() run, + this.run$async = false, }) : _run = run; final void Function() _run; + final bool run$async; void run() { return _run(); @@ -5440,6 +5454,30 @@ class MyRunnableRunner extends _$jni.JObject { reference.pointer, _id_runOnAnotherThread as _$jni.JMethodIDPtr) .check(); } + + static final _id_runOnAnotherThreadAndJoin = _class.instanceMethodId( + r'runOnAnotherThreadAndJoin', + r'()V', + ); + + static final _runOnAnotherThreadAndJoin = _$jni.ProtectedJniExtensions.lookup< + _$jni.NativeFunction< + _$jni.JThrowablePtr Function( + _$jni.Pointer<_$jni.Void>, + _$jni.JMethodIDPtr, + )>>('globalEnv_CallVoidMethod') + .asFunction< + _$jni.JThrowablePtr Function( + _$jni.Pointer<_$jni.Void>, + _$jni.JMethodIDPtr, + )>(); + + /// from: `public void runOnAnotherThreadAndJoin()` + void runOnAnotherThreadAndJoin() { + _runOnAnotherThreadAndJoin(reference.pointer, + _id_runOnAnotherThreadAndJoin as _$jni.JMethodIDPtr) + .check(); + } } final class $MyRunnableRunner$Type extends _$jni.JObjType { @@ -5655,6 +5693,7 @@ class StringConverter extends _$jni.JObject { r'com.github.dart_lang.jnigen.interfaces.StringConverter', $p, _$invokePointer, + [], ); final $a = $p.sendPort.nativePort; _$impls[$a] = $impl; @@ -5671,7 +5710,7 @@ class StringConverter extends _$jni.JObject { } } -abstract interface class $StringConverter { +abstract mixin class $StringConverter { factory $StringConverter({ required int Function(_$jni.JString s) parseToInt, }) = _$StringConverter; @@ -6340,6 +6379,7 @@ class JsonSerializable extends _$jni.JObject { r'com.github.dart_lang.jnigen.annotations.JsonSerializable', $p, _$invokePointer, + [], ); final $a = $p.sendPort.nativePort; _$impls[$a] = $impl; @@ -6356,7 +6396,7 @@ class JsonSerializable extends _$jni.JObject { } } -abstract interface class $JsonSerializable { +abstract mixin class $JsonSerializable { factory $JsonSerializable({ required JsonSerializable_Case Function() value, }) = _$JsonSerializable; diff --git a/pkgs/jnigen/test/simple_package_test/java/com/github/dart_lang/jnigen/interfaces/MyRunnableRunner.java b/pkgs/jnigen/test/simple_package_test/java/com/github/dart_lang/jnigen/interfaces/MyRunnableRunner.java index 2903f37e0..7aeb91a3b 100644 --- a/pkgs/jnigen/test/simple_package_test/java/com/github/dart_lang/jnigen/interfaces/MyRunnableRunner.java +++ b/pkgs/jnigen/test/simple_package_test/java/com/github/dart_lang/jnigen/interfaces/MyRunnableRunner.java @@ -25,4 +25,10 @@ public void runOnAnotherThread() { var thread = new Thread(this::runOnSameThread); thread.start(); } + + public void runOnAnotherThreadAndJoin() throws InterruptedException { + var thread = new Thread(this::runOnSameThread); + thread.start(); + thread.join(); + } } diff --git a/pkgs/jnigen/test/simple_package_test/runtime_test_registrant.dart b/pkgs/jnigen/test/simple_package_test/runtime_test_registrant.dart index 642ba29a3..e9afc9a0d 100644 --- a/pkgs/jnigen/test/simple_package_test/runtime_test_registrant.dart +++ b/pkgs/jnigen/test/simple_package_test/runtime_test_registrant.dart @@ -702,6 +702,44 @@ void registerTests(String groupName, TestRunnerCallback test) { expect(fortyTwo.intValue(releaseOriginal: true), 42); }); }); + for (final style in ['callback', 'implemented class']) { + test('Listener callbacks - $style style', () async { + final completer = Completer(); + + final MyRunnable runnable; + if (style == 'callback') { + runnable = MyRunnable.implement($MyRunnable( + run: completer.complete, + run$async: true, + )); + } else { + runnable = MyRunnable.implement(AsyncRunnable(completer)); + } + final runner = MyRunnableRunner(runnable); + // Normally this would cause a deadlock, but as the callback is a + // listener, it will work. + runner.runOnAnotherThreadAndJoin(); + await completer.future; + expect(MyRunnable.$impls, hasLength(1)); + runnable.release(); + runner.release(); + if (!Platform.isAndroid) { + // Running garbage collection does not work on Android. Skipping + // this test for android. + _runJavaGC(); + for (var i = 0; i < 8; ++i) { + await Future.delayed( + Duration(milliseconds: (1 << i) * 100)); + if (MyInterface.$impls.isEmpty) { + break; + } + } + // Since the interface is now deleted, the cleaner must signal to + // Dart to clean up. + expect(MyRunnable.$impls, isEmpty); + } + }); + } } group('Dart exceptions are handled', () { for (final exception in [UnimplementedError(), 'Hello!']) { @@ -906,3 +944,17 @@ class DartStringToIntParser implements $StringConverter { return int.parse(s.toDartString(releaseOriginal: true), radix: radix); } } + +class AsyncRunnable with $MyRunnable { + final Completer completer; + + AsyncRunnable(this.completer); + + @override + Future run() async { + completer.complete(); + } + + @override + bool get run$async => true; +}