Skip to content

Commit ae32a51

Browse files
[jnigen] Fix sync suspend funs from hanging (#2028)
* [jnigen] Fix sync `suspend fun`s from hanging * Handle thrown exceptions from suspend funs
1 parent f180639 commit ae32a51

File tree

12 files changed

+253
-50
lines changed

12 files changed

+253
-50
lines changed

pkgs/jni/CHANGELOG.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
- Updated `bin/setup.dart` to use Gradle instead of Maven for building Java sources. Added gradle executables
44
and bootstrap jars [#2003](https://github.com/dart-lang/native/issues/2003)
5+
- Added `JObject.isInstanceOf` which checks whether a `JObject` is an instance
6+
of a java class.
57

68
## 0.14.0
79

@@ -14,8 +16,8 @@
1416
instead use the default value of `build/jni_libs`.
1517
- Added `JArray.of`, which allows a `JArray` to be constructed from an
1618
`Iterable`.
17-
- Added `JObject.isA`, which checks whether a `JObject` is a instance of a java
18-
class.
19+
- Added `JObject.isA`, which checks whether a `JObject` is an instance of a java
20+
typeclass.
1921
- Do not require JAWT when building for desktop.
2022
- Added `JByteArray.from`, which allows a `JByteArray` to be constructed from an
2123
`Iterable<int>`.

pkgs/jni/lib/_internal.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ export 'package:meta/meta.dart' show internal;
3030
export 'src/accessors.dart';
3131
export 'src/jni.dart' show ProtectedJniExtensions;
3232
export 'src/jreference.dart';
33+
export 'src/kotlin.dart'
34+
show coroutineSingletonsClass, failureExceptionField, result$FailureClass;
3335
export 'src/method_invocation.dart';
3436
export 'src/types.dart'
3537
show

pkgs/jni/lib/src/jni.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,7 @@ abstract final class Jni {
183183

184184
/// Throws an exception.
185185
// TODO(#561): Throw an actual `JThrowable`.
186+
@internal
186187
static void throwException(JThrowablePtr exception) {
187188
final details = _bindings.GetExceptionDetails(exception);
188189
final env = Jni.env;

pkgs/jni/lib/src/jobject.dart

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -148,12 +148,17 @@ class JObject {
148148
/// }
149149
/// ```
150150
bool isA<T extends JObject?>(JObjType<T> type) {
151-
final targetJClass = type.jClass.reference.toPointer();
152-
final canBeCasted = Jni.env.IsInstanceOf(reference.pointer, targetJClass);
153-
Jni.env.DeleteGlobalRef(targetJClass);
151+
final targetJClass = type.jClass;
152+
final canBeCasted = isInstanceOf(targetJClass);
153+
targetJClass.release();
154154
return canBeCasted;
155155
}
156156

157+
/// Whether this object is of the type of the given [jclass].
158+
bool isInstanceOf(JClass jclass) {
159+
return Jni.env.IsInstanceOf(reference.pointer, jclass.reference.pointer);
160+
}
161+
157162
/// Casts this object to another [type].
158163
///
159164
/// If [releaseOriginal] is `true`, the casted object will be released.

pkgs/jni/lib/src/kotlin.dart

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
// Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file
2+
// for details. All rights reserved. Use of this source code is governed by a
3+
// BSD-style license that can be found in the LICENSE file.
4+
5+
import 'package:meta/meta.dart' show internal;
6+
7+
import 'types.dart';
8+
9+
@internal
10+
final coroutineSingletonsClass =
11+
JClass.forName('kotlin/coroutines/intrinsics/CoroutineSingletons');
12+
13+
@internal
14+
final result$FailureClass = JClass.forName(r'kotlin/Result$Failure');
15+
16+
@internal
17+
final failureExceptionField =
18+
result$FailureClass.instanceFieldId('exception', 'Ljava/lang/Throwable;');

pkgs/jnigen/CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
- Added support for generating matching Kotlin operators as Dart operators.
44
- Include the methods of the superinterfaces of a class or interface in the
55
bindings.
6+
- Fix a bug where Kotlin suspendable functions that returned the result without
7+
ever suspending would timeout in Dart.
68
- Retrieval of dependencies uses Gradle (`GradleTools`) en lieu of Maven (`MavenTools`).
79
- Updated `bin\download_maven_jars.dart` to use `GradleTools`.
810

pkgs/jnigen/example/kotlin_plugin/lib/kotlin_bindings.dart

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -98,22 +98,27 @@ class Example extends jni$_.JObject {
9898
final $p = jni$_.ReceivePort();
9999
final _$continuation = jni$_.ProtectedJniExtensions.newPortContinuation($p);
100100

101-
_thinkBeforeAnswering(
101+
final $r = _thinkBeforeAnswering(
102102
reference.pointer,
103103
_id_thinkBeforeAnswering as jni$_.JMethodIDPtr,
104104
_$continuation.pointer)
105-
.object<jni$_.JObject>(const jni$_.JObjectType())
106-
.release();
105+
.object<jni$_.JObject>(const jni$_.JObjectType());
107106
_$continuation.release();
108-
final $o =
109-
jni$_.JGlobalReference(jni$_.JObjectPtr.fromAddress(await $p.first));
110-
final $k = const jni$_.JStringType().jClass.reference;
111-
if (!jni$_.Jni.env.IsInstanceOf($o.pointer, $k.pointer)) {
112-
$k.release();
113-
throw 'Failed';
107+
final jni$_.JObject $o;
108+
if ($r.isInstanceOf(jni$_.coroutineSingletonsClass)) {
109+
$r.release();
110+
$o = jni$_.JObject.fromReference(
111+
jni$_.JGlobalReference(jni$_.JObjectPtr.fromAddress(await $p.first)));
112+
if ($o.isInstanceOf(jni$_.result$FailureClass)) {
113+
final $e =
114+
jni$_.failureExceptionField.get($o, const jni$_.JObjectType());
115+
$o.release();
116+
jni$_.Jni.throwException($e.reference.toPointer());
117+
}
118+
} else {
119+
$o = $r;
114120
}
115-
$k.release();
116-
return const jni$_.JStringType().fromReference($o);
121+
return $o.as(const jni$_.JStringType(), releaseOriginal: true);
117122
}
118123
}
119124

pkgs/jnigen/lib/src/bindings/dart_generator.dart

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1418,9 +1418,6 @@ ${modifier}final _$name = $_protectedExtension
14181418
? '$_core.Future<'
14191419
'${node.asyncReturnType!.accept(_TypeGenerator(resolver))}>'
14201420
: node.returnType.accept(_TypeGenerator(resolver));
1421-
final returnTypeClass = (node.asyncReturnType ?? node.returnType)
1422-
.accept(_TypeClassGenerator(resolver))
1423-
.name;
14241421
final ifStatic = node.isStatic && !isTopLevel ? 'static ' : '';
14251422
final defArgs = node.params.accept(_ParamDef(resolver)).toList();
14261423
final typeClassDef = node.typeParams
@@ -1451,16 +1448,23 @@ ${modifier}final _$name = $_protectedExtension
14511448
final \$p = $_jni.ReceivePort();
14521449
final _\$$continuation = $_protectedExtension.newPortContinuation(\$p);
14531450
${localReferences.join(_newLine(depth: 2))}
1454-
$callExpr.release();
1451+
final \$r = $callExpr;
14551452
_\$$continuation.release();
1456-
final \$o = $_jGlobalReference($_jPointer.fromAddress(await \$p.first));
1457-
final \$k = $returnTypeClass.jClass.reference;
1458-
if (!$_jni.Jni.env.IsInstanceOf(\$o.pointer, \$k.pointer)) {
1459-
\$k.release();
1460-
throw 'Failed';
1453+
final $_jObject \$o;
1454+
if (\$r.isInstanceOf($_jni.coroutineSingletonsClass)) {
1455+
\$r.release();
1456+
\$o = $_jObject.fromReference(
1457+
$_jGlobalReference($_jPointer.fromAddress(await \$p.first)));
1458+
if (\$o.isInstanceOf($_jni.result\$FailureClass)) {
1459+
final \$e =
1460+
$_jni.failureExceptionField.get(\$o, const ${_jObject}Type());
1461+
\$o.release();
1462+
$_jni.Jni.throwException(\$e.reference.toPointer());
1463+
}
1464+
} else {
1465+
\$o = \$r;
14611466
}
1462-
\$k.release();
1463-
return $returningType.fromReference(\$o);
1467+
return \$o.as($returningType, releaseOriginal: true);
14641468
}
14651469
14661470
''');

pkgs/jnigen/lib/src/bindings/renamer.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,8 @@ const Map<String, int> _definedSyms = {
9595
'runtimeType': 1,
9696
'noSuchMethod': 1,
9797
'reference': 1,
98+
'isA': 1,
99+
'isInstanceOf': 1,
98100
'isReleased': 1,
99101
'isNull': 1,
100102
'use': 1,

pkgs/jnigen/test/kotlin_test/bindings/kotlin.dart

Lines changed: 167 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -2630,6 +2630,137 @@ class SuspendFun extends jni$_.JObject {
26302630
.reference);
26312631
}
26322632

2633+
static final _id_sayHelloWithoutDelay = _class.instanceMethodId(
2634+
r'sayHelloWithoutDelay',
2635+
r'(Lkotlin/coroutines/Continuation;)Ljava/lang/Object;',
2636+
);
2637+
2638+
static final _sayHelloWithoutDelay = jni$_.ProtectedJniExtensions.lookup<
2639+
jni$_.NativeFunction<
2640+
jni$_.JniResult Function(
2641+
jni$_.Pointer<jni$_.Void>,
2642+
jni$_.JMethodIDPtr,
2643+
jni$_.VarArgs<(jni$_.Pointer<jni$_.Void>,)>)>>(
2644+
'globalEnv_CallObjectMethod')
2645+
.asFunction<
2646+
jni$_.JniResult Function(jni$_.Pointer<jni$_.Void>,
2647+
jni$_.JMethodIDPtr, jni$_.Pointer<jni$_.Void>)>();
2648+
2649+
/// from: `public final java.lang.Object sayHelloWithoutDelay(kotlin.coroutines.Continuation continuation)`
2650+
/// The returned object must be released after use, by calling the [release] method.
2651+
core$_.Future<jni$_.JString> sayHelloWithoutDelay() async {
2652+
final $p = jni$_.ReceivePort();
2653+
final _$continuation = jni$_.ProtectedJniExtensions.newPortContinuation($p);
2654+
2655+
final $r = _sayHelloWithoutDelay(
2656+
reference.pointer,
2657+
_id_sayHelloWithoutDelay as jni$_.JMethodIDPtr,
2658+
_$continuation.pointer)
2659+
.object<jni$_.JObject>(const jni$_.JObjectType());
2660+
_$continuation.release();
2661+
final jni$_.JObject $o;
2662+
if ($r.isInstanceOf(jni$_.coroutineSingletonsClass)) {
2663+
$r.release();
2664+
$o = jni$_.JObject.fromReference(
2665+
jni$_.JGlobalReference(jni$_.JObjectPtr.fromAddress(await $p.first)));
2666+
if ($o.isInstanceOf(jni$_.result$FailureClass)) {
2667+
final $e =
2668+
jni$_.failureExceptionField.get($o, const jni$_.JObjectType());
2669+
$o.release();
2670+
jni$_.Jni.throwException($e.reference.toPointer());
2671+
}
2672+
} else {
2673+
$o = $r;
2674+
}
2675+
return $o.as(const jni$_.JStringType(), releaseOriginal: true);
2676+
}
2677+
2678+
static final _id_failWithoutDelay = _class.instanceMethodId(
2679+
r'failWithoutDelay',
2680+
r'(Lkotlin/coroutines/Continuation;)Ljava/lang/Object;',
2681+
);
2682+
2683+
static final _failWithoutDelay = jni$_.ProtectedJniExtensions.lookup<
2684+
jni$_.NativeFunction<
2685+
jni$_.JniResult Function(
2686+
jni$_.Pointer<jni$_.Void>,
2687+
jni$_.JMethodIDPtr,
2688+
jni$_.VarArgs<(jni$_.Pointer<jni$_.Void>,)>)>>(
2689+
'globalEnv_CallObjectMethod')
2690+
.asFunction<
2691+
jni$_.JniResult Function(jni$_.Pointer<jni$_.Void>,
2692+
jni$_.JMethodIDPtr, jni$_.Pointer<jni$_.Void>)>();
2693+
2694+
/// from: `public final java.lang.Object failWithoutDelay(kotlin.coroutines.Continuation continuation)`
2695+
/// The returned object must be released after use, by calling the [release] method.
2696+
core$_.Future<jni$_.JString> failWithoutDelay() async {
2697+
final $p = jni$_.ReceivePort();
2698+
final _$continuation = jni$_.ProtectedJniExtensions.newPortContinuation($p);
2699+
2700+
final $r = _failWithoutDelay(reference.pointer,
2701+
_id_failWithoutDelay as jni$_.JMethodIDPtr, _$continuation.pointer)
2702+
.object<jni$_.JObject>(const jni$_.JObjectType());
2703+
_$continuation.release();
2704+
final jni$_.JObject $o;
2705+
if ($r.isInstanceOf(jni$_.coroutineSingletonsClass)) {
2706+
$r.release();
2707+
$o = jni$_.JObject.fromReference(
2708+
jni$_.JGlobalReference(jni$_.JObjectPtr.fromAddress(await $p.first)));
2709+
if ($o.isInstanceOf(jni$_.result$FailureClass)) {
2710+
final $e =
2711+
jni$_.failureExceptionField.get($o, const jni$_.JObjectType());
2712+
$o.release();
2713+
jni$_.Jni.throwException($e.reference.toPointer());
2714+
}
2715+
} else {
2716+
$o = $r;
2717+
}
2718+
return $o.as(const jni$_.JStringType(), releaseOriginal: true);
2719+
}
2720+
2721+
static final _id_fail = _class.instanceMethodId(
2722+
r'fail',
2723+
r'(Lkotlin/coroutines/Continuation;)Ljava/lang/Object;',
2724+
);
2725+
2726+
static final _fail = jni$_.ProtectedJniExtensions.lookup<
2727+
jni$_.NativeFunction<
2728+
jni$_.JniResult Function(
2729+
jni$_.Pointer<jni$_.Void>,
2730+
jni$_.JMethodIDPtr,
2731+
jni$_.VarArgs<(jni$_.Pointer<jni$_.Void>,)>)>>(
2732+
'globalEnv_CallObjectMethod')
2733+
.asFunction<
2734+
jni$_.JniResult Function(jni$_.Pointer<jni$_.Void>,
2735+
jni$_.JMethodIDPtr, jni$_.Pointer<jni$_.Void>)>();
2736+
2737+
/// from: `public final java.lang.Object fail(kotlin.coroutines.Continuation continuation)`
2738+
/// The returned object must be released after use, by calling the [release] method.
2739+
core$_.Future<jni$_.JString> fail() async {
2740+
final $p = jni$_.ReceivePort();
2741+
final _$continuation = jni$_.ProtectedJniExtensions.newPortContinuation($p);
2742+
2743+
final $r = _fail(reference.pointer, _id_fail as jni$_.JMethodIDPtr,
2744+
_$continuation.pointer)
2745+
.object<jni$_.JObject>(const jni$_.JObjectType());
2746+
_$continuation.release();
2747+
final jni$_.JObject $o;
2748+
if ($r.isInstanceOf(jni$_.coroutineSingletonsClass)) {
2749+
$r.release();
2750+
$o = jni$_.JObject.fromReference(
2751+
jni$_.JGlobalReference(jni$_.JObjectPtr.fromAddress(await $p.first)));
2752+
if ($o.isInstanceOf(jni$_.result$FailureClass)) {
2753+
final $e =
2754+
jni$_.failureExceptionField.get($o, const jni$_.JObjectType());
2755+
$o.release();
2756+
jni$_.Jni.throwException($e.reference.toPointer());
2757+
}
2758+
} else {
2759+
$o = $r;
2760+
}
2761+
return $o.as(const jni$_.JStringType(), releaseOriginal: true);
2762+
}
2763+
26332764
static final _id_sayHello = _class.instanceMethodId(
26342765
r'sayHello',
26352766
r'(Lkotlin/coroutines/Continuation;)Ljava/lang/Object;',
@@ -2652,20 +2783,25 @@ class SuspendFun extends jni$_.JObject {
26522783
final $p = jni$_.ReceivePort();
26532784
final _$continuation = jni$_.ProtectedJniExtensions.newPortContinuation($p);
26542785

2655-
_sayHello(reference.pointer, _id_sayHello as jni$_.JMethodIDPtr,
2786+
final $r = _sayHello(reference.pointer, _id_sayHello as jni$_.JMethodIDPtr,
26562787
_$continuation.pointer)
2657-
.object<jni$_.JObject>(const jni$_.JObjectType())
2658-
.release();
2788+
.object<jni$_.JObject>(const jni$_.JObjectType());
26592789
_$continuation.release();
2660-
final $o =
2661-
jni$_.JGlobalReference(jni$_.JObjectPtr.fromAddress(await $p.first));
2662-
final $k = const jni$_.JStringType().jClass.reference;
2663-
if (!jni$_.Jni.env.IsInstanceOf($o.pointer, $k.pointer)) {
2664-
$k.release();
2665-
throw 'Failed';
2790+
final jni$_.JObject $o;
2791+
if ($r.isInstanceOf(jni$_.coroutineSingletonsClass)) {
2792+
$r.release();
2793+
$o = jni$_.JObject.fromReference(
2794+
jni$_.JGlobalReference(jni$_.JObjectPtr.fromAddress(await $p.first)));
2795+
if ($o.isInstanceOf(jni$_.result$FailureClass)) {
2796+
final $e =
2797+
jni$_.failureExceptionField.get($o, const jni$_.JObjectType());
2798+
$o.release();
2799+
jni$_.Jni.throwException($e.reference.toPointer());
2800+
}
2801+
} else {
2802+
$o = $r;
26662803
}
2667-
$k.release();
2668-
return const jni$_.JStringType().fromReference($o);
2804+
return $o.as(const jni$_.JStringType(), releaseOriginal: true);
26692805
}
26702806

26712807
static final _id_sayHello$1 = _class.instanceMethodId(
@@ -2698,20 +2834,28 @@ class SuspendFun extends jni$_.JObject {
26982834
final $p = jni$_.ReceivePort();
26992835
final _$continuation = jni$_.ProtectedJniExtensions.newPortContinuation($p);
27002836
final _$string = string.reference;
2701-
_sayHello$1(reference.pointer, _id_sayHello$1 as jni$_.JMethodIDPtr,
2702-
_$string.pointer, _$continuation.pointer)
2703-
.object<jni$_.JObject>(const jni$_.JObjectType())
2704-
.release();
2837+
final $r = _sayHello$1(
2838+
reference.pointer,
2839+
_id_sayHello$1 as jni$_.JMethodIDPtr,
2840+
_$string.pointer,
2841+
_$continuation.pointer)
2842+
.object<jni$_.JObject>(const jni$_.JObjectType());
27052843
_$continuation.release();
2706-
final $o =
2707-
jni$_.JGlobalReference(jni$_.JObjectPtr.fromAddress(await $p.first));
2708-
final $k = const jni$_.JStringType().jClass.reference;
2709-
if (!jni$_.Jni.env.IsInstanceOf($o.pointer, $k.pointer)) {
2710-
$k.release();
2711-
throw 'Failed';
2844+
final jni$_.JObject $o;
2845+
if ($r.isInstanceOf(jni$_.coroutineSingletonsClass)) {
2846+
$r.release();
2847+
$o = jni$_.JObject.fromReference(
2848+
jni$_.JGlobalReference(jni$_.JObjectPtr.fromAddress(await $p.first)));
2849+
if ($o.isInstanceOf(jni$_.result$FailureClass)) {
2850+
final $e =
2851+
jni$_.failureExceptionField.get($o, const jni$_.JObjectType());
2852+
$o.release();
2853+
jni$_.Jni.throwException($e.reference.toPointer());
2854+
}
2855+
} else {
2856+
$o = $r;
27122857
}
2713-
$k.release();
2714-
return const jni$_.JStringType().fromReference($o);
2858+
return $o.as(const jni$_.JStringType(), releaseOriginal: true);
27152859
}
27162860
}
27172861

0 commit comments

Comments
 (0)