From b550a5f7a0325a94d85f3368e40ab7c525d91598 Mon Sep 17 00:00:00 2001 From: Liam Appelbe Date: Wed, 10 Dec 2025 15:33:00 +1100 Subject: [PATCH 01/10] WIP: Kotlin interface with suspend method --- pkgs/jni/lib/_internal.dart | 3 +- pkgs/jni/lib/src/kotlin.dart | 65 +++++ .../android/src/main/kotlin/Example.kt | 11 +- .../kotlin_plugin/example/lib/main.dart | 17 +- pkgs/jnigen/example/kotlin_plugin/jnigen.yaml | 1 + .../kotlin_plugin/lib/kotlin_bindings.dart | 276 +++++++++++++++++- 6 files changed, 356 insertions(+), 17 deletions(-) diff --git a/pkgs/jni/lib/_internal.dart b/pkgs/jni/lib/_internal.dart index 9c0db34f76..f9c934a681 100644 --- a/pkgs/jni/lib/_internal.dart +++ b/pkgs/jni/lib/_internal.dart @@ -52,7 +52,8 @@ export 'src/jni.dart' show ProtectedJniExtensions; export 'src/jobject.dart' show $JObject$NullableType$, $JObject$Type$; export 'src/jreference.dart'; export 'src/kotlin.dart' - show coroutineSingletonsClass, failureExceptionField, result$FailureClass; + show coroutineSingletonsClass, failureExceptionField, + result$FailureClass, KotlinContinuation; export 'src/lang/jboolean.dart' show $JBoolean$NullableType$, $JBoolean$Type$; export 'src/lang/jbyte.dart' show $JByte$NullableType$, $JByte$Type$; export 'src/lang/jcharacter.dart' diff --git a/pkgs/jni/lib/src/kotlin.dart b/pkgs/jni/lib/src/kotlin.dart index eb0e1d11a8..48710eb702 100644 --- a/pkgs/jni/lib/src/kotlin.dart +++ b/pkgs/jni/lib/src/kotlin.dart @@ -2,8 +2,16 @@ // for details. All rights reserved. Use of this source code is governed by a // BSD-style license that can be found in the LICENSE file. +import 'dart:ffi'; + import 'package:meta/meta.dart' show internal; +import 'jni.dart'; +import 'jobject.dart'; +import 'lang/jstring.dart'; +import 'jreference.dart'; +import 'jvalues.dart'; +import 'third_party/generated_bindings.dart'; import 'types.dart'; @internal @@ -16,3 +24,60 @@ final result$FailureClass = JClass.forName(r'kotlin/Result$Failure'); @internal final failureExceptionField = result$FailureClass.instanceFieldId('exception', 'Ljava/lang/Throwable;'); + +final _coroutineSuspended = coroutineSingletonsClass.staticFieldId( + 'COROUTINE_SUSPENDED', 'Lkotlin/coroutines/intrinsics/CoroutineSingletons') + .get(coroutineSingletonsClass, const $JObject$Type$()); + +@internal +class KotlinContinuation extends JObject { + KotlinContinuation.fromReference( + super.reference, + ) : super.fromReference(); + + static final _class = JClass.forName(r'kotlin/coroutines/Continuation'); + + static final _resumeWithId = + _class.instanceMethodId(r'resumeWith', r'(Ljava/lang/Object;)V'); + void resumeWith(JObject result) { + _resumeWithId(this, const jvoidType(), [result]); + } + + JObject resumeWithFuture(Future future) { + future.then((JObject result) { + final r = _Result(result); + print(r); + resumeWith(r); + }, onError: (error) { + // TODO + }); + return _coroutineSuspended; + } +} + +class _Result extends JObject { + _Result.fromReference( + super.reference, + ) : super.fromReference(); + + static final _class = JClass.forName(r'kotlin/Result'); + + static final _id_new$ = _class.constructorId( + r'(Ljava/lang/Object;)V', + ); + + static final _new$ = ProtectedJniExtensions.lookup< + NativeFunction< + JniResult Function( + Pointer, + JMethodIDPtr, + VarArgs<(Pointer,)>)>>( + 'globalEnv_NewObject') + .asFunction< + JniResult Function(Pointer, + JMethodIDPtr, Pointer)>(); + + factory _Result(JObject object) { + return _Result.fromReference(_id_new$(_class, referenceType, [object])); + } +} diff --git a/pkgs/jnigen/example/kotlin_plugin/android/src/main/kotlin/Example.kt b/pkgs/jnigen/example/kotlin_plugin/android/src/main/kotlin/Example.kt index 531f899b45..7be9fcd552 100644 --- a/pkgs/jnigen/example/kotlin_plugin/android/src/main/kotlin/Example.kt +++ b/pkgs/jnigen/example/kotlin_plugin/android/src/main/kotlin/Example.kt @@ -1,10 +1,17 @@ import androidx.annotation.Keep +import kotlin.* +import kotlin.coroutines.* import kotlinx.coroutines.* +@Keep +interface Thinker { + public suspend fun message(): String +} + @Keep class Example { - public suspend fun thinkBeforeAnswering(): String { + public suspend fun thinkBeforeAnswering(thinker: Thinker): String { delay(1000L) - return "42" + return "Kotlin[" + thinker.message() + ']' } } diff --git a/pkgs/jnigen/example/kotlin_plugin/example/lib/main.dart b/pkgs/jnigen/example/kotlin_plugin/example/lib/main.dart index 84a91337c4..041e95022c 100644 --- a/pkgs/jnigen/example/kotlin_plugin/example/lib/main.dart +++ b/pkgs/jnigen/example/kotlin_plugin/example/lib/main.dart @@ -1,4 +1,6 @@ import 'package:flutter/material.dart'; +import 'package:jni/_internal.dart'; +import 'package:jni/jni.dart'; import 'package:kotlin_plugin/kotlin_plugin.dart'; void main() { @@ -62,8 +64,19 @@ class _MyHomePageState extends State { ElevatedButton( onPressed: () { setState(() { - answer = example.thinkBeforeAnswering().then( - (value) => value.toDartString(releaseOriginal: true)); + answer = () async { + final _thinker = $Thinker( + message: () async { + await Future.delayed(Duration(seconds: 3)); + return JString.fromString("App"); + } + ); + final thinker = Thinker.implement(_thinker); + final value1 = (await example.thinkBeforeAnswering(thinker)).toDartString(releaseOriginal: true); + final value2 = '-';//_thinker.message(JObject.fromReference(jNullReference)).as(const $JString$Type$()).toDartString(); + final value3 = '-'; //(await thinker.message()).toDartString(); + return value1 + '\n' + value2 + '\n' + value3; + }(); }); }, child: const Text('Think...'), diff --git a/pkgs/jnigen/example/kotlin_plugin/jnigen.yaml b/pkgs/jnigen/example/kotlin_plugin/jnigen.yaml index e6f08ecc68..f8633f259d 100644 --- a/pkgs/jnigen/example/kotlin_plugin/jnigen.yaml +++ b/pkgs/jnigen/example/kotlin_plugin/jnigen.yaml @@ -14,3 +14,4 @@ log_level: all classes: - 'Example' + - 'Thinker' diff --git a/pkgs/jnigen/example/kotlin_plugin/lib/kotlin_bindings.dart b/pkgs/jnigen/example/kotlin_plugin/lib/kotlin_bindings.dart index a6b68485a1..0b41fee03f 100644 --- a/pkgs/jnigen/example/kotlin_plugin/lib/kotlin_bindings.dart +++ b/pkgs/jnigen/example/kotlin_plugin/lib/kotlin_bindings.dart @@ -81,29 +81,38 @@ class Example extends jni$_.JObject { static final _id_thinkBeforeAnswering = _class.instanceMethodId( r'thinkBeforeAnswering', - r'(Lkotlin/coroutines/Continuation;)Ljava/lang/Object;', + r'(LThinker;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;', ); static final _thinkBeforeAnswering = jni$_.ProtectedJniExtensions.lookup< - jni$_.NativeFunction< - jni$_.JniResult Function( - jni$_.Pointer, - jni$_.JMethodIDPtr, - jni$_.VarArgs<(jni$_.Pointer,)>)>>( - 'globalEnv_CallObjectMethod') + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs< + ( + jni$_.Pointer, + jni$_.Pointer + )>)>>('globalEnv_CallObjectMethod') .asFunction< - jni$_.JniResult Function(jni$_.Pointer, - jni$_.JMethodIDPtr, jni$_.Pointer)>(); + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + jni$_.Pointer)>(); - /// from: `public suspend fun thinkBeforeAnswering(): kotlin.String` + /// from: `public suspend fun thinkBeforeAnswering(thinker: Thinker): kotlin.String` /// The returned object must be released after use, by calling the [release] method. - core$_.Future thinkBeforeAnswering() async { + core$_.Future thinkBeforeAnswering( + Thinker thinker, + ) async { final $p = jni$_.ReceivePort(); final _$continuation = jni$_.ProtectedJniExtensions.newPortContinuation($p); - + final _$thinker = thinker.reference; final $r = _thinkBeforeAnswering( reference.pointer, _id_thinkBeforeAnswering as jni$_.JMethodIDPtr, + _$thinker.pointer, _$continuation.pointer) .object(const jni$_.$JObject$Type$()); _$continuation.release(); @@ -199,3 +208,246 @@ final class $Example$Type$ extends jni$_.JType { return other.runtimeType == ($Example$Type$) && other is $Example$Type$; } } + +/// from: `Thinker` +class Thinker extends jni$_.JObject { + @jni$_.internal + @core$_.override + final jni$_.JType $type; + + @jni$_.internal + Thinker.fromReference( + jni$_.JReference reference, + ) : $type = type, + super.fromReference(reference); + + static final _class = jni$_.JClass.forName(r'Thinker'); + + /// The type which includes information such as the signature of this class. + static const jni$_.JType nullableType = $Thinker$NullableType$(); + + /// The type which includes information such as the signature of this class. + static const jni$_.JType type = $Thinker$Type$(); + static final _id_message = _class.instanceMethodId( + r'message', + r'(Lkotlin/coroutines/Continuation;)Ljava/lang/Object;', + ); + + static final _message = jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer,)>)>>( + 'globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function(jni$_.Pointer, + jni$_.JMethodIDPtr, jni$_.Pointer)>(); + + /// from: `public suspend fun message(): kotlin.String` + /// The returned object must be released after use, by calling the [release] method. + core$_.Future message() async { + final $p = jni$_.ReceivePort(); + final _$continuation = jni$_.ProtectedJniExtensions.newPortContinuation($p); + + final $r = _message(reference.pointer, _id_message as jni$_.JMethodIDPtr, + _$continuation.pointer) + .object(const jni$_.$JObject$Type$()); + _$continuation.release(); + final jni$_.JObject $o; + if ($r.isInstanceOf(jni$_.coroutineSingletonsClass)) { + $r.release(); + final $a = await $p.first; + $o = jni$_.JObject.fromReference( + jni$_.JGlobalReference(jni$_.JObjectPtr.fromAddress($a))); + if ($o.isInstanceOf(jni$_.result$FailureClass)) { + final $e = + jni$_.failureExceptionField.get($o, const jni$_.$JObject$Type$()); + $o.release(); + jni$_.Jni.throwException($e.reference.toPointer()); + } + } else { + $o = $r; + } + return $o.as( + const jni$_.$JString$Type$(), + releaseOriginal: true, + ); + } + + /// Maps a specific port to the implemented interface. + static final core$_.Map _$impls = {}; + static jni$_.JObjectPtr _$invoke( + int port, + jni$_.JObjectPtr descriptor, + jni$_.JObjectPtr args, + ) { + return _$invokeMethod( + port, + jni$_.MethodInvocation.fromAddresses( + 0, + descriptor.address, + args.address, + ), + ); + } + + static final jni$_.Pointer< + jni$_.NativeFunction< + jni$_.JObjectPtr Function( + jni$_.Int64, jni$_.JObjectPtr, jni$_.JObjectPtr)>> + _$invokePointer = jni$_.Pointer.fromFunction(_$invoke); + + static jni$_.Pointer _$invokeMethod( + int $p, + jni$_.MethodInvocation $i, + ) { + try { + final $d = $i.methodDescriptor.toDartString(releaseOriginal: true); + final $a = $i.args; + if ($d == + r'message(Lkotlin/coroutines/Continuation;)Ljava/lang/Object;') { + final $r = _$impls[$p]!.message( + $a![0]!.as(const jni$_.$JObject$Type$(), releaseOriginal: true), + ); + return ($r as jni$_.JObject?) + ?.as(const jni$_.$JObject$Type$()) + .reference + .toPointer() ?? + jni$_.nullptr; + } + } catch (e) { + return jni$_.ProtectedJniExtensions.newDartException(e); + } + return jni$_.nullptr; + } + + static void implementIn( + jni$_.JImplementer implementer, + $Thinker $impl, + ) { + late final jni$_.RawReceivePort $p; + $p = jni$_.RawReceivePort(($m) { + if ($m == null) { + _$impls.remove($p.sendPort.nativePort); + $p.close(); + return; + } + final $i = jni$_.MethodInvocation.fromMessage($m); + final $r = _$invokeMethod($p.sendPort.nativePort, $i); + jni$_.ProtectedJniExtensions.returnResult($i.result, $r); + }); + implementer.add( + r'Thinker', + $p, + _$invokePointer, + [], + ); + final $a = $p.sendPort.nativePort; + _$impls[$a] = $impl; + } + + factory Thinker.implement( + $Thinker $impl, + ) { + final $i = jni$_.JImplementer(); + implementIn($i, $impl); + return Thinker.fromReference( + $i.implementReference(), + ); + } +} + +abstract base mixin class $Thinker { + factory $Thinker({ + required core$_.Future Function() message, + }) = _$Thinker; + + jni$_.JObject message(jni$_.JObject continuation); +} + +final class _$Thinker with $Thinker { + _$Thinker({ + required core$_.Future Function() message, + }) : _message = message; + + final core$_.Future Function() _message; + + jni$_.JObject message(jni$_.JObject continuation) { + return jni$_.KotlinContinuation.fromReference(continuation.reference) + .resumeWithFuture(_message().then((jni$_.JString result) { + return jni$_.JString.fromString("Bindings(" + result.toDartString() + ")"); + })); + } +} + +final class $Thinker$NullableType$ extends jni$_.JType { + @jni$_.internal + const $Thinker$NullableType$(); + + @jni$_.internal + @core$_.override + String get signature => r'LThinker;'; + + @jni$_.internal + @core$_.override + Thinker? fromReference(jni$_.JReference reference) => reference.isNull + ? null + : Thinker.fromReference( + reference, + ); + @jni$_.internal + @core$_.override + jni$_.JType get superType => const jni$_.$JObject$Type$(); + + @jni$_.internal + @core$_.override + jni$_.JType get nullableType => this; + + @jni$_.internal + @core$_.override + final superCount = 1; + + @core$_.override + int get hashCode => ($Thinker$NullableType$).hashCode; + + @core$_.override + bool operator ==(Object other) { + return other.runtimeType == ($Thinker$NullableType$) && + other is $Thinker$NullableType$; + } +} + +final class $Thinker$Type$ extends jni$_.JType { + @jni$_.internal + const $Thinker$Type$(); + + @jni$_.internal + @core$_.override + String get signature => r'LThinker;'; + + @jni$_.internal + @core$_.override + Thinker fromReference(jni$_.JReference reference) => Thinker.fromReference( + reference, + ); + @jni$_.internal + @core$_.override + jni$_.JType get superType => const jni$_.$JObject$Type$(); + + @jni$_.internal + @core$_.override + jni$_.JType get nullableType => const $Thinker$NullableType$(); + + @jni$_.internal + @core$_.override + final superCount = 1; + + @core$_.override + int get hashCode => ($Thinker$Type$).hashCode; + + @core$_.override + bool operator ==(Object other) { + return other.runtimeType == ($Thinker$Type$) && other is $Thinker$Type$; + } +} From 418d97066542ad080a32ffeb209a67f471215a9b Mon Sep 17 00:00:00 2001 From: Liam Appelbe Date: Mon, 15 Dec 2025 13:36:21 +1100 Subject: [PATCH 02/10] Fixed crash, and implemented code gen --- .../dart_lang/jni/PortContinuation.java | 2 +- pkgs/jni/lib/_internal.dart | 2 +- pkgs/jni/lib/src/kotlin.dart | 21 +++--- .../kotlin_plugin/example/lib/main.dart | 7 +- .../kotlin_plugin/lib/kotlin_bindings.dart | 16 +++-- .../lib/src/bindings/dart_generator.dart | 64 ++++++++++++------- 6 files changed, 69 insertions(+), 43 deletions(-) diff --git a/pkgs/jni/java/src/main/java/com/github/dart_lang/jni/PortContinuation.java b/pkgs/jni/java/src/main/java/com/github/dart_lang/jni/PortContinuation.java index 2900ab1282..b8de3dd508 100644 --- a/pkgs/jni/java/src/main/java/com/github/dart_lang/jni/PortContinuation.java +++ b/pkgs/jni/java/src/main/java/com/github/dart_lang/jni/PortContinuation.java @@ -26,7 +26,7 @@ public PortContinuation(long port) { @Override public CoroutineContext getContext() { - return (CoroutineContext) Dispatchers.getIO(); + return (CoroutineContext) Dispatchers.getDefault(); } @Override diff --git a/pkgs/jni/lib/_internal.dart b/pkgs/jni/lib/_internal.dart index f9c934a681..c5e8fb6a17 100644 --- a/pkgs/jni/lib/_internal.dart +++ b/pkgs/jni/lib/_internal.dart @@ -53,7 +53,7 @@ export 'src/jobject.dart' show $JObject$NullableType$, $JObject$Type$; export 'src/jreference.dart'; export 'src/kotlin.dart' show coroutineSingletonsClass, failureExceptionField, - result$FailureClass, KotlinContinuation; + result$FailureClass, result$Class, resultValueField, KotlinContinuation; export 'src/lang/jboolean.dart' show $JBoolean$NullableType$, $JBoolean$Type$; export 'src/lang/jbyte.dart' show $JByte$NullableType$, $JByte$Type$; export 'src/lang/jcharacter.dart' diff --git a/pkgs/jni/lib/src/kotlin.dart b/pkgs/jni/lib/src/kotlin.dart index 48710eb702..f720337feb 100644 --- a/pkgs/jni/lib/src/kotlin.dart +++ b/pkgs/jni/lib/src/kotlin.dart @@ -25,9 +25,18 @@ final result$FailureClass = JClass.forName(r'kotlin/Result$Failure'); final failureExceptionField = result$FailureClass.instanceFieldId('exception', 'Ljava/lang/Throwable;'); -final _coroutineSuspended = coroutineSingletonsClass.staticFieldId( - 'COROUTINE_SUSPENDED', 'Lkotlin/coroutines/intrinsics/CoroutineSingletons') - .get(coroutineSingletonsClass, const $JObject$Type$()); +@internal +final result$Class = JClass.forName(r'kotlin/Result'); + +@internal +final resultValueField = + result$Class.instanceFieldId('value', 'Ljava/lang/Object;'); + +final _coroutineIntrinsicsClass = + JClass.forName('kotlin/coroutines/intrinsics/IntrinsicsKt'); +final _coroutineSuspended = _coroutineIntrinsicsClass.staticMethodId( + 'getCOROUTINE_SUSPENDED', '()Ljava/lang/Object;')( + _coroutineIntrinsicsClass, const $JObject$Type$(), [])!; @internal class KotlinContinuation extends JObject { @@ -44,11 +53,7 @@ class KotlinContinuation extends JObject { } JObject resumeWithFuture(Future future) { - future.then((JObject result) { - final r = _Result(result); - print(r); - resumeWith(r); - }, onError: (error) { + future.then(resumeWith, onError: (error) { // TODO }); return _coroutineSuspended; diff --git a/pkgs/jnigen/example/kotlin_plugin/example/lib/main.dart b/pkgs/jnigen/example/kotlin_plugin/example/lib/main.dart index 041e95022c..b895f0a90f 100644 --- a/pkgs/jnigen/example/kotlin_plugin/example/lib/main.dart +++ b/pkgs/jnigen/example/kotlin_plugin/example/lib/main.dart @@ -67,15 +67,14 @@ class _MyHomePageState extends State { answer = () async { final _thinker = $Thinker( message: () async { - await Future.delayed(Duration(seconds: 3)); + await Future.delayed(Duration(milliseconds: 300)); return JString.fromString("App"); } ); final thinker = Thinker.implement(_thinker); final value1 = (await example.thinkBeforeAnswering(thinker)).toDartString(releaseOriginal: true); - final value2 = '-';//_thinker.message(JObject.fromReference(jNullReference)).as(const $JString$Type$()).toDartString(); - final value3 = '-'; //(await thinker.message()).toDartString(); - return value1 + '\n' + value2 + '\n' + value3; + final value2 = (await thinker.message()).toDartString(); + return value1 + '\n' + value2; }(); }); }, diff --git a/pkgs/jnigen/example/kotlin_plugin/lib/kotlin_bindings.dart b/pkgs/jnigen/example/kotlin_plugin/lib/kotlin_bindings.dart index 0b41fee03f..24297b9866 100644 --- a/pkgs/jnigen/example/kotlin_plugin/lib/kotlin_bindings.dart +++ b/pkgs/jnigen/example/kotlin_plugin/lib/kotlin_bindings.dart @@ -116,13 +116,15 @@ class Example extends jni$_.JObject { _$continuation.pointer) .object(const jni$_.$JObject$Type$()); _$continuation.release(); - final jni$_.JObject $o; + jni$_.JObject $o; if ($r.isInstanceOf(jni$_.coroutineSingletonsClass)) { $r.release(); final $a = await $p.first; $o = jni$_.JObject.fromReference( jni$_.JGlobalReference(jni$_.JObjectPtr.fromAddress($a))); - if ($o.isInstanceOf(jni$_.result$FailureClass)) { + if ($o.isInstanceOf(jni$_.result$Class)) { + $o = jni$_.resultValueField.get($o, const jni$_.$JObject$Type$()); + } else if ($o.isInstanceOf(jni$_.result$FailureClass)) { final $e = jni$_.failureExceptionField.get($o, const jni$_.$JObject$Type$()); $o.release(); @@ -254,13 +256,15 @@ class Thinker extends jni$_.JObject { _$continuation.pointer) .object(const jni$_.$JObject$Type$()); _$continuation.release(); - final jni$_.JObject $o; + jni$_.JObject $o; if ($r.isInstanceOf(jni$_.coroutineSingletonsClass)) { $r.release(); final $a = await $p.first; $o = jni$_.JObject.fromReference( jni$_.JGlobalReference(jni$_.JObjectPtr.fromAddress($a))); - if ($o.isInstanceOf(jni$_.result$FailureClass)) { + if ($o.isInstanceOf(jni$_.result$Class)) { + $o = jni$_.resultValueField.get($o, const jni$_.$JObject$Type$()); + } else if ($o.isInstanceOf(jni$_.result$FailureClass)) { final $e = jni$_.failureExceptionField.get($o, const jni$_.$JObject$Type$()); $o.release(); @@ -375,9 +379,7 @@ final class _$Thinker with $Thinker { jni$_.JObject message(jni$_.JObject continuation) { return jni$_.KotlinContinuation.fromReference(continuation.reference) - .resumeWithFuture(_message().then((jni$_.JString result) { - return jni$_.JString.fromString("Bindings(" + result.toDartString() + ")"); - })); + .resumeWithFuture(_message()); } } diff --git a/pkgs/jnigen/lib/src/bindings/dart_generator.dart b/pkgs/jnigen/lib/src/bindings/dart_generator.dart index d92ed11dd3..ca54598d74 100644 --- a/pkgs/jnigen/lib/src/bindings/dart_generator.dart +++ b/pkgs/jnigen/lib/src/bindings/dart_generator.dart @@ -81,6 +81,23 @@ extension on DeclaredType { } } +extension on Method { + bool get isSuspendFun => asyncReturnType != null; + + String returnTypeMaybeAsync(_TypeGenerator generator) => + isSuspendFun + ? '$_core.Future<${asyncReturnType!.accept(generator)}>' + : returnType.accept(generator); + + List get paramsMaybeAsync { + final p = params.toList(); + if (isSuspendFun) { + p.removeLast(); + } + return p; + } +} + String _newLine({int depth = 0}) { return '\n${' ' * depth}'; } @@ -1337,10 +1354,6 @@ ${modifier}final _$name = $_protectedExtension '''); } - bool isSuspendFun(Method node) { - return node.asyncReturnType != null; - } - String constructor(Method node) { final name = node.finalName; final params = [ @@ -1447,10 +1460,7 @@ ${modifier}final _$name = $_protectedExtension } final name = node.finalName; - final returnType = isSuspendFun(node) - ? '$_core.Future<' - '${node.asyncReturnType!.accept(_TypeGenerator(resolver))}>' - : node.returnType.accept(_TypeGenerator(resolver)); + final returnType = node.returnTypeMaybeAsync(_TypeGenerator(resolver)); final ifStatic = node.isStatic && !isTopLevel ? 'static ' : ''; final defArgs = node.params.accept(_ParamDef(resolver)).toList(); final typeClassDef = node.typeParams @@ -1465,14 +1475,14 @@ ${modifier}final _$name = $_protectedExtension .accept(const _TypeParamDef()) .join(', ') .encloseIfNotEmpty('<', '>'); - if (isSuspendFun(node)) { + if (node.isSuspendFun) { defArgs.removeLast(); localReferences.removeLast(); } final params = defArgs.delimited(', '); s.write(' $ifStatic$returnType $name$typeParamsDef($params$typeClassDef)'); final callExpr = methodCall(node); - if (isSuspendFun(node)) { + if (node.isSuspendFun) { final returningType = node.asyncReturnType!.accept(_TypeGenerator(resolver)); final returningTypeClass = @@ -1486,13 +1496,15 @@ ${modifier}final _$name = $_protectedExtension ${localReferences.join(_newLine(depth: 2))} final \$r = $callExpr; _\$$continuation.release(); - final $_jObject${isNullable ? '?' : ''} \$o; + $_jObject${isNullable ? '?' : ''} \$o; if (${isNullable ? '\$r != null && ' : ''}\$r.isInstanceOf($_jni.coroutineSingletonsClass)) { \$r.release(); final \$a = await \$p.first; \$o = ${isNullable ? '\$a == 0 ? null :' : ''}$_jObject.fromReference( $_jGlobalReference($_jPointer.fromAddress(\$a))); - if (${isNullable ? '\$o != null && ' : ''}\$o.isInstanceOf($_jni.result\$FailureClass)) { + if (${isNullable ? '\$o != null && ' : ''}\$o.isInstanceOf($_jni.result\$Class)) { + \$o = $_jni.resultValueField.get(\$o, const ${_jObjectTypePrefix}Type\$()); + } else if (${isNullable ? '\$o != null && ' : ''}\$o.isInstanceOf($_jni.result\$FailureClass)) { final \$e = $_jni.failureExceptionField.get(\$o, const ${_jObjectTypePrefix}Type\$()); \$o.release(); @@ -1862,11 +1874,9 @@ class _ConcreteImplClosureDef extends Visitor { @override void visit(Method node) { - final returnType = node.returnType.accept( - _TypeGenerator(resolver, forInterfaceImplementation: true), - ); + final returnType = node.returnTypeMaybeAsync(_TypeGenerator(resolver, forInterfaceImplementation: true)); final name = node.finalName; - final args = node.params + final args = node.paramsMaybeAsync .accept(_ParamDef(resolver, methodGenericErasure: true)) .join(', '); s.writeln(' final $returnType Function($args) _$name;'); @@ -1885,11 +1895,11 @@ class _AbstractImplFactoryArg extends Visitor { @override String visit(Method node) { - final returnType = node.returnType.accept( + final returnType = node.returnTypeMaybeAsync( _TypeGenerator(resolver, forInterfaceImplementation: true), ); final name = node.finalName; - final args = node.params + final args = node.paramsMaybeAsync .accept(_ParamDef(resolver, methodGenericErasure: true)) .join(', '); final functionArg = 'required $returnType Function($args) $name,'; @@ -1909,11 +1919,11 @@ class _ConcreteImplClosureCtorArg extends Visitor { @override String visit(Method node) { - final returnType = node.returnType.accept( + final returnType = node.returnTypeMaybeAsync( _TypeGenerator(resolver, forInterfaceImplementation: true), ); final name = node.finalName; - final args = node.params + final args = node.paramsMaybeAsync .accept(_ParamDef(resolver, methodGenericErasure: true)) .join(', '); final functionArg = 'required $returnType Function($args) $name,'; @@ -1940,11 +1950,21 @@ class _ConcreteImplMethod extends Visitor { final argsDef = node.params .accept(_ParamDef(resolver, methodGenericErasure: true)) .join(', '); - final argsCall = node.params.map((param) => param.finalName).join(', '); - s.write(''' + final argsCall = + node.paramsMaybeAsync.map((param) => param.finalName).join(', '); + if (node.isSuspendFun) { + final contArg = node.params.last.finalName; + s.write(''' + $returnType $name($argsDef) { + return $_jni.KotlinContinuation.fromReference($contArg.reference) + .resumeWithFuture(_$name($argsCall)); + }'''); + } else { + s.write(''' $returnType $name($argsDef) { return _$name($argsCall); }'''); + } } } From 90c3c1bcb2664769f7a5a292fbefea5459b34f2c Mon Sep 17 00:00:00 2001 From: Liam Appelbe Date: Tue, 16 Dec 2025 13:39:13 +1100 Subject: [PATCH 03/10] format --- .../com/github/dart_lang/jni/PortContinuation.java | 2 +- .../example/kotlin_plugin/example/lib/main.dart | 13 ++++++------- pkgs/jnigen/lib/src/bindings/dart_generator.dart | 10 +++++----- 3 files changed, 12 insertions(+), 13 deletions(-) diff --git a/pkgs/jni/java/src/main/java/com/github/dart_lang/jni/PortContinuation.java b/pkgs/jni/java/src/main/java/com/github/dart_lang/jni/PortContinuation.java index b8de3dd508..2900ab1282 100644 --- a/pkgs/jni/java/src/main/java/com/github/dart_lang/jni/PortContinuation.java +++ b/pkgs/jni/java/src/main/java/com/github/dart_lang/jni/PortContinuation.java @@ -26,7 +26,7 @@ public PortContinuation(long port) { @Override public CoroutineContext getContext() { - return (CoroutineContext) Dispatchers.getDefault(); + return (CoroutineContext) Dispatchers.getIO(); } @Override diff --git a/pkgs/jnigen/example/kotlin_plugin/example/lib/main.dart b/pkgs/jnigen/example/kotlin_plugin/example/lib/main.dart index b895f0a90f..32aaa6f6d6 100644 --- a/pkgs/jnigen/example/kotlin_plugin/example/lib/main.dart +++ b/pkgs/jnigen/example/kotlin_plugin/example/lib/main.dart @@ -65,14 +65,13 @@ class _MyHomePageState extends State { onPressed: () { setState(() { answer = () async { - final _thinker = $Thinker( - message: () async { - await Future.delayed(Duration(milliseconds: 300)); - return JString.fromString("App"); - } - ); + final _thinker = $Thinker(message: () async { + await Future.delayed(Duration(milliseconds: 300)); + return JString.fromString("App"); + }); final thinker = Thinker.implement(_thinker); - final value1 = (await example.thinkBeforeAnswering(thinker)).toDartString(releaseOriginal: true); + final value1 = (await example.thinkBeforeAnswering(thinker)) + .toDartString(releaseOriginal: true); final value2 = (await thinker.message()).toDartString(); return value1 + '\n' + value2; }(); diff --git a/pkgs/jnigen/lib/src/bindings/dart_generator.dart b/pkgs/jnigen/lib/src/bindings/dart_generator.dart index ca54598d74..421e5d244c 100644 --- a/pkgs/jnigen/lib/src/bindings/dart_generator.dart +++ b/pkgs/jnigen/lib/src/bindings/dart_generator.dart @@ -84,10 +84,9 @@ extension on DeclaredType { extension on Method { bool get isSuspendFun => asyncReturnType != null; - String returnTypeMaybeAsync(_TypeGenerator generator) => - isSuspendFun - ? '$_core.Future<${asyncReturnType!.accept(generator)}>' - : returnType.accept(generator); + String returnTypeMaybeAsync(_TypeGenerator generator) => isSuspendFun + ? '$_core.Future<${asyncReturnType!.accept(generator)}>' + : returnType.accept(generator); List get paramsMaybeAsync { final p = params.toList(); @@ -1874,7 +1873,8 @@ class _ConcreteImplClosureDef extends Visitor { @override void visit(Method node) { - final returnType = node.returnTypeMaybeAsync(_TypeGenerator(resolver, forInterfaceImplementation: true)); + final returnType = node.returnTypeMaybeAsync( + _TypeGenerator(resolver, forInterfaceImplementation: true)); final name = node.finalName; final args = node.paramsMaybeAsync .accept(_ParamDef(resolver, methodGenericErasure: true)) From 64b5434f0607052b34a4757ce21a33a5f84f7806 Mon Sep 17 00:00:00 2001 From: Liam Appelbe Date: Tue, 16 Dec 2025 14:54:43 +1100 Subject: [PATCH 04/10] basic tests --- pkgs/jni/lib/src/kotlin.dart | 4 +- .../test/kotlin_test/bindings/kotlin.dart | 689 +++++++++++++++++- .../com/github/dart_lang/jnigen/SuspendFun.kt | 9 + .../kotlin_test/runtime_test_registrant.dart | 69 ++ .../bindings/simple_package.dart | 2 +- 5 files changed, 756 insertions(+), 17 deletions(-) diff --git a/pkgs/jni/lib/src/kotlin.dart b/pkgs/jni/lib/src/kotlin.dart index f720337feb..20f4359e4f 100644 --- a/pkgs/jni/lib/src/kotlin.dart +++ b/pkgs/jni/lib/src/kotlin.dart @@ -48,11 +48,11 @@ class KotlinContinuation extends JObject { static final _resumeWithId = _class.instanceMethodId(r'resumeWith', r'(Ljava/lang/Object;)V'); - void resumeWith(JObject result) { + void resumeWith(JObject? result) { _resumeWithId(this, const jvoidType(), [result]); } - JObject resumeWithFuture(Future future) { + JObject resumeWithFuture(Future future) { future.then(resumeWith, onError: (error) { // TODO }); diff --git a/pkgs/jnigen/test/kotlin_test/bindings/kotlin.dart b/pkgs/jnigen/test/kotlin_test/bindings/kotlin.dart index 2a14012ab0..e4795d5adc 100644 --- a/pkgs/jnigen/test/kotlin_test/bindings/kotlin.dart +++ b/pkgs/jnigen/test/kotlin_test/bindings/kotlin.dart @@ -3162,13 +3162,15 @@ class SuspendFun extends jni$_.JObject { _$continuation.pointer) .object(const jni$_.$JObject$Type$()); _$continuation.release(); - final jni$_.JObject $o; + jni$_.JObject $o; if ($r.isInstanceOf(jni$_.coroutineSingletonsClass)) { $r.release(); final $a = await $p.first; $o = jni$_.JObject.fromReference( jni$_.JGlobalReference(jni$_.JObjectPtr.fromAddress($a))); - if ($o.isInstanceOf(jni$_.result$FailureClass)) { + if ($o.isInstanceOf(jni$_.result$Class)) { + $o = jni$_.resultValueField.get($o, const jni$_.$JObject$Type$()); + } else if ($o.isInstanceOf(jni$_.result$FailureClass)) { final $e = jni$_.failureExceptionField.get($o, const jni$_.$JObject$Type$()); $o.release(); @@ -3209,13 +3211,15 @@ class SuspendFun extends jni$_.JObject { _id_failWithoutDelay as jni$_.JMethodIDPtr, _$continuation.pointer) .object(const jni$_.$JObject$Type$()); _$continuation.release(); - final jni$_.JObject $o; + jni$_.JObject $o; if ($r.isInstanceOf(jni$_.coroutineSingletonsClass)) { $r.release(); final $a = await $p.first; $o = jni$_.JObject.fromReference( jni$_.JGlobalReference(jni$_.JObjectPtr.fromAddress($a))); - if ($o.isInstanceOf(jni$_.result$FailureClass)) { + if ($o.isInstanceOf(jni$_.result$Class)) { + $o = jni$_.resultValueField.get($o, const jni$_.$JObject$Type$()); + } else if ($o.isInstanceOf(jni$_.result$FailureClass)) { final $e = jni$_.failureExceptionField.get($o, const jni$_.$JObject$Type$()); $o.release(); @@ -3256,13 +3260,15 @@ class SuspendFun extends jni$_.JObject { _$continuation.pointer) .object(const jni$_.$JObject$Type$()); _$continuation.release(); - final jni$_.JObject $o; + jni$_.JObject $o; if ($r.isInstanceOf(jni$_.coroutineSingletonsClass)) { $r.release(); final $a = await $p.first; $o = jni$_.JObject.fromReference( jni$_.JGlobalReference(jni$_.JObjectPtr.fromAddress($a))); - if ($o.isInstanceOf(jni$_.result$FailureClass)) { + if ($o.isInstanceOf(jni$_.result$Class)) { + $o = jni$_.resultValueField.get($o, const jni$_.$JObject$Type$()); + } else if ($o.isInstanceOf(jni$_.result$FailureClass)) { final $e = jni$_.failureExceptionField.get($o, const jni$_.$JObject$Type$()); $o.release(); @@ -3303,13 +3309,15 @@ class SuspendFun extends jni$_.JObject { _$continuation.pointer) .object(const jni$_.$JObject$Type$()); _$continuation.release(); - final jni$_.JObject $o; + jni$_.JObject $o; if ($r.isInstanceOf(jni$_.coroutineSingletonsClass)) { $r.release(); final $a = await $p.first; $o = jni$_.JObject.fromReference( jni$_.JGlobalReference(jni$_.JObjectPtr.fromAddress($a))); - if ($o.isInstanceOf(jni$_.result$FailureClass)) { + if ($o.isInstanceOf(jni$_.result$Class)) { + $o = jni$_.resultValueField.get($o, const jni$_.$JObject$Type$()); + } else if ($o.isInstanceOf(jni$_.result$FailureClass)) { final $e = jni$_.failureExceptionField.get($o, const jni$_.$JObject$Type$()); $o.release(); @@ -3361,13 +3369,15 @@ class SuspendFun extends jni$_.JObject { _$continuation.pointer) .object(const jni$_.$JObject$Type$()); _$continuation.release(); - final jni$_.JObject $o; + jni$_.JObject $o; if ($r.isInstanceOf(jni$_.coroutineSingletonsClass)) { $r.release(); final $a = await $p.first; $o = jni$_.JObject.fromReference( jni$_.JGlobalReference(jni$_.JObjectPtr.fromAddress($a))); - if ($o.isInstanceOf(jni$_.result$FailureClass)) { + if ($o.isInstanceOf(jni$_.result$Class)) { + $o = jni$_.resultValueField.get($o, const jni$_.$JObject$Type$()); + } else if ($o.isInstanceOf(jni$_.result$FailureClass)) { final $e = jni$_.failureExceptionField.get($o, const jni$_.$JObject$Type$()); $o.release(); @@ -3414,7 +3424,7 @@ class SuspendFun extends jni$_.JObject { _$continuation.pointer) .object(const jni$_.$JObject$NullableType$()); _$continuation.release(); - final jni$_.JObject? $o; + jni$_.JObject? $o; if ($r != null && $r.isInstanceOf(jni$_.coroutineSingletonsClass)) { $r.release(); final $a = await $p.first; @@ -3422,7 +3432,9 @@ class SuspendFun extends jni$_.JObject { ? null : jni$_.JObject.fromReference( jni$_.JGlobalReference(jni$_.JObjectPtr.fromAddress($a))); - if ($o != null && $o.isInstanceOf(jni$_.result$FailureClass)) { + if ($o != null && $o.isInstanceOf(jni$_.result$Class)) { + $o = jni$_.resultValueField.get($o, const jni$_.$JObject$Type$()); + } else if ($o != null && $o.isInstanceOf(jni$_.result$FailureClass)) { final $e = jni$_.failureExceptionField.get($o, const jni$_.$JObject$Type$()); $o.release(); @@ -3469,7 +3481,7 @@ class SuspendFun extends jni$_.JObject { _$continuation.pointer) .object(const jni$_.$JObject$NullableType$()); _$continuation.release(); - final jni$_.JObject? $o; + jni$_.JObject? $o; if ($r != null && $r.isInstanceOf(jni$_.coroutineSingletonsClass)) { $r.release(); final $a = await $p.first; @@ -3477,7 +3489,9 @@ class SuspendFun extends jni$_.JObject { ? null : jni$_.JObject.fromReference( jni$_.JGlobalReference(jni$_.JObjectPtr.fromAddress($a))); - if ($o != null && $o.isInstanceOf(jni$_.result$FailureClass)) { + if ($o != null && $o.isInstanceOf(jni$_.result$Class)) { + $o = jni$_.resultValueField.get($o, const jni$_.$JObject$Type$()); + } else if ($o != null && $o.isInstanceOf(jni$_.result$FailureClass)) { final $e = jni$_.failureExceptionField.get($o, const jni$_.$JObject$Type$()); $o.release(); @@ -3567,6 +3581,653 @@ final class $SuspendFun$Type$ extends jni$_.JType { } } +/// from: `com.github.dart_lang.jnigen.SuspendInterface` +class SuspendInterface extends jni$_.JObject { + @jni$_.internal + @core$_.override + final jni$_.JType $type; + + @jni$_.internal + SuspendInterface.fromReference( + jni$_.JReference reference, + ) : $type = type, + super.fromReference(reference); + + static final _class = + jni$_.JClass.forName(r'com/github/dart_lang/jnigen/SuspendInterface'); + + /// The type which includes information such as the signature of this class. + static const jni$_.JType nullableType = + $SuspendInterface$NullableType$(); + + /// The type which includes information such as the signature of this class. + static const jni$_.JType type = $SuspendInterface$Type$(); + static final _id_sayHello = _class.instanceMethodId( + r'sayHello', + r'(Lkotlin/coroutines/Continuation;)Ljava/lang/Object;', + ); + + static final _sayHello = jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer,)>)>>( + 'globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function(jni$_.Pointer, + jni$_.JMethodIDPtr, jni$_.Pointer)>(); + + /// from: `public suspend fun sayHello(): kotlin.String` + /// The returned object must be released after use, by calling the [release] method. + core$_.Future sayHello() async { + final $p = jni$_.ReceivePort(); + final _$continuation = jni$_.ProtectedJniExtensions.newPortContinuation($p); + + final $r = _sayHello(reference.pointer, _id_sayHello as jni$_.JMethodIDPtr, + _$continuation.pointer) + .object(const jni$_.$JObject$Type$()); + _$continuation.release(); + jni$_.JObject $o; + if ($r.isInstanceOf(jni$_.coroutineSingletonsClass)) { + $r.release(); + final $a = await $p.first; + $o = jni$_.JObject.fromReference( + jni$_.JGlobalReference(jni$_.JObjectPtr.fromAddress($a))); + if ($o.isInstanceOf(jni$_.result$Class)) { + $o = jni$_.resultValueField.get($o, const jni$_.$JObject$Type$()); + } else if ($o.isInstanceOf(jni$_.result$FailureClass)) { + final $e = + jni$_.failureExceptionField.get($o, const jni$_.$JObject$Type$()); + $o.release(); + jni$_.Jni.throwException($e.reference.toPointer()); + } + } else { + $o = $r; + } + return $o.as( + const jni$_.$JString$Type$(), + releaseOriginal: true, + ); + } + + static final _id_sayHello$1 = _class.instanceMethodId( + r'sayHello', + r'(Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;', + ); + + static final _sayHello$1 = jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs< + ( + jni$_.Pointer, + jni$_.Pointer + )>)>>('globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + jni$_.Pointer)>(); + + /// from: `public suspend fun sayHello(name: kotlin.String): kotlin.String` + /// The returned object must be released after use, by calling the [release] method. + core$_.Future sayHello$1( + jni$_.JString string, + ) async { + final $p = jni$_.ReceivePort(); + final _$continuation = jni$_.ProtectedJniExtensions.newPortContinuation($p); + final _$string = string.reference; + final $r = _sayHello$1( + reference.pointer, + _id_sayHello$1 as jni$_.JMethodIDPtr, + _$string.pointer, + _$continuation.pointer) + .object(const jni$_.$JObject$Type$()); + _$continuation.release(); + jni$_.JObject $o; + if ($r.isInstanceOf(jni$_.coroutineSingletonsClass)) { + $r.release(); + final $a = await $p.first; + $o = jni$_.JObject.fromReference( + jni$_.JGlobalReference(jni$_.JObjectPtr.fromAddress($a))); + if ($o.isInstanceOf(jni$_.result$Class)) { + $o = jni$_.resultValueField.get($o, const jni$_.$JObject$Type$()); + } else if ($o.isInstanceOf(jni$_.result$FailureClass)) { + final $e = + jni$_.failureExceptionField.get($o, const jni$_.$JObject$Type$()); + $o.release(); + jni$_.Jni.throwException($e.reference.toPointer()); + } + } else { + $o = $r; + } + return $o.as( + const jni$_.$JString$Type$(), + releaseOriginal: true, + ); + } + + static final _id_nullableHello = _class.instanceMethodId( + r'nullableHello', + r'(ZLkotlin/coroutines/Continuation;)Ljava/lang/Object;', + ); + + static final _nullableHello = jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_ + .VarArgs<(jni$_.Int32, jni$_.Pointer)>)>>( + 'globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function(jni$_.Pointer, + jni$_.JMethodIDPtr, int, jni$_.Pointer)>(); + + /// from: `public suspend fun nullableHello(returnNull: kotlin.Boolean): kotlin.String?` + /// The returned object must be released after use, by calling the [release] method. + core$_.Future nullableHello( + bool z, + ) async { + final $p = jni$_.ReceivePort(); + final _$continuation = jni$_.ProtectedJniExtensions.newPortContinuation($p); + + final $r = _nullableHello( + reference.pointer, + _id_nullableHello as jni$_.JMethodIDPtr, + z ? 1 : 0, + _$continuation.pointer) + .object(const jni$_.$JObject$NullableType$()); + _$continuation.release(); + jni$_.JObject? $o; + if ($r != null && $r.isInstanceOf(jni$_.coroutineSingletonsClass)) { + $r.release(); + final $a = await $p.first; + $o = $a == 0 + ? null + : jni$_.JObject.fromReference( + jni$_.JGlobalReference(jni$_.JObjectPtr.fromAddress($a))); + if ($o != null && $o.isInstanceOf(jni$_.result$Class)) { + $o = jni$_.resultValueField.get($o, const jni$_.$JObject$Type$()); + } else if ($o != null && $o.isInstanceOf(jni$_.result$FailureClass)) { + final $e = + jni$_.failureExceptionField.get($o, const jni$_.$JObject$Type$()); + $o.release(); + jni$_.Jni.throwException($e.reference.toPointer()); + } + } else { + $o = $r; + } + return $o?.as( + const jni$_.$JString$NullableType$(), + releaseOriginal: true, + ); + } + + static final _id_sayInt = _class.instanceMethodId( + r'sayInt', + r'(Lkotlin/coroutines/Continuation;)Ljava/lang/Object;', + ); + + static final _sayInt = jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer,)>)>>( + 'globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function(jni$_.Pointer, + jni$_.JMethodIDPtr, jni$_.Pointer)>(); + + /// from: `public suspend fun sayInt(): java.lang.Integer` + /// The returned object must be released after use, by calling the [release] method. + core$_.Future sayInt() async { + final $p = jni$_.ReceivePort(); + final _$continuation = jni$_.ProtectedJniExtensions.newPortContinuation($p); + + final $r = _sayInt(reference.pointer, _id_sayInt as jni$_.JMethodIDPtr, + _$continuation.pointer) + .object(const jni$_.$JObject$Type$()); + _$continuation.release(); + jni$_.JObject $o; + if ($r.isInstanceOf(jni$_.coroutineSingletonsClass)) { + $r.release(); + final $a = await $p.first; + $o = jni$_.JObject.fromReference( + jni$_.JGlobalReference(jni$_.JObjectPtr.fromAddress($a))); + if ($o.isInstanceOf(jni$_.result$Class)) { + $o = jni$_.resultValueField.get($o, const jni$_.$JObject$Type$()); + } else if ($o.isInstanceOf(jni$_.result$FailureClass)) { + final $e = + jni$_.failureExceptionField.get($o, const jni$_.$JObject$Type$()); + $o.release(); + jni$_.Jni.throwException($e.reference.toPointer()); + } + } else { + $o = $r; + } + return $o.as( + const jni$_.$JInteger$Type$(), + releaseOriginal: true, + ); + } + + static final _id_sayInt$1 = _class.instanceMethodId( + r'sayInt', + r'(Ljava/lang/Integer;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;', + ); + + static final _sayInt$1 = jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs< + ( + jni$_.Pointer, + jni$_.Pointer + )>)>>('globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + jni$_.Pointer)>(); + + /// from: `public suspend fun sayInt(value: java.lang.Integer): java.lang.Integer` + /// The returned object must be released after use, by calling the [release] method. + core$_.Future sayInt$1( + jni$_.JInteger integer, + ) async { + final $p = jni$_.ReceivePort(); + final _$continuation = jni$_.ProtectedJniExtensions.newPortContinuation($p); + final _$integer = integer.reference; + final $r = _sayInt$1(reference.pointer, _id_sayInt$1 as jni$_.JMethodIDPtr, + _$integer.pointer, _$continuation.pointer) + .object(const jni$_.$JObject$Type$()); + _$continuation.release(); + jni$_.JObject $o; + if ($r.isInstanceOf(jni$_.coroutineSingletonsClass)) { + $r.release(); + final $a = await $p.first; + $o = jni$_.JObject.fromReference( + jni$_.JGlobalReference(jni$_.JObjectPtr.fromAddress($a))); + if ($o.isInstanceOf(jni$_.result$Class)) { + $o = jni$_.resultValueField.get($o, const jni$_.$JObject$Type$()); + } else if ($o.isInstanceOf(jni$_.result$FailureClass)) { + final $e = + jni$_.failureExceptionField.get($o, const jni$_.$JObject$Type$()); + $o.release(); + jni$_.Jni.throwException($e.reference.toPointer()); + } + } else { + $o = $r; + } + return $o.as( + const jni$_.$JInteger$Type$(), + releaseOriginal: true, + ); + } + + static final _id_nullableInt = _class.instanceMethodId( + r'nullableInt', + r'(ZLkotlin/coroutines/Continuation;)Ljava/lang/Object;', + ); + + static final _nullableInt = jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_ + .VarArgs<(jni$_.Int32, jni$_.Pointer)>)>>( + 'globalEnv_CallObjectMethod') + .asFunction< + jni$_.JniResult Function(jni$_.Pointer, + jni$_.JMethodIDPtr, int, jni$_.Pointer)>(); + + /// from: `public suspend fun nullableInt(returnNull: kotlin.Boolean): java.lang.Integer?` + /// The returned object must be released after use, by calling the [release] method. + core$_.Future nullableInt( + bool z, + ) async { + final $p = jni$_.ReceivePort(); + final _$continuation = jni$_.ProtectedJniExtensions.newPortContinuation($p); + + final $r = _nullableInt( + reference.pointer, + _id_nullableInt as jni$_.JMethodIDPtr, + z ? 1 : 0, + _$continuation.pointer) + .object(const jni$_.$JObject$NullableType$()); + _$continuation.release(); + jni$_.JObject? $o; + if ($r != null && $r.isInstanceOf(jni$_.coroutineSingletonsClass)) { + $r.release(); + final $a = await $p.first; + $o = $a == 0 + ? null + : jni$_.JObject.fromReference( + jni$_.JGlobalReference(jni$_.JObjectPtr.fromAddress($a))); + if ($o != null && $o.isInstanceOf(jni$_.result$Class)) { + $o = jni$_.resultValueField.get($o, const jni$_.$JObject$Type$()); + } else if ($o != null && $o.isInstanceOf(jni$_.result$FailureClass)) { + final $e = + jni$_.failureExceptionField.get($o, const jni$_.$JObject$Type$()); + $o.release(); + jni$_.Jni.throwException($e.reference.toPointer()); + } + } else { + $o = $r; + } + return $o?.as( + const jni$_.$JInteger$NullableType$(), + releaseOriginal: true, + ); + } + + /// Maps a specific port to the implemented interface. + static final core$_.Map _$impls = {}; + static jni$_.JObjectPtr _$invoke( + int port, + jni$_.JObjectPtr descriptor, + jni$_.JObjectPtr args, + ) { + return _$invokeMethod( + port, + jni$_.MethodInvocation.fromAddresses( + 0, + descriptor.address, + args.address, + ), + ); + } + + static final jni$_.Pointer< + jni$_.NativeFunction< + jni$_.JObjectPtr Function( + jni$_.Int64, jni$_.JObjectPtr, jni$_.JObjectPtr)>> + _$invokePointer = jni$_.Pointer.fromFunction(_$invoke); + + static jni$_.Pointer _$invokeMethod( + int $p, + jni$_.MethodInvocation $i, + ) { + try { + final $d = $i.methodDescriptor.toDartString(releaseOriginal: true); + final $a = $i.args; + if ($d == + r'sayHello(Lkotlin/coroutines/Continuation;)Ljava/lang/Object;') { + final $r = _$impls[$p]!.sayHello( + $a![0]!.as(const jni$_.$JObject$Type$(), releaseOriginal: true), + ); + return ($r as jni$_.JObject?) + ?.as(const jni$_.$JObject$Type$()) + .reference + .toPointer() ?? + jni$_.nullptr; + } + if ($d == + r'sayHello(Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;') { + final $r = _$impls[$p]!.sayHello$1( + $a![0]!.as(const jni$_.$JString$Type$(), releaseOriginal: true), + $a![1]!.as(const jni$_.$JObject$Type$(), releaseOriginal: true), + ); + return ($r as jni$_.JObject?) + ?.as(const jni$_.$JObject$Type$()) + .reference + .toPointer() ?? + jni$_.nullptr; + } + if ($d == + r'nullableHello(ZLkotlin/coroutines/Continuation;)Ljava/lang/Object;') { + final $r = _$impls[$p]!.nullableHello( + $a![0]! + .as(const jni$_.$JBoolean$Type$(), releaseOriginal: true) + .booleanValue(releaseOriginal: true), + $a![1]!.as(const jni$_.$JObject$Type$(), releaseOriginal: true), + ); + return ($r as jni$_.JObject?) + ?.as(const jni$_.$JObject$Type$()) + .reference + .toPointer() ?? + jni$_.nullptr; + } + if ($d == r'sayInt(Lkotlin/coroutines/Continuation;)Ljava/lang/Object;') { + final $r = _$impls[$p]!.sayInt( + $a![0]!.as(const jni$_.$JObject$Type$(), releaseOriginal: true), + ); + return ($r as jni$_.JObject?) + ?.as(const jni$_.$JObject$Type$()) + .reference + .toPointer() ?? + jni$_.nullptr; + } + if ($d == + r'sayInt(Ljava/lang/Integer;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;') { + final $r = _$impls[$p]!.sayInt$1( + $a![0]!.as(const jni$_.$JInteger$Type$(), releaseOriginal: true), + $a![1]!.as(const jni$_.$JObject$Type$(), releaseOriginal: true), + ); + return ($r as jni$_.JObject?) + ?.as(const jni$_.$JObject$Type$()) + .reference + .toPointer() ?? + jni$_.nullptr; + } + if ($d == + r'nullableInt(ZLkotlin/coroutines/Continuation;)Ljava/lang/Object;') { + final $r = _$impls[$p]!.nullableInt( + $a![0]! + .as(const jni$_.$JBoolean$Type$(), releaseOriginal: true) + .booleanValue(releaseOriginal: true), + $a![1]!.as(const jni$_.$JObject$Type$(), releaseOriginal: true), + ); + return ($r as jni$_.JObject?) + ?.as(const jni$_.$JObject$Type$()) + .reference + .toPointer() ?? + jni$_.nullptr; + } + } catch (e) { + return jni$_.ProtectedJniExtensions.newDartException(e); + } + return jni$_.nullptr; + } + + static void implementIn( + jni$_.JImplementer implementer, + $SuspendInterface $impl, + ) { + late final jni$_.RawReceivePort $p; + $p = jni$_.RawReceivePort(($m) { + if ($m == null) { + _$impls.remove($p.sendPort.nativePort); + $p.close(); + return; + } + final $i = jni$_.MethodInvocation.fromMessage($m); + final $r = _$invokeMethod($p.sendPort.nativePort, $i); + jni$_.ProtectedJniExtensions.returnResult($i.result, $r); + }); + implementer.add( + r'com.github.dart_lang.jnigen.SuspendInterface', + $p, + _$invokePointer, + [], + ); + final $a = $p.sendPort.nativePort; + _$impls[$a] = $impl; + } + + factory SuspendInterface.implement( + $SuspendInterface $impl, + ) { + final $i = jni$_.JImplementer(); + implementIn($i, $impl); + return SuspendInterface.fromReference( + $i.implementReference(), + ); + } +} + +abstract base mixin class $SuspendInterface { + factory $SuspendInterface({ + required core$_.Future Function() sayHello, + required core$_.Future Function(jni$_.JString string) + sayHello$1, + required core$_.Future Function(bool z) nullableHello, + required core$_.Future Function() sayInt, + required core$_.Future Function(jni$_.JInteger integer) + sayInt$1, + required core$_.Future Function(bool z) nullableInt, + }) = _$SuspendInterface; + + jni$_.JObject sayHello(jni$_.JObject continuation); + jni$_.JObject sayHello$1(jni$_.JString string, jni$_.JObject continuation); + jni$_.JObject? nullableHello(bool z, jni$_.JObject continuation); + jni$_.JObject sayInt(jni$_.JObject continuation); + jni$_.JObject sayInt$1(jni$_.JInteger integer, jni$_.JObject continuation); + jni$_.JObject? nullableInt(bool z, jni$_.JObject continuation); +} + +final class _$SuspendInterface with $SuspendInterface { + _$SuspendInterface({ + required core$_.Future Function() sayHello, + required core$_.Future Function(jni$_.JString string) + sayHello$1, + required core$_.Future Function(bool z) nullableHello, + required core$_.Future Function() sayInt, + required core$_.Future Function(jni$_.JInteger integer) + sayInt$1, + required core$_.Future Function(bool z) nullableInt, + }) : _sayHello = sayHello, + _sayHello$1 = sayHello$1, + _nullableHello = nullableHello, + _sayInt = sayInt, + _sayInt$1 = sayInt$1, + _nullableInt = nullableInt; + + final core$_.Future Function() _sayHello; + final core$_.Future Function(jni$_.JString string) _sayHello$1; + final core$_.Future Function(bool z) _nullableHello; + final core$_.Future Function() _sayInt; + final core$_.Future Function(jni$_.JInteger integer) + _sayInt$1; + final core$_.Future Function(bool z) _nullableInt; + + jni$_.JObject sayHello(jni$_.JObject continuation) { + return jni$_.KotlinContinuation.fromReference(continuation.reference) + .resumeWithFuture(_sayHello()); + } + + jni$_.JObject sayHello$1(jni$_.JString string, jni$_.JObject continuation) { + return jni$_.KotlinContinuation.fromReference(continuation.reference) + .resumeWithFuture(_sayHello$1(string)); + } + + jni$_.JObject? nullableHello(bool z, jni$_.JObject continuation) { + return jni$_.KotlinContinuation.fromReference(continuation.reference) + .resumeWithFuture(_nullableHello(z)); + } + + jni$_.JObject sayInt(jni$_.JObject continuation) { + return jni$_.KotlinContinuation.fromReference(continuation.reference) + .resumeWithFuture(_sayInt()); + } + + jni$_.JObject sayInt$1(jni$_.JInteger integer, jni$_.JObject continuation) { + return jni$_.KotlinContinuation.fromReference(continuation.reference) + .resumeWithFuture(_sayInt$1(integer)); + } + + jni$_.JObject? nullableInt(bool z, jni$_.JObject continuation) { + return jni$_.KotlinContinuation.fromReference(continuation.reference) + .resumeWithFuture(_nullableInt(z)); + } +} + +final class $SuspendInterface$NullableType$ + extends jni$_.JType { + @jni$_.internal + const $SuspendInterface$NullableType$(); + + @jni$_.internal + @core$_.override + String get signature => r'Lcom/github/dart_lang/jnigen/SuspendInterface;'; + + @jni$_.internal + @core$_.override + SuspendInterface? fromReference(jni$_.JReference reference) => + reference.isNull + ? null + : SuspendInterface.fromReference( + reference, + ); + @jni$_.internal + @core$_.override + jni$_.JType get superType => const jni$_.$JObject$Type$(); + + @jni$_.internal + @core$_.override + jni$_.JType get nullableType => this; + + @jni$_.internal + @core$_.override + final superCount = 1; + + @core$_.override + int get hashCode => ($SuspendInterface$NullableType$).hashCode; + + @core$_.override + bool operator ==(Object other) { + return other.runtimeType == ($SuspendInterface$NullableType$) && + other is $SuspendInterface$NullableType$; + } +} + +final class $SuspendInterface$Type$ extends jni$_.JType { + @jni$_.internal + const $SuspendInterface$Type$(); + + @jni$_.internal + @core$_.override + String get signature => r'Lcom/github/dart_lang/jnigen/SuspendInterface;'; + + @jni$_.internal + @core$_.override + SuspendInterface fromReference(jni$_.JReference reference) => + SuspendInterface.fromReference( + reference, + ); + @jni$_.internal + @core$_.override + jni$_.JType get superType => const jni$_.$JObject$Type$(); + + @jni$_.internal + @core$_.override + jni$_.JType get nullableType => + const $SuspendInterface$NullableType$(); + + @jni$_.internal + @core$_.override + final superCount = 1; + + @core$_.override + int get hashCode => ($SuspendInterface$Type$).hashCode; + + @core$_.override + bool operator ==(Object other) { + return other.runtimeType == ($SuspendInterface$Type$) && + other is $SuspendInterface$Type$; + } +} + final _TopLevelKtClass = jni$_.JClass.forName(r'com/github/dart_lang/jnigen/TopLevelKt'); diff --git a/pkgs/jnigen/test/kotlin_test/kotlin/src/main/kotlin/com/github/dart_lang/jnigen/SuspendFun.kt b/pkgs/jnigen/test/kotlin_test/kotlin/src/main/kotlin/com/github/dart_lang/jnigen/SuspendFun.kt index cd3a07cf8b..99081be116 100644 --- a/pkgs/jnigen/test/kotlin_test/kotlin/src/main/kotlin/com/github/dart_lang/jnigen/SuspendFun.kt +++ b/pkgs/jnigen/test/kotlin_test/kotlin/src/main/kotlin/com/github/dart_lang/jnigen/SuspendFun.kt @@ -47,3 +47,12 @@ public class SuspendFun { return "Hello!" } } + +public interface SuspendInterface { + suspend fun sayHello(): String + suspend fun sayHello(name: String): String + suspend fun nullableHello(returnNull: Boolean): String? + suspend fun sayInt(): Integer + suspend fun sayInt(value: Integer): Integer + suspend fun nullableInt(returnNull: Boolean): Integer? +} diff --git a/pkgs/jnigen/test/kotlin_test/runtime_test_registrant.dart b/pkgs/jnigen/test/kotlin_test/runtime_test_registrant.dart index 4e41f1a542..6fb92c769d 100644 --- a/pkgs/jnigen/test/kotlin_test/runtime_test_registrant.dart +++ b/pkgs/jnigen/test/kotlin_test/runtime_test_registrant.dart @@ -37,6 +37,75 @@ void registerTests(String groupName, TestRunnerCallback test) { }); }); + group('Interface with suspend functions', () { + test('return immediately', () async { + await using((arena) async { + final itf = SuspendInterface.implement($SuspendInterface( + sayHello: () async => JString.fromString("Hello"), + sayHello$1: (JString name) async => + JString.fromString("Hello ${name.toDartString()}"), + nullableHello: (bool returnNull) async => + returnNull ? null : JString.fromString("Hello"), + sayInt: () async => JInteger(123), + sayInt$1: (JInteger value) async => JInteger(10 * value.intValue()), + nullableInt: (bool returnNull) async => + returnNull ? null : JInteger(123), + )); + expect((await itf.sayHello()).toDartString(), "Hello"); + expect( + (await itf.sayHello$1(JString.fromString("Bob"))).toDartString(), + "Hello Bob"); + expect((await itf.nullableHello(false))?.toDartString(), "Hello"); + expect(await itf.nullableHello(true), null); + expect((await itf.sayInt()).intValue(), 123); + expect((await itf.sayInt$1(JInteger(456))).intValue(), 4560); + expect((await itf.nullableInt(false))?.intValue(), 123); + expect(await itf.nullableInt(true), null); + }); + }); + + test('return delayed', () async { + await using((arena) async { + final itf = SuspendInterface.implement($SuspendInterface( + sayHello: () async { + await Future.delayed(Duration(milliseconds: 100)); + return JString.fromString("Hello"); + }, + sayHello$1: (JString name) async { + await Future.delayed(Duration(milliseconds: 100)); + return JString.fromString("Hello ${name.toDartString()}"); + }, + nullableHello: (bool returnNull) async { + await Future.delayed(Duration(milliseconds: 100)); + return returnNull ? null : JString.fromString("Hello"); + }, + sayInt: () async { + await Future.delayed(Duration(milliseconds: 100)); + return JInteger(123); + }, + sayInt$1: (JInteger value) async { + await Future.delayed(Duration(milliseconds: 100)); + return JInteger(10 * value.intValue()); + }, + nullableInt: (bool returnNull) async { + await Future.delayed(Duration(milliseconds: 100)); + return returnNull ? null : JInteger(123); + }, + )); + expect((await itf.sayHello()).toDartString(), "Hello"); + expect( + (await itf.sayHello$1(JString.fromString("Bob"))).toDartString(), + "Hello Bob"); + expect((await itf.nullableHello(false))?.toDartString(), "Hello"); + expect(await itf.nullableHello(true), null); + expect((await itf.sayInt()).intValue(), 123); + expect((await itf.sayInt$1(JInteger(456))).intValue(), 4560); + expect((await itf.nullableInt(false))?.intValue(), 123); + expect(await itf.nullableInt(true), null); + }); + }); + }); + test('Top levels', () { expect(topLevel(), 42); expect(topLevel$1(), 42); 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 e41c179208..4a067dc26f 100644 --- a/pkgs/jnigen/test/simple_package_test/bindings/simple_package.dart +++ b/pkgs/jnigen/test/simple_package_test/bindings/simple_package.dart @@ -1,4 +1,4 @@ -// AUTO GENERATED BY JNIGEN 0.15.0. DO NOT EDIT! +// AUTO GENERATED BY JNIGEN 0.15.1. DO NOT EDIT! // Copyright (c) 2022, the Dart project authors. Please see the AUTHORS file // for details. All rights reserved. Use of this source code is governed by a From a4b3975d3fa4dfa7014203963929208a9c85af92 Mon Sep 17 00:00:00 2001 From: Liam Appelbe Date: Thu, 18 Dec 2025 11:49:54 +1100 Subject: [PATCH 05/10] Handle errors --- pkgs/jni/lib/src/kotlin.dart | 40 +--- .../kotlin_test/runtime_test_registrant.dart | 202 ++++++++++++------ 2 files changed, 143 insertions(+), 99 deletions(-) diff --git a/pkgs/jni/lib/src/kotlin.dart b/pkgs/jni/lib/src/kotlin.dart index 20f4359e4f..3ffc339746 100644 --- a/pkgs/jni/lib/src/kotlin.dart +++ b/pkgs/jni/lib/src/kotlin.dart @@ -52,37 +52,17 @@ class KotlinContinuation extends JObject { _resumeWithId(this, const jvoidType(), [result]); } - JObject resumeWithFuture(Future future) { - future.then(resumeWith, onError: (error) { - // TODO - }); - return _coroutineSuspended; + static final _result$FailureConstructor = + result$FailureClass.constructorId(r'(Ljava/lang/Throwable;)V'); + void resumeWithException(Object dartException, StackTrace stackTrace) { + resumeWith(_result$FailureConstructor( + result$FailureClass, JObject.type, [ + ProtectedJniExtensions.newDartException('$dartException\n$stackTrace'), + ])); } -} - -class _Result extends JObject { - _Result.fromReference( - super.reference, - ) : super.fromReference(); - static final _class = JClass.forName(r'kotlin/Result'); - - static final _id_new$ = _class.constructorId( - r'(Ljava/lang/Object;)V', - ); - - static final _new$ = ProtectedJniExtensions.lookup< - NativeFunction< - JniResult Function( - Pointer, - JMethodIDPtr, - VarArgs<(Pointer,)>)>>( - 'globalEnv_NewObject') - .asFunction< - JniResult Function(Pointer, - JMethodIDPtr, Pointer)>(); - - factory _Result(JObject object) { - return _Result.fromReference(_id_new$(_class, referenceType, [object])); + JObject resumeWithFuture(Future future) { + future.then(resumeWith, onError: resumeWithException); + return _coroutineSuspended; } } diff --git a/pkgs/jnigen/test/kotlin_test/runtime_test_registrant.dart b/pkgs/jnigen/test/kotlin_test/runtime_test_registrant.dart index 6fb92c769d..ceee4c701f 100644 --- a/pkgs/jnigen/test/kotlin_test/runtime_test_registrant.dart +++ b/pkgs/jnigen/test/kotlin_test/runtime_test_registrant.dart @@ -37,75 +37,6 @@ void registerTests(String groupName, TestRunnerCallback test) { }); }); - group('Interface with suspend functions', () { - test('return immediately', () async { - await using((arena) async { - final itf = SuspendInterface.implement($SuspendInterface( - sayHello: () async => JString.fromString("Hello"), - sayHello$1: (JString name) async => - JString.fromString("Hello ${name.toDartString()}"), - nullableHello: (bool returnNull) async => - returnNull ? null : JString.fromString("Hello"), - sayInt: () async => JInteger(123), - sayInt$1: (JInteger value) async => JInteger(10 * value.intValue()), - nullableInt: (bool returnNull) async => - returnNull ? null : JInteger(123), - )); - expect((await itf.sayHello()).toDartString(), "Hello"); - expect( - (await itf.sayHello$1(JString.fromString("Bob"))).toDartString(), - "Hello Bob"); - expect((await itf.nullableHello(false))?.toDartString(), "Hello"); - expect(await itf.nullableHello(true), null); - expect((await itf.sayInt()).intValue(), 123); - expect((await itf.sayInt$1(JInteger(456))).intValue(), 4560); - expect((await itf.nullableInt(false))?.intValue(), 123); - expect(await itf.nullableInt(true), null); - }); - }); - - test('return delayed', () async { - await using((arena) async { - final itf = SuspendInterface.implement($SuspendInterface( - sayHello: () async { - await Future.delayed(Duration(milliseconds: 100)); - return JString.fromString("Hello"); - }, - sayHello$1: (JString name) async { - await Future.delayed(Duration(milliseconds: 100)); - return JString.fromString("Hello ${name.toDartString()}"); - }, - nullableHello: (bool returnNull) async { - await Future.delayed(Duration(milliseconds: 100)); - return returnNull ? null : JString.fromString("Hello"); - }, - sayInt: () async { - await Future.delayed(Duration(milliseconds: 100)); - return JInteger(123); - }, - sayInt$1: (JInteger value) async { - await Future.delayed(Duration(milliseconds: 100)); - return JInteger(10 * value.intValue()); - }, - nullableInt: (bool returnNull) async { - await Future.delayed(Duration(milliseconds: 100)); - return returnNull ? null : JInteger(123); - }, - )); - expect((await itf.sayHello()).toDartString(), "Hello"); - expect( - (await itf.sayHello$1(JString.fromString("Bob"))).toDartString(), - "Hello Bob"); - expect((await itf.nullableHello(false))?.toDartString(), "Hello"); - expect(await itf.nullableHello(true), null); - expect((await itf.sayInt()).intValue(), 123); - expect((await itf.sayInt$1(JInteger(456))).intValue(), 4560); - expect((await itf.nullableInt(false))?.intValue(), 123); - expect(await itf.nullableInt(true), null); - }); - }); - }); - test('Top levels', () { expect(topLevel(), 42); expect(topLevel$1(), 42); @@ -424,5 +355,138 @@ void registerTests(String groupName, TestRunnerCallback test) { }); }); }); + + group('Interface with suspend functions', () { + test('return immediately', () async { + await using((arena) async { + final itf = SuspendInterface.implement($SuspendInterface( + sayHello: () async => JString.fromString('Hello'), + sayHello$1: (JString name) async => + JString.fromString('Hello ${name.toDartString()}'), + nullableHello: (bool returnNull) async => + returnNull ? null : JString.fromString('Hello'), + sayInt: () async => JInteger(123), + sayInt$1: (JInteger value) async => JInteger(10 * value.intValue()), + nullableInt: (bool returnNull) async => + returnNull ? null : JInteger(123), + )); + expect((await itf.sayHello()).toDartString(), 'Hello'); + expect( + (await itf.sayHello$1(JString.fromString('Bob'))).toDartString(), + 'Hello Bob'); + expect((await itf.nullableHello(false))?.toDartString(), 'Hello'); + expect(await itf.nullableHello(true), null); + expect((await itf.sayInt()).intValue(), 123); + expect((await itf.sayInt$1(JInteger(456))).intValue(), 4560); + expect((await itf.nullableInt(false))?.intValue(), 123); + expect(await itf.nullableInt(true), null); + }); + }); + + test('return delayed', () async { + await using((arena) async { + final itf = SuspendInterface.implement($SuspendInterface( + sayHello: () async { + await Future.delayed(const Duration(milliseconds: 100)); + return JString.fromString('Hello'); + }, + sayHello$1: (JString name) async { + await Future.delayed(const Duration(milliseconds: 100)); + return JString.fromString('Hello ${name.toDartString()}'); + }, + nullableHello: (bool returnNull) async { + await Future.delayed(const Duration(milliseconds: 100)); + return returnNull ? null : JString.fromString('Hello'); + }, + sayInt: () async { + await Future.delayed(const Duration(milliseconds: 100)); + return JInteger(123); + }, + sayInt$1: (JInteger value) async { + await Future.delayed(const Duration(milliseconds: 100)); + return JInteger(10 * value.intValue()); + }, + nullableInt: (bool returnNull) async { + await Future.delayed(const Duration(milliseconds: 100)); + return returnNull ? null : JInteger(123); + }, + )); + expect((await itf.sayHello()).toDartString(), 'Hello'); + expect( + (await itf.sayHello$1(JString.fromString('Bob'))).toDartString(), + 'Hello Bob'); + expect((await itf.nullableHello(false))?.toDartString(), 'Hello'); + expect(await itf.nullableHello(true), null); + expect((await itf.sayInt()).intValue(), 123); + expect((await itf.sayInt$1(JInteger(456))).intValue(), 4560); + expect((await itf.nullableInt(false))?.intValue(), 123); + expect(await itf.nullableInt(true), null); + }); + }); + + test('throw immediately', () async { + await using((arena) async { + final itf = SuspendInterface.implement($SuspendInterface( + sayHello: () async => throw Exception(), + sayHello$1: (JString name) async => throw Exception(), + nullableHello: (bool returnNull) async => throw Exception(), + sayInt: () async => throw Exception(), + sayInt$1: (JInteger value) async => throw Exception(), + nullableInt: (bool returnNull) async => throw Exception(), + )); + await expectLater(itf.sayHello(), throwsA(isA())); + await expectLater(itf.sayHello$1(JString.fromString('Bob')), + throwsA(isA())); + await expectLater( + itf.nullableHello(false), throwsA(isA())); + await expectLater(itf.sayInt(), throwsA(isA())); + await expectLater( + itf.sayInt$1(JInteger(456)), throwsA(isA())); + await expectLater( + itf.nullableInt(false), throwsA(isA())); + }); + }); + + test('throw delayed', () async { + await using((arena) async { + final itf = SuspendInterface.implement($SuspendInterface( + sayHello: () async { + await Future.delayed(const Duration(milliseconds: 100)); + throw Exception(); + }, + sayHello$1: (JString name) async { + await Future.delayed(const Duration(milliseconds: 100)); + throw Exception(); + }, + nullableHello: (bool returnNull) async { + await Future.delayed(const Duration(milliseconds: 100)); + throw Exception(); + }, + sayInt: () async { + await Future.delayed(const Duration(milliseconds: 100)); + throw Exception(); + }, + sayInt$1: (JInteger value) async { + await Future.delayed(const Duration(milliseconds: 100)); + throw Exception(); + }, + nullableInt: (bool returnNull) async { + await Future.delayed(const Duration(milliseconds: 100)); + throw Exception(); + }, + )); + await expectLater(itf.sayHello(), throwsA(isA())); + await expectLater(itf.sayHello$1(JString.fromString('Bob')), + throwsA(isA())); + await expectLater( + itf.nullableHello(false), throwsA(isA())); + await expectLater(itf.sayInt(), throwsA(isA())); + await expectLater( + itf.sayInt$1(JInteger(456)), throwsA(isA())); + await expectLater( + itf.nullableInt(false), throwsA(isA())); + }); + }); + }); }); } From 58703c6798072cafeea5d432a2e5ae497bc5947e Mon Sep 17 00:00:00 2001 From: Liam Appelbe Date: Thu, 18 Dec 2025 11:51:39 +1100 Subject: [PATCH 06/10] Revert example --- .../android/src/main/kotlin/Example.kt | 11 +- .../kotlin_plugin/example/lib/main.dart | 15 +- pkgs/jnigen/example/kotlin_plugin/jnigen.yaml | 1 - .../kotlin_plugin/lib/kotlin_bindings.dart | 282 +----------------- 4 files changed, 18 insertions(+), 291 deletions(-) diff --git a/pkgs/jnigen/example/kotlin_plugin/android/src/main/kotlin/Example.kt b/pkgs/jnigen/example/kotlin_plugin/android/src/main/kotlin/Example.kt index 7be9fcd552..531f899b45 100644 --- a/pkgs/jnigen/example/kotlin_plugin/android/src/main/kotlin/Example.kt +++ b/pkgs/jnigen/example/kotlin_plugin/android/src/main/kotlin/Example.kt @@ -1,17 +1,10 @@ import androidx.annotation.Keep -import kotlin.* -import kotlin.coroutines.* import kotlinx.coroutines.* -@Keep -interface Thinker { - public suspend fun message(): String -} - @Keep class Example { - public suspend fun thinkBeforeAnswering(thinker: Thinker): String { + public suspend fun thinkBeforeAnswering(): String { delay(1000L) - return "Kotlin[" + thinker.message() + ']' + return "42" } } diff --git a/pkgs/jnigen/example/kotlin_plugin/example/lib/main.dart b/pkgs/jnigen/example/kotlin_plugin/example/lib/main.dart index 32aaa6f6d6..84a91337c4 100644 --- a/pkgs/jnigen/example/kotlin_plugin/example/lib/main.dart +++ b/pkgs/jnigen/example/kotlin_plugin/example/lib/main.dart @@ -1,6 +1,4 @@ import 'package:flutter/material.dart'; -import 'package:jni/_internal.dart'; -import 'package:jni/jni.dart'; import 'package:kotlin_plugin/kotlin_plugin.dart'; void main() { @@ -64,17 +62,8 @@ class _MyHomePageState extends State { ElevatedButton( onPressed: () { setState(() { - answer = () async { - final _thinker = $Thinker(message: () async { - await Future.delayed(Duration(milliseconds: 300)); - return JString.fromString("App"); - }); - final thinker = Thinker.implement(_thinker); - final value1 = (await example.thinkBeforeAnswering(thinker)) - .toDartString(releaseOriginal: true); - final value2 = (await thinker.message()).toDartString(); - return value1 + '\n' + value2; - }(); + answer = example.thinkBeforeAnswering().then( + (value) => value.toDartString(releaseOriginal: true)); }); }, child: const Text('Think...'), diff --git a/pkgs/jnigen/example/kotlin_plugin/jnigen.yaml b/pkgs/jnigen/example/kotlin_plugin/jnigen.yaml index f8633f259d..e6f08ecc68 100644 --- a/pkgs/jnigen/example/kotlin_plugin/jnigen.yaml +++ b/pkgs/jnigen/example/kotlin_plugin/jnigen.yaml @@ -14,4 +14,3 @@ log_level: all classes: - 'Example' - - 'Thinker' diff --git a/pkgs/jnigen/example/kotlin_plugin/lib/kotlin_bindings.dart b/pkgs/jnigen/example/kotlin_plugin/lib/kotlin_bindings.dart index cc097efce9..e8531aec76 100644 --- a/pkgs/jnigen/example/kotlin_plugin/lib/kotlin_bindings.dart +++ b/pkgs/jnigen/example/kotlin_plugin/lib/kotlin_bindings.dart @@ -81,50 +81,39 @@ class Example extends jni$_.JObject { static final _id_thinkBeforeAnswering = _class.instanceMethodId( r'thinkBeforeAnswering', - r'(LThinker;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;', + r'(Lkotlin/coroutines/Continuation;)Ljava/lang/Object;', ); static final _thinkBeforeAnswering = jni$_.ProtectedJniExtensions.lookup< - jni$_.NativeFunction< - jni$_.JniResult Function( - jni$_.Pointer, - jni$_.JMethodIDPtr, - jni$_.VarArgs< - ( - jni$_.Pointer, - jni$_.Pointer - )>)>>('globalEnv_CallObjectMethod') + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer,)>)>>( + 'globalEnv_CallObjectMethod') .asFunction< - jni$_.JniResult Function( - jni$_.Pointer, - jni$_.JMethodIDPtr, - jni$_.Pointer, - jni$_.Pointer)>(); + jni$_.JniResult Function(jni$_.Pointer, + jni$_.JMethodIDPtr, jni$_.Pointer)>(); - /// from: `public suspend fun thinkBeforeAnswering(thinker: Thinker): kotlin.String` + /// from: `public suspend fun thinkBeforeAnswering(): kotlin.String` /// The returned object must be released after use, by calling the [release] method. - core$_.Future thinkBeforeAnswering( - Thinker thinker, - ) async { + core$_.Future thinkBeforeAnswering() async { final $p = jni$_.ReceivePort(); final _$continuation = jni$_.ProtectedJniExtensions.newPortContinuation($p); - final _$thinker = thinker.reference; + final $r = _thinkBeforeAnswering( reference.pointer, _id_thinkBeforeAnswering as jni$_.JMethodIDPtr, - _$thinker.pointer, _$continuation.pointer) .object(const jni$_.$JObject$Type$()); _$continuation.release(); - jni$_.JObject $o; + final jni$_.JObject $o; if ($r.isInstanceOf(jni$_.coroutineSingletonsClass)) { $r.release(); final $a = await $p.first; $o = jni$_.JObject.fromReference( jni$_.JGlobalReference(jni$_.JObjectPtr.fromAddress($a))); - if ($o.isInstanceOf(jni$_.result$Class)) { - $o = jni$_.resultValueField.get($o, const jni$_.$JObject$Type$()); - } else if ($o.isInstanceOf(jni$_.result$FailureClass)) { + if ($o.isInstanceOf(jni$_.result$FailureClass)) { final $e = jni$_.failureExceptionField.get($o, const jni$_.$JObject$Type$()); $o.release(); @@ -210,246 +199,3 @@ final class $Example$Type$ extends jni$_.JType { return other.runtimeType == ($Example$Type$) && other is $Example$Type$; } } - -/// from: `Thinker` -class Thinker extends jni$_.JObject { - @jni$_.internal - @core$_.override - final jni$_.JType $type; - - @jni$_.internal - Thinker.fromReference( - jni$_.JReference reference, - ) : $type = type, - super.fromReference(reference); - - static final _class = jni$_.JClass.forName(r'Thinker'); - - /// The type which includes information such as the signature of this class. - static const jni$_.JType nullableType = $Thinker$NullableType$(); - - /// The type which includes information such as the signature of this class. - static const jni$_.JType type = $Thinker$Type$(); - static final _id_message = _class.instanceMethodId( - r'message', - r'(Lkotlin/coroutines/Continuation;)Ljava/lang/Object;', - ); - - static final _message = jni$_.ProtectedJniExtensions.lookup< - jni$_.NativeFunction< - jni$_.JniResult Function( - jni$_.Pointer, - jni$_.JMethodIDPtr, - jni$_.VarArgs<(jni$_.Pointer,)>)>>( - 'globalEnv_CallObjectMethod') - .asFunction< - jni$_.JniResult Function(jni$_.Pointer, - jni$_.JMethodIDPtr, jni$_.Pointer)>(); - - /// from: `public suspend fun message(): kotlin.String` - /// The returned object must be released after use, by calling the [release] method. - core$_.Future message() async { - final $p = jni$_.ReceivePort(); - final _$continuation = jni$_.ProtectedJniExtensions.newPortContinuation($p); - - final $r = _message(reference.pointer, _id_message as jni$_.JMethodIDPtr, - _$continuation.pointer) - .object(const jni$_.$JObject$Type$()); - _$continuation.release(); - jni$_.JObject $o; - if ($r.isInstanceOf(jni$_.coroutineSingletonsClass)) { - $r.release(); - final $a = await $p.first; - $o = jni$_.JObject.fromReference( - jni$_.JGlobalReference(jni$_.JObjectPtr.fromAddress($a))); - if ($o.isInstanceOf(jni$_.result$Class)) { - $o = jni$_.resultValueField.get($o, const jni$_.$JObject$Type$()); - } else if ($o.isInstanceOf(jni$_.result$FailureClass)) { - final $e = - jni$_.failureExceptionField.get($o, const jni$_.$JObject$Type$()); - $o.release(); - jni$_.Jni.throwException($e.reference.toPointer()); - } - } else { - $o = $r; - } - return $o.as( - const jni$_.$JString$Type$(), - releaseOriginal: true, - ); - } - - /// Maps a specific port to the implemented interface. - static final core$_.Map _$impls = {}; - static jni$_.JObjectPtr _$invoke( - int port, - jni$_.JObjectPtr descriptor, - jni$_.JObjectPtr args, - ) { - return _$invokeMethod( - port, - jni$_.MethodInvocation.fromAddresses( - 0, - descriptor.address, - args.address, - ), - ); - } - - static final jni$_.Pointer< - jni$_.NativeFunction< - jni$_.JObjectPtr Function( - jni$_.Int64, jni$_.JObjectPtr, jni$_.JObjectPtr)>> - _$invokePointer = jni$_.Pointer.fromFunction(_$invoke); - - static jni$_.Pointer _$invokeMethod( - int $p, - jni$_.MethodInvocation $i, - ) { - try { - final $d = $i.methodDescriptor.toDartString(releaseOriginal: true); - final $a = $i.args; - if ($d == - r'message(Lkotlin/coroutines/Continuation;)Ljava/lang/Object;') { - final $r = _$impls[$p]!.message( - $a![0]!.as(const jni$_.$JObject$Type$(), releaseOriginal: true), - ); - return ($r as jni$_.JObject?) - ?.as(const jni$_.$JObject$Type$()) - .reference - .toPointer() ?? - jni$_.nullptr; - } - } catch (e) { - return jni$_.ProtectedJniExtensions.newDartException(e); - } - return jni$_.nullptr; - } - - static void implementIn( - jni$_.JImplementer implementer, - $Thinker $impl, - ) { - late final jni$_.RawReceivePort $p; - $p = jni$_.RawReceivePort(($m) { - if ($m == null) { - _$impls.remove($p.sendPort.nativePort); - $p.close(); - return; - } - final $i = jni$_.MethodInvocation.fromMessage($m); - final $r = _$invokeMethod($p.sendPort.nativePort, $i); - jni$_.ProtectedJniExtensions.returnResult($i.result, $r); - }); - implementer.add( - r'Thinker', - $p, - _$invokePointer, - [], - ); - final $a = $p.sendPort.nativePort; - _$impls[$a] = $impl; - } - - factory Thinker.implement( - $Thinker $impl, - ) { - final $i = jni$_.JImplementer(); - implementIn($i, $impl); - return Thinker.fromReference( - $i.implementReference(), - ); - } -} - -abstract base mixin class $Thinker { - factory $Thinker({ - required core$_.Future Function() message, - }) = _$Thinker; - - jni$_.JObject message(jni$_.JObject continuation); -} - -final class _$Thinker with $Thinker { - _$Thinker({ - required core$_.Future Function() message, - }) : _message = message; - - final core$_.Future Function() _message; - - jni$_.JObject message(jni$_.JObject continuation) { - return jni$_.KotlinContinuation.fromReference(continuation.reference) - .resumeWithFuture(_message()); - } -} - -final class $Thinker$NullableType$ extends jni$_.JType { - @jni$_.internal - const $Thinker$NullableType$(); - - @jni$_.internal - @core$_.override - String get signature => r'LThinker;'; - - @jni$_.internal - @core$_.override - Thinker? fromReference(jni$_.JReference reference) => reference.isNull - ? null - : Thinker.fromReference( - reference, - ); - @jni$_.internal - @core$_.override - jni$_.JType get superType => const jni$_.$JObject$Type$(); - - @jni$_.internal - @core$_.override - jni$_.JType get nullableType => this; - - @jni$_.internal - @core$_.override - final superCount = 1; - - @core$_.override - int get hashCode => ($Thinker$NullableType$).hashCode; - - @core$_.override - bool operator ==(Object other) { - return other.runtimeType == ($Thinker$NullableType$) && - other is $Thinker$NullableType$; - } -} - -final class $Thinker$Type$ extends jni$_.JType { - @jni$_.internal - const $Thinker$Type$(); - - @jni$_.internal - @core$_.override - String get signature => r'LThinker;'; - - @jni$_.internal - @core$_.override - Thinker fromReference(jni$_.JReference reference) => Thinker.fromReference( - reference, - ); - @jni$_.internal - @core$_.override - jni$_.JType get superType => const jni$_.$JObject$Type$(); - - @jni$_.internal - @core$_.override - jni$_.JType get nullableType => const $Thinker$NullableType$(); - - @jni$_.internal - @core$_.override - final superCount = 1; - - @core$_.override - int get hashCode => ($Thinker$Type$).hashCode; - - @core$_.override - bool operator ==(Object other) { - return other.runtimeType == ($Thinker$Type$) && other is $Thinker$Type$; - } -} From 85e10363e665f8e4b846b5a37bbee79fa72c4ea6 Mon Sep 17 00:00:00 2001 From: Liam Appelbe Date: Thu, 18 Dec 2025 14:11:43 +1100 Subject: [PATCH 07/10] Kotlin invocation tests --- .../test/kotlin_test/bindings/kotlin.dart | 117 ++++++++++++++++++ .../com/github/dart_lang/jnigen/SuspendFun.kt | 19 ++- .../kotlin_test/runtime_test_registrant.dart | 60 +++++++++ 3 files changed, 195 insertions(+), 1 deletion(-) diff --git a/pkgs/jnigen/test/kotlin_test/bindings/kotlin.dart b/pkgs/jnigen/test/kotlin_test/bindings/kotlin.dart index 001e864aa5..9293b57928 100644 --- a/pkgs/jnigen/test/kotlin_test/bindings/kotlin.dart +++ b/pkgs/jnigen/test/kotlin_test/bindings/kotlin.dart @@ -3581,6 +3581,123 @@ final class $SuspendFun$Type$ extends jni$_.JType { } } +final _SuspendFunKtClass = + jni$_.JClass.forName(r'com/github/dart_lang/jnigen/SuspendFunKt'); + +final _id_consumeOnAnotherThread = _SuspendFunKtClass.staticMethodId( + r'consumeOnAnotherThread', + r'(Lcom/github/dart_lang/jnigen/SuspendInterface;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;', +); + +final _consumeOnAnotherThread = jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs< + ( + jni$_.Pointer, + jni$_.Pointer + )>)>>('globalEnv_CallStaticObjectMethod') + .asFunction< + jni$_.JniResult Function(jni$_.Pointer, jni$_.JMethodIDPtr, + jni$_.Pointer, jni$_.Pointer)>(); + +/// from: `public suspend fun consumeOnAnotherThread(itf: com.github.dart_lang.jnigen.SuspendInterface): kotlin.String` +/// The returned object must be released after use, by calling the [release] method. +core$_.Future consumeOnAnotherThread( + SuspendInterface suspendInterface, +) async { + final $p = jni$_.ReceivePort(); + final _$continuation = jni$_.ProtectedJniExtensions.newPortContinuation($p); + final _$suspendInterface = suspendInterface.reference; + final $r = _consumeOnAnotherThread( + _SuspendFunKtClass.reference.pointer, + _id_consumeOnAnotherThread as jni$_.JMethodIDPtr, + _$suspendInterface.pointer, + _$continuation.pointer) + .object(const jni$_.$JObject$Type$()); + _$continuation.release(); + jni$_.JObject $o; + if ($r.isInstanceOf(jni$_.coroutineSingletonsClass)) { + $r.release(); + final $a = await $p.first; + $o = jni$_.JObject.fromReference( + jni$_.JGlobalReference(jni$_.JObjectPtr.fromAddress($a))); + if ($o.isInstanceOf(jni$_.result$Class)) { + $o = jni$_.resultValueField.get($o, const jni$_.$JObject$Type$()); + } else if ($o.isInstanceOf(jni$_.result$FailureClass)) { + final $e = + jni$_.failureExceptionField.get($o, const jni$_.$JObject$Type$()); + $o.release(); + jni$_.Jni.throwException($e.reference.toPointer()); + } + } else { + $o = $r; + } + return $o.as( + const jni$_.$JString$Type$(), + releaseOriginal: true, + ); +} + +final _id_consumeOnSameThread = _SuspendFunKtClass.staticMethodId( + r'consumeOnSameThread', + r'(Lcom/github/dart_lang/jnigen/SuspendInterface;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;', +); + +final _consumeOnSameThread = jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs< + ( + jni$_.Pointer, + jni$_.Pointer + )>)>>('globalEnv_CallStaticObjectMethod') + .asFunction< + jni$_.JniResult Function(jni$_.Pointer, jni$_.JMethodIDPtr, + jni$_.Pointer, jni$_.Pointer)>(); + +/// from: `public suspend fun consumeOnSameThread(itf: com.github.dart_lang.jnigen.SuspendInterface): kotlin.String` +/// The returned object must be released after use, by calling the [release] method. +core$_.Future consumeOnSameThread( + SuspendInterface suspendInterface, +) async { + final $p = jni$_.ReceivePort(); + final _$continuation = jni$_.ProtectedJniExtensions.newPortContinuation($p); + final _$suspendInterface = suspendInterface.reference; + final $r = _consumeOnSameThread( + _SuspendFunKtClass.reference.pointer, + _id_consumeOnSameThread as jni$_.JMethodIDPtr, + _$suspendInterface.pointer, + _$continuation.pointer) + .object(const jni$_.$JObject$Type$()); + _$continuation.release(); + jni$_.JObject $o; + if ($r.isInstanceOf(jni$_.coroutineSingletonsClass)) { + $r.release(); + final $a = await $p.first; + $o = jni$_.JObject.fromReference( + jni$_.JGlobalReference(jni$_.JObjectPtr.fromAddress($a))); + if ($o.isInstanceOf(jni$_.result$Class)) { + $o = jni$_.resultValueField.get($o, const jni$_.$JObject$Type$()); + } else if ($o.isInstanceOf(jni$_.result$FailureClass)) { + final $e = + jni$_.failureExceptionField.get($o, const jni$_.$JObject$Type$()); + $o.release(); + jni$_.Jni.throwException($e.reference.toPointer()); + } + } else { + $o = $r; + } + return $o.as( + const jni$_.$JString$Type$(), + releaseOriginal: true, + ); +} + /// from: `com.github.dart_lang.jnigen.SuspendInterface` class SuspendInterface extends jni$_.JObject { @jni$_.internal diff --git a/pkgs/jnigen/test/kotlin_test/kotlin/src/main/kotlin/com/github/dart_lang/jnigen/SuspendFun.kt b/pkgs/jnigen/test/kotlin_test/kotlin/src/main/kotlin/com/github/dart_lang/jnigen/SuspendFun.kt index 99081be116..1247c90bb6 100644 --- a/pkgs/jnigen/test/kotlin_test/kotlin/src/main/kotlin/com/github/dart_lang/jnigen/SuspendFun.kt +++ b/pkgs/jnigen/test/kotlin_test/kotlin/src/main/kotlin/com/github/dart_lang/jnigen/SuspendFun.kt @@ -5,7 +5,7 @@ package com.github.dart_lang.jnigen -import kotlinx.coroutines.delay +import kotlinx.coroutines.* import kotlin.coroutines.Continuation public class SuspendFun { @@ -56,3 +56,20 @@ public interface SuspendInterface { suspend fun sayInt(value: Integer): Integer suspend fun nullableInt(returnNull: Boolean): Integer? } + +suspend fun consumeOnAnotherThread(itf: SuspendInterface): String { + return withContext(Dispatchers.Default) { + consumeOnSameThread(itf) + } +} + +suspend fun consumeOnSameThread(itf: SuspendInterface): String { + return """ +${itf.sayHello()} +${itf.sayHello("Alice")} +${itf.nullableHello(false)} +${itf.sayInt()} +${itf.sayInt(Integer(789))} +${itf.nullableInt(false)} +""".trim(); +} diff --git a/pkgs/jnigen/test/kotlin_test/runtime_test_registrant.dart b/pkgs/jnigen/test/kotlin_test/runtime_test_registrant.dart index ceee4c701f..e4d07613e4 100644 --- a/pkgs/jnigen/test/kotlin_test/runtime_test_registrant.dart +++ b/pkgs/jnigen/test/kotlin_test/runtime_test_registrant.dart @@ -370,6 +370,7 @@ void registerTests(String groupName, TestRunnerCallback test) { nullableInt: (bool returnNull) async => returnNull ? null : JInteger(123), )); + expect((await itf.sayHello()).toDartString(), 'Hello'); expect( (await itf.sayHello$1(JString.fromString('Bob'))).toDartString(), @@ -380,6 +381,29 @@ void registerTests(String groupName, TestRunnerCallback test) { expect((await itf.sayInt$1(JInteger(456))).intValue(), 4560); expect((await itf.nullableInt(false))?.intValue(), 123); expect(await itf.nullableInt(true), null); + + expect( + (await consumeOnSameThread(itf)).toDartString(), + ''' +Hello +Hello Alice +Hello +123 +7890 +123 +''' + .trim()); + expect( + (await consumeOnAnotherThread(itf)).toDartString(), + ''' +Hello +Hello Alice +Hello +123 +7890 +123 +''' + .trim()); }); }); @@ -411,6 +435,7 @@ void registerTests(String groupName, TestRunnerCallback test) { return returnNull ? null : JInteger(123); }, )); + expect((await itf.sayHello()).toDartString(), 'Hello'); expect( (await itf.sayHello$1(JString.fromString('Bob'))).toDartString(), @@ -421,6 +446,29 @@ void registerTests(String groupName, TestRunnerCallback test) { expect((await itf.sayInt$1(JInteger(456))).intValue(), 4560); expect((await itf.nullableInt(false))?.intValue(), 123); expect(await itf.nullableInt(true), null); + + expect( + (await consumeOnSameThread(itf)).toDartString(), + ''' +Hello +Hello Alice +Hello +123 +7890 +123 +''' + .trim()); + expect( + (await consumeOnAnotherThread(itf)).toDartString(), + ''' +Hello +Hello Alice +Hello +123 +7890 +123 +''' + .trim()); }); }); @@ -434,6 +482,7 @@ void registerTests(String groupName, TestRunnerCallback test) { sayInt$1: (JInteger value) async => throw Exception(), nullableInt: (bool returnNull) async => throw Exception(), )); + await expectLater(itf.sayHello(), throwsA(isA())); await expectLater(itf.sayHello$1(JString.fromString('Bob')), throwsA(isA())); @@ -444,6 +493,11 @@ void registerTests(String groupName, TestRunnerCallback test) { itf.sayInt$1(JInteger(456)), throwsA(isA())); await expectLater( itf.nullableInt(false), throwsA(isA())); + + await expectLater( + consumeOnSameThread(itf), throwsA(isA())); + await expectLater( + consumeOnAnotherThread(itf), throwsA(isA())); }); }); @@ -475,6 +529,7 @@ void registerTests(String groupName, TestRunnerCallback test) { throw Exception(); }, )); + await expectLater(itf.sayHello(), throwsA(isA())); await expectLater(itf.sayHello$1(JString.fromString('Bob')), throwsA(isA())); @@ -485,6 +540,11 @@ void registerTests(String groupName, TestRunnerCallback test) { itf.sayInt$1(JInteger(456)), throwsA(isA())); await expectLater( itf.nullableInt(false), throwsA(isA())); + + await expectLater( + consumeOnSameThread(itf), throwsA(isA())); + await expectLater( + consumeOnAnotherThread(itf), throwsA(isA())); }); }); }); From 9a664afe2e059c4f1169462e125d5a759c3e5eed Mon Sep 17 00:00:00 2001 From: Liam Appelbe Date: Thu, 18 Dec 2025 14:20:45 +1100 Subject: [PATCH 08/10] fmt --- pkgs/jni/lib/_internal.dart | 9 +++++++-- pkgs/jni/lib/src/kotlin.dart | 9 ++++----- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/pkgs/jni/lib/_internal.dart b/pkgs/jni/lib/_internal.dart index c5e8fb6a17..b94d56c255 100644 --- a/pkgs/jni/lib/_internal.dart +++ b/pkgs/jni/lib/_internal.dart @@ -52,8 +52,13 @@ export 'src/jni.dart' show ProtectedJniExtensions; export 'src/jobject.dart' show $JObject$NullableType$, $JObject$Type$; export 'src/jreference.dart'; export 'src/kotlin.dart' - show coroutineSingletonsClass, failureExceptionField, - result$FailureClass, result$Class, resultValueField, KotlinContinuation; + show + coroutineSingletonsClass, + failureExceptionField, + result$FailureClass, + result$Class, + resultValueField, + KotlinContinuation; export 'src/lang/jboolean.dart' show $JBoolean$NullableType$, $JBoolean$Type$; export 'src/lang/jbyte.dart' show $JByte$NullableType$, $JByte$Type$; export 'src/lang/jcharacter.dart' diff --git a/pkgs/jni/lib/src/kotlin.dart b/pkgs/jni/lib/src/kotlin.dart index 3ffc339746..d64ab53501 100644 --- a/pkgs/jni/lib/src/kotlin.dart +++ b/pkgs/jni/lib/src/kotlin.dart @@ -35,7 +35,7 @@ final resultValueField = final _coroutineIntrinsicsClass = JClass.forName('kotlin/coroutines/intrinsics/IntrinsicsKt'); final _coroutineSuspended = _coroutineIntrinsicsClass.staticMethodId( - 'getCOROUTINE_SUSPENDED', '()Ljava/lang/Object;')( + 'getCOROUTINE_SUSPENDED', '()Ljava/lang/Object;')( _coroutineIntrinsicsClass, const $JObject$Type$(), [])!; @internal @@ -55,10 +55,9 @@ class KotlinContinuation extends JObject { static final _result$FailureConstructor = result$FailureClass.constructorId(r'(Ljava/lang/Throwable;)V'); void resumeWithException(Object dartException, StackTrace stackTrace) { - resumeWith(_result$FailureConstructor( - result$FailureClass, JObject.type, [ - ProtectedJniExtensions.newDartException('$dartException\n$stackTrace'), - ])); + resumeWith(_result$FailureConstructor(result$FailureClass, JObject.type, [ + ProtectedJniExtensions.newDartException('$dartException\n$stackTrace'), + ])); } JObject resumeWithFuture(Future future) { From 9bdd0db8358dd36f307c6551e359da8f8b3585bc Mon Sep 17 00:00:00 2001 From: Liam Appelbe Date: Thu, 18 Dec 2025 14:26:46 +1100 Subject: [PATCH 09/10] Fix analysis --- pkgs/jni/lib/_internal.dart | 6 +++--- pkgs/jni/lib/src/kotlin.dart | 8 +------- 2 files changed, 4 insertions(+), 10 deletions(-) diff --git a/pkgs/jni/lib/_internal.dart b/pkgs/jni/lib/_internal.dart index b94d56c255..2e6fcb4a06 100644 --- a/pkgs/jni/lib/_internal.dart +++ b/pkgs/jni/lib/_internal.dart @@ -53,12 +53,12 @@ export 'src/jobject.dart' show $JObject$NullableType$, $JObject$Type$; export 'src/jreference.dart'; export 'src/kotlin.dart' show + KotlinContinuation, coroutineSingletonsClass, failureExceptionField, - result$FailureClass, result$Class, - resultValueField, - KotlinContinuation; + result$FailureClass, + resultValueField; export 'src/lang/jboolean.dart' show $JBoolean$NullableType$, $JBoolean$Type$; export 'src/lang/jbyte.dart' show $JByte$NullableType$, $JByte$Type$; export 'src/lang/jcharacter.dart' diff --git a/pkgs/jni/lib/src/kotlin.dart b/pkgs/jni/lib/src/kotlin.dart index d64ab53501..3c4534e7d8 100644 --- a/pkgs/jni/lib/src/kotlin.dart +++ b/pkgs/jni/lib/src/kotlin.dart @@ -2,16 +2,10 @@ // for details. All rights reserved. Use of this source code is governed by a // BSD-style license that can be found in the LICENSE file. -import 'dart:ffi'; - import 'package:meta/meta.dart' show internal; import 'jni.dart'; import 'jobject.dart'; -import 'lang/jstring.dart'; -import 'jreference.dart'; -import 'jvalues.dart'; -import 'third_party/generated_bindings.dart'; import 'types.dart'; @internal @@ -36,7 +30,7 @@ final _coroutineIntrinsicsClass = JClass.forName('kotlin/coroutines/intrinsics/IntrinsicsKt'); final _coroutineSuspended = _coroutineIntrinsicsClass.staticMethodId( 'getCOROUTINE_SUSPENDED', '()Ljava/lang/Object;')( - _coroutineIntrinsicsClass, const $JObject$Type$(), [])!; + _coroutineIntrinsicsClass, const $JObject$Type$(), []); @internal class KotlinContinuation extends JObject { From 79cc03357fb6dc0492438ae19551ed1182f5847b Mon Sep 17 00:00:00 2001 From: Liam Appelbe Date: Thu, 18 Dec 2025 14:45:15 +1100 Subject: [PATCH 10/10] changelog, regen bindings --- pkgs/jni/lib/src/plugin/generated_plugin.dart | 2 +- pkgs/jnigen/CHANGELOG.md | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/pkgs/jni/lib/src/plugin/generated_plugin.dart b/pkgs/jni/lib/src/plugin/generated_plugin.dart index 1a843d6266..c88e49b089 100644 --- a/pkgs/jni/lib/src/plugin/generated_plugin.dart +++ b/pkgs/jni/lib/src/plugin/generated_plugin.dart @@ -1,4 +1,4 @@ -// AUTO GENERATED BY JNIGEN 0.15.0. DO NOT EDIT! +// AUTO GENERATED BY JNIGEN 0.15.1. DO NOT EDIT! // Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file // for details. All rights reserved. Use of this source code is governed by a diff --git a/pkgs/jnigen/CHANGELOG.md b/pkgs/jnigen/CHANGELOG.md index 7aeb7b3735..02846012e4 100644 --- a/pkgs/jnigen/CHANGELOG.md +++ b/pkgs/jnigen/CHANGELOG.md @@ -1,6 +1,8 @@ ## 0.15.1-wip - Add docs about debugging. +- Add support for Kotlin interfaces with suspend functions. These can now be + implemented using Dart functions that return a `Future`. ## 0.15.0