From 12bc10a59e62af690cd3bc6e7ea11a6c818123e8 Mon Sep 17 00:00:00 2001 From: Hossein Yousefi Date: Fri, 20 Sep 2024 13:44:06 +0200 Subject: [PATCH] [jnigen] Implement multiple interfaces (#1584) --- pkgs/jni/example/lib/main.dart | 10 +- .../{PortProxy.java => PortProxyBuilder.java} | 59 +++-- pkgs/jni/lib/jni.dart | 3 +- pkgs/jni/lib/src/accessors.dart | 3 +- pkgs/jni/lib/src/jimplementer.dart | 118 +++++++++ pkgs/jni/lib/src/jni.dart | 84 ++----- .../third_party/jni_bindings_generated.dart | 28 +-- pkgs/jni/src/dartjni.c | 54 ++--- pkgs/jni/src/dartjni.h | 7 +- pkgs/jni/test/global_env_test.dart | 3 +- pkgs/jnigen/CHANGELOG.md | 6 + pkgs/jnigen/docs/interface_implementation.md | 145 +++++++++++ .../in_app_java/lib/android_utils.dart | 3 +- .../kotlin_plugin/lib/kotlin_bindings.dart | 3 +- .../lib/notifications.dart | 3 +- .../org/apache/pdfbox/pdmodel/PDDocument.dart | 3 +- .../pdfbox/pdmodel/PDDocumentInformation.dart | 3 +- .../apache/pdfbox/text/PDFTextStripper.dart | 3 +- .../lib/src/bindings/dart_generator.dart | 55 +++-- pkgs/jnigen/lib/src/bindings/renamer.dart | 4 + .../fasterxml/jackson/core/JsonFactory.dart | 3 +- .../fasterxml/jackson/core/JsonParser.dart | 3 +- .../com/fasterxml/jackson/core/JsonToken.dart | 3 +- .../test/kotlin_test/bindings/kotlin.dart | 3 +- pkgs/jnigen/test/renamer_test.dart | 30 +++ .../bindings/simple_package.dart | 225 ++++++++++-------- .../test/simple_package_test/generate.dart | 5 +- .../runtime_test_registrant.dart | 173 ++++++++++---- 28 files changed, 724 insertions(+), 318 deletions(-) rename pkgs/jni/java/src/main/java/com/github/dart_lang/jni/{PortProxy.java => PortProxyBuilder.java} (63%) create mode 100644 pkgs/jni/lib/src/jimplementer.dart create mode 100644 pkgs/jnigen/docs/interface_implementation.md diff --git a/pkgs/jni/example/lib/main.dart b/pkgs/jni/example/lib/main.dart index b406a9d3e..bd6fb00bc 100644 --- a/pkgs/jni/example/lib/main.dart +++ b/pkgs/jni/example/lib/main.dart @@ -7,9 +7,17 @@ import 'dart:ffi'; import 'dart:io'; +import 'package:ffi/ffi.dart'; import 'package:flutter/material.dart'; import 'package:jni/jni.dart'; +extension on String { + /// Returns a Utf-8 encoded `Pointer` with contents same as this string. + Pointer toNativeChars(Allocator allocator) { + return toNativeUtf8(allocator: allocator).cast(); + } +} + // An example of calling JNI methods using low level primitives. // GlobalJniEnv is a thin abstraction over JNIEnv in JNI C API. // @@ -18,7 +26,7 @@ import 'package:jni/jni.dart'; String toJavaStringUsingEnv(int n) => using((arena) { final env = Jni.env; final cls = env.FindClass("java/lang/String".toNativeChars(arena)); - final mId = env.GetStaticMethodID(cls, "valueOf".toNativeChars(), + final mId = env.GetStaticMethodID(cls, "valueOf".toNativeChars(arena), "(I)Ljava/lang/String;".toNativeChars(arena)); final i = arena(); i.ref.i = n; diff --git a/pkgs/jni/java/src/main/java/com/github/dart_lang/jni/PortProxy.java b/pkgs/jni/java/src/main/java/com/github/dart_lang/jni/PortProxyBuilder.java similarity index 63% rename from pkgs/jni/java/src/main/java/com/github/dart_lang/jni/PortProxy.java rename to pkgs/jni/java/src/main/java/com/github/dart_lang/jni/PortProxyBuilder.java index 5218ebcef..f4026189e 100644 --- a/pkgs/jni/java/src/main/java/com/github/dart_lang/jni/PortProxy.java +++ b/pkgs/jni/java/src/main/java/com/github/dart_lang/jni/PortProxyBuilder.java @@ -5,22 +5,32 @@ package com.github.dart_lang.jni; import java.lang.reflect.*; +import java.util.ArrayList; +import java.util.HashMap; -public class PortProxy implements InvocationHandler { +public class PortProxyBuilder implements InvocationHandler { private static final PortCleaner cleaner = new PortCleaner(); static { System.loadLibrary("dartjni"); } - private final long port; + private static final class DartImplementation { + final long port; + final long pointer; + + DartImplementation(long port, long pointer) { + this.port = port; + this.pointer = pointer; + } + } + + private boolean built = false; private final long isolateId; - private final long functionPtr; + private final HashMap implementations = new HashMap<>(); - private PortProxy(long port, long isolateId, long functionPtr) { - this.port = port; + public PortProxyBuilder(long isolateId) { this.isolateId = isolateId; - this.functionPtr = functionPtr; } private static String getDescriptor(Method method) { @@ -62,15 +72,28 @@ private static void appendType(StringBuilder descriptor, Class type) { } } - public static Object newInstance(String binaryName, long port, long isolateId, long functionPtr) - throws ClassNotFoundException { - Class clazz = Class.forName(binaryName); + public void addImplementation(String binaryName, long port, long functionPointer) { + implementations.put(binaryName, new DartImplementation(port, functionPointer)); + } + + public Object build() throws ClassNotFoundException { + if (implementations.isEmpty()) { + throw new IllegalStateException("No interface implementation added"); + } + if (built) { + throw new IllegalStateException("This proxy has already been built"); + } + built = true; + ArrayList> classes = new ArrayList<>(); + for (String binaryName : implementations.keySet()) { + classes.add(Class.forName(binaryName)); + } Object obj = Proxy.newProxyInstance( - clazz.getClassLoader(), - new Class[] {clazz}, - new PortProxy(port, isolateId, functionPtr)); - cleaner.register(obj, port); + classes.get(0).getClassLoader(), classes.toArray(new Class[0]), this); + for (DartImplementation implementation : implementations.values()) { + cleaner.register(obj, implementation.port); + } return obj; } @@ -89,7 +112,15 @@ private static native Object[] _invoke( @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { - Object[] result = _invoke(port, isolateId, functionPtr, proxy, getDescriptor(method), args); + DartImplementation implementation = implementations.get(method.getDeclaringClass().getName()); + Object[] result = + _invoke( + implementation.port, + isolateId, + implementation.pointer, + proxy, + getDescriptor(method), + args); _cleanUp((Long) result[0]); if (result[1] instanceof DartException) { Throwable cause = ((DartException) result[1]).cause; diff --git a/pkgs/jni/lib/jni.dart b/pkgs/jni/lib/jni.dart index 5f7c1e27e..713a3dd6b 100644 --- a/pkgs/jni/lib/jni.dart +++ b/pkgs/jni/lib/jni.dart @@ -65,7 +65,8 @@ export 'dart:ffi' show nullptr; export 'package:ffi/ffi.dart' show Arena, using; export 'src/errors.dart'; -export 'src/jni.dart' hide ProtectedJniExtensions; +export 'src/jimplementer.dart'; +export 'src/jni.dart' hide ProtectedJniExtensions, StringMethodsForJni; export 'src/jobject.dart'; export 'src/jreference.dart' hide ProtectedJReference; export 'src/jvalues.dart'; diff --git a/pkgs/jni/lib/src/accessors.dart b/pkgs/jni/lib/src/accessors.dart index bc414aec1..4629fbb2e 100644 --- a/pkgs/jni/lib/src/accessors.dart +++ b/pkgs/jni/lib/src/accessors.dart @@ -59,7 +59,8 @@ extension JniResultMethods on JniResult { } JReference get reference { - return JGlobalReference(objectPointer); + final pointer = objectPointer; + return pointer == nullptr ? jNullReference : JGlobalReference(pointer); } T object(JObjType type) { diff --git a/pkgs/jni/lib/src/jimplementer.dart b/pkgs/jni/lib/src/jimplementer.dart new file mode 100644 index 000000000..79ba52bbb --- /dev/null +++ b/pkgs/jni/lib/src/jimplementer.dart @@ -0,0 +1,118 @@ +// Copyright (c) 2024, the Dart project authors. Please see the AUTHORS file +// 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 'dart:isolate'; + +import 'package:ffi/ffi.dart'; +import 'package:meta/meta.dart'; + +import '../_internal.dart'; +import 'jobject.dart'; +import 'lang/jstring.dart'; +import 'third_party/generated_bindings.dart'; +import 'types.dart'; + +/// A builder that builds proxy objects that implement one or more interfaces. +/// +/// Example: +/// ```dart +/// final implementer = JImplemeneter(); +/// Foo.implementIn(implementer, fooImpl); +/// Bar.implementIn(implementer, barImpl); +/// final foobar = implementer.build(Foo.type); // Or `Bar.type`. +/// ``` +class JImplementer extends JObject { + JImplementer.fromReference(super.reference) : super.fromReference(); + + static final _class = + JClass.forName(r'com/github/dart_lang/jni/PortProxyBuilder'); + + static final _newId = _class.constructorId(r'(J)V'); + + static final _new = ProtectedJniExtensions.lookup< + NativeFunction< + JniResult Function(Pointer, JMethodIDPtr, + VarArgs<(Int64,)>)>>('globalEnv_NewObject') + .asFunction, JMethodIDPtr, int)>(); + + factory JImplementer() { + ProtectedJniExtensions.ensureInitialized(); + return JImplementer.fromReference(_new( + _class.reference.pointer, + _newId as JMethodIDPtr, + ProtectedJniExtensions.getCurrentIsolateId()) + .reference); + } + + static final _addImplementationId = _class.instanceMethodId( + r'addImplementation', + r'(Ljava/lang/String;JJ)V', + ); + + static final _addImplementation = ProtectedJniExtensions.lookup< + NativeFunction< + JThrowablePtr Function(Pointer, JMethodIDPtr, + VarArgs<(Pointer, Int64, Int64)>)>>( + 'globalEnv_CallVoidMethod') + .asFunction< + JThrowablePtr Function( + Pointer, JMethodIDPtr, Pointer, int, int)>(); + + /// Should not be used directly. + /// + /// Use `implementIn` from the generated interface instead. + @internal + void add( + String binaryName, + RawReceivePort port, + Pointer> + pointer, + ) { + using((arena) { + _addImplementation( + reference.pointer, + _addImplementationId as JMethodIDPtr, + (binaryName.toJString()..releasedBy(arena)).reference.pointer, + port.sendPort.nativePort, + pointer.address) + .check(); + }); + } + + static final _buildId = _class.instanceMethodId( + r'build', + r'()Ljava/lang/Object;', + ); + + static final _build = ProtectedJniExtensions.lookup< + NativeFunction< + JniResult Function( + Pointer, + JMethodIDPtr, + )>>('globalEnv_CallObjectMethod') + .asFunction< + JniResult Function( + Pointer, + JMethodIDPtr, + )>(); + + /// Builds an proxy object with the specified [type] that implements all the + /// added interfaces with the given implementations. + /// + /// Releases this implementer. + T implement(JObjType type) { + return type.fromReference(implementReference()); + } + + /// Used in the JNIgen generated code. + /// + /// It is unnecessary to construct the type object when the code is generated. + @internal + JReference implementReference() { + final ref = _build(reference.pointer, _buildId as JMethodIDPtr).reference; + release(); + return ref; + } +} diff --git a/pkgs/jni/lib/src/jni.dart b/pkgs/jni/lib/src/jni.dart index c96e183b8..cfafc37b8 100644 --- a/pkgs/jni/lib/src/jni.dart +++ b/pkgs/jni/lib/src/jni.dart @@ -10,8 +10,8 @@ import 'package:ffi/ffi.dart'; import 'package:meta/meta.dart' show internal; import 'package:path/path.dart'; +import '../_internal.dart'; import '../jni.dart'; -import 'accessors.dart'; import 'third_party/generated_bindings.dart'; String _getLibraryFileName(String base) { @@ -45,8 +45,6 @@ DynamicLibrary _loadDartJniLibrary({String? dir, String baseName = 'dartjni'}) { abstract final class Jni { static final DynamicLibrary _dylib = _loadDartJniLibrary(dir: _dylibDir); static final JniBindings _bindings = JniBindings(_dylib); - static final _getJniEnvFn = _dylib.lookup('GetJniEnv'); - static final _getJniContextFn = _dylib.lookup('GetJniContextPtr'); /// Store dylibDir if any was used. static String? _dylibDir; @@ -64,19 +62,6 @@ abstract final class Jni { } } - static bool _initialized = false; - - /// Initializes DartApiDL used for Continuations and interface implementation. - static void _ensureInitialized() { - if (!_initialized) { - assert(NativeApi.majorVersion == 2); - assert(NativeApi.minorVersion >= 3); - final result = _bindings.InitDartApiDL(NativeApi.initializeApiDLData); - _initialized = result == 0; - assert(_initialized); - } - } - /// Spawn an instance of JVM using JNI. This method should be called at the /// beginning of the program with appropriate options, before other isolates /// are spawned. @@ -236,29 +221,28 @@ abstract final class Jni { JGlobalReference(_bindings.GetClassLoader()); } -typedef _SetJniGettersNativeType = Void Function(Pointer, Pointer); -typedef _SetJniGettersDartType = void Function(Pointer, Pointer); - /// Extensions for use by `jnigen` generated code. @internal extension ProtectedJniExtensions on Jni { - static final _jThrowableClass = JClass.forName('java/lang/Throwable'); + static bool _initialized = false; - static Pointer Function(String) initGeneratedLibrary( - String name) { - var path = _getLibraryFileName(name); - if (Jni._dylibDir != null) { - path = join(Jni._dylibDir!, path); + /// Initializes DartApiDL used for Continuations and interface implementation. + static void ensureInitialized() { + if (!_initialized) { + assert(NativeApi.majorVersion == 2); + assert(NativeApi.minorVersion >= 3); + final result = Jni._bindings.InitDartApiDL(NativeApi.initializeApiDLData); + _initialized = result == 0; + assert(_initialized); } - final dl = DynamicLibrary.open(path); - final setJniGetters = - dl.lookupFunction<_SetJniGettersNativeType, _SetJniGettersDartType>( - 'setJniGetters'); - setJniGetters(Jni._getJniContextFn, Jni._getJniEnvFn); - final lookup = dl.lookup; - return lookup; } + static int getCurrentIsolateId() { + return Jni._bindings.GetCurrentIsolateId(); + } + + static final _jThrowableClass = JClass.forName('java/lang/Throwable'); + /// Returns a new DartException. static Pointer newDartException(Object exception) { JObjectPtr? cause; @@ -275,7 +259,7 @@ extension ProtectedJniExtensions on Jni { /// Returns a new PortContinuation. static JReference newPortContinuation(ReceivePort port) { - Jni._ensureInitialized(); + ensureInitialized(); return JGlobalReference( Jni._bindings .PortContinuation__ctor(port.sendPort.nativePort) @@ -283,24 +267,6 @@ extension ProtectedJniExtensions on Jni { ); } - /// Returns a new PortProxy for a class with the given [binaryName]. - static JReference newPortProxy( - String binaryName, - ReceivePort port, - Pointer< - NativeFunction< - Pointer Function(Uint64, Pointer, Pointer)>> - functionPtr) { - Jni._ensureInitialized(); - return JGlobalReference(Jni._bindings - .PortProxy__newInstance( - Jni.env.toJStringPtr(binaryName), - port.sendPort.nativePort, - functionPtr.address, - ) - .objectPointer); - } - /// Returns the result of a callback. static void returnResult( Pointer result, JObjectPtr object) async { @@ -312,7 +278,7 @@ extension ProtectedJniExtensions on Jni { Pointer reference, int refType, ) { - Jni._ensureInitialized(); + ensureInitialized(); return Jni._bindings .newJObjectFinalizableHandle(object, reference, refType); } @@ -321,13 +287,13 @@ extension ProtectedJniExtensions on Jni { Object object, Pointer reference, ) { - Jni._ensureInitialized(); + ensureInitialized(); return Jni._bindings.newBooleanFinalizableHandle(object, reference); } static void deleteFinalizableHandle( Dart_FinalizableHandle finalizableHandle, Object object) { - Jni._ensureInitialized(); + ensureInitialized(); Jni._bindings.deleteFinalizableHandle(finalizableHandle, object); } @@ -368,16 +334,10 @@ extension AdditionalEnvMethods on GlobalJniEnv { }); } +@internal extension StringMethodsForJni on String { /// Returns a Utf-8 encoded `Pointer` with contents same as this string. - Pointer toNativeChars([Allocator allocator = malloc]) { + Pointer toNativeChars(Allocator allocator) { return toNativeUtf8(allocator: allocator).cast(); } } - -extension CharPtrMethodsForJni on Pointer { - /// Same as calling `cast` followed by `toDartString`. - String toDartString({int? length}) { - return cast().toDartString(length: length); - } -} diff --git a/pkgs/jni/lib/src/third_party/jni_bindings_generated.dart b/pkgs/jni/lib/src/third_party/jni_bindings_generated.dart index 8f03912ba..e970cbe4b 100644 --- a/pkgs/jni/lib/src/third_party/jni_bindings_generated.dart +++ b/pkgs/jni/lib/src/third_party/jni_bindings_generated.dart @@ -185,6 +185,15 @@ class JniBindings { late final _InitDartApiDL = _InitDartApiDLPtr.asFunction)>(); + int GetCurrentIsolateId() { + return _GetCurrentIsolateId(); + } + + late final _GetCurrentIsolateIdPtr = + _lookup>('GetCurrentIsolateId'); + late final _GetCurrentIsolateId = + _GetCurrentIsolateIdPtr.asFunction(); + JniResult DartException__ctor( JStringPtr message, JThrowablePtr cause, @@ -215,25 +224,6 @@ class JniBindings { late final _PortContinuation__ctor = _PortContinuation__ctorPtr.asFunction(); - JniResult PortProxy__newInstance( - JObjectPtr binaryName, - int port, - int functionPtr, - ) { - return _PortProxy__newInstance( - binaryName, - port, - functionPtr, - ); - } - - late final _PortProxy__newInstancePtr = _lookup< - ffi.NativeFunction< - JniResult Function( - JObjectPtr, ffi.Int64, ffi.Int64)>>('PortProxy__newInstance'); - late final _PortProxy__newInstance = _PortProxy__newInstancePtr.asFunction< - JniResult Function(JObjectPtr, int, int)>(); - void resultFor( ffi.Pointer result, JObjectPtr object, diff --git a/pkgs/jni/src/dartjni.c b/pkgs/jni/src/dartjni.c index 18a981a31..0ae465ef6 100644 --- a/pkgs/jni/src/dartjni.c +++ b/pkgs/jni/src/dartjni.c @@ -278,6 +278,10 @@ FFI_PLUGIN_EXPORT intptr_t InitDartApiDL(void* data) { return Dart_InitializeApiDL(data); } +FFI_PLUGIN_EXPORT int64_t GetCurrentIsolateId() { + return (int64_t)Dart_CurrentIsolate_DL(); +} + // com.github.dart_lang.jni.DartException jclass _c_DartException = NULL; @@ -285,8 +289,9 @@ jmethodID _m_DartException__ctor = NULL; FFI_PLUGIN_EXPORT JniResult DartException__ctor(jstring message, jthrowable cause) { attach_thread(); - load_class_global_ref(&_c_DartException, - "com/github/dart_lang/jni/PortProxy$DartException"); + load_class_global_ref( + &_c_DartException, + "com/github/dart_lang/jni/PortProxyBuilder$DartException"); if (_c_DartException == NULL) return (JniResult){.value = {.j = 0}, .exception = check_exception()}; load_method(_c_DartException, &_m_DartException__ctor, "", @@ -338,28 +343,6 @@ JniResult PortContinuation__ctor(int64_t j) { return (JniResult){.value = {.l = _result}, .exception = check_exception()}; } -// com.github.dart_lang.jni.PortProxy -jclass _c_PortProxy = NULL; - -jmethodID _m_PortProxy__newInstance = NULL; -FFI_PLUGIN_EXPORT -JniResult PortProxy__newInstance(jobject binaryName, - int64_t port, - int64_t functionPtr) { - attach_thread(); - load_class_global_ref(&_c_PortProxy, "com/github/dart_lang/jni/PortProxy"); - if (_c_PortProxy == NULL) - return (JniResult){.value = {.j = 0}, .exception = check_exception()}; - load_static_method(_c_PortProxy, &_m_PortProxy__newInstance, "newInstance", - "(Ljava/lang/String;JJJ)Ljava/lang/Object;"); - if (_m_PortProxy__newInstance == NULL) - return (JniResult){.value = {.j = 0}, .exception = check_exception()}; - jobject _result = (*jniEnv)->CallStaticObjectMethod( - jniEnv, _c_PortProxy, _m_PortProxy__newInstance, binaryName, port, - (jlong)Dart_CurrentIsolate_DL(), functionPtr); - return to_global_ref_result(_result); -} - FFI_PLUGIN_EXPORT void resultFor(CallbackResult* result, jobject object) { acquire_lock(&result->lock); @@ -427,14 +410,15 @@ jclass _c_Long = NULL; jmethodID _m_Long_init = NULL; JNIEXPORT jobjectArray JNICALL -Java_com_github_dart_1lang_jni_PortProxy__1invoke(JNIEnv* env, - jclass clazz, - jlong port, - jlong isolateId, - jlong functionPtr, - jobject proxy, - jstring methodDescriptor, - jobjectArray args) { +Java_com_github_dart_1lang_jni_PortProxyBuilder__1invoke( + JNIEnv* env, + jclass clazz, + jlong port, + jlong isolateId, + jlong functionPtr, + jobject proxy, + jstring methodDescriptor, + jobjectArray args) { CallbackResult* result = (CallbackResult*)malloc(sizeof(CallbackResult)); if (isolateId != (jlong)Dart_CurrentIsolate_DL()) { init_lock(&result->lock); @@ -493,9 +477,9 @@ Java_com_github_dart_1lang_jni_PortProxy__1invoke(JNIEnv* env, } JNIEXPORT void JNICALL -Java_com_github_dart_1lang_jni_PortProxy__1cleanUp(JNIEnv* env, - jclass clazz, - jlong resultPtr) { +Java_com_github_dart_1lang_jni_PortProxyBuilder__1cleanUp(JNIEnv* env, + jclass clazz, + jlong resultPtr) { CallbackResult* result = (CallbackResult*)resultPtr; (*env)->DeleteGlobalRef(env, result->object); free(result); diff --git a/pkgs/jni/src/dartjni.h b/pkgs/jni/src/dartjni.h index 53a4ad15d..5d182c13a 100644 --- a/pkgs/jni/src/dartjni.h +++ b/pkgs/jni/src/dartjni.h @@ -340,17 +340,14 @@ static inline JniResult to_global_ref_result(jobject ref) { FFI_PLUGIN_EXPORT intptr_t InitDartApiDL(void* data); +FFI_PLUGIN_EXPORT int64_t GetCurrentIsolateId(); + FFI_PLUGIN_EXPORT JniResult DartException__ctor(jstring message, jthrowable cause); FFI_PLUGIN_EXPORT JniResult PortContinuation__ctor(int64_t j); -FFI_PLUGIN_EXPORT -JniResult PortProxy__newInstance(jobject binaryName, - int64_t port, - int64_t functionPtr); - FFI_PLUGIN_EXPORT void resultFor(CallbackResult* result, jobject object); FFI_PLUGIN_EXPORT diff --git a/pkgs/jni/test/global_env_test.dart b/pkgs/jni/test/global_env_test.dart index 377d3705b..d1296c3c7 100644 --- a/pkgs/jni/test/global_env_test.dart +++ b/pkgs/jni/test/global_env_test.dart @@ -6,6 +6,7 @@ import 'dart:io'; import 'package:ffi/ffi.dart'; import 'package:jni/jni.dart'; +import 'package:jni/src/jni.dart'; import 'package:test/test.dart'; import 'test_util/test_util.dart'; @@ -122,7 +123,7 @@ void run({required TestRunnerCallback testRunner}) { final jstr = env.NewStringUTF(str.toNativeChars(arena)); final jchars = env.GetStringUTFChars(jstr, nullptr); final jlen = env.GetStringUTFLength(jstr); - final dstr = jchars.toDartString(length: jlen); + final dstr = jchars.cast().toDartString(length: jlen); env.ReleaseStringUTFChars(jstr, jchars); expect(str, equals(dstr)); env.DeleteGlobalRef(jstr); diff --git a/pkgs/jnigen/CHANGELOG.md b/pkgs/jnigen/CHANGELOG.md index 567e0281c..98238dd0d 100644 --- a/pkgs/jnigen/CHANGELOG.md +++ b/pkgs/jnigen/CHANGELOG.md @@ -9,6 +9,12 @@ [documentation](https://github.com/dart-lang/native/tree/main/pkgs/jnigen/docs/java_differences.md#method-overloading). - **Breaking Change**: Each single dollar sign is replaced with two dollar signs in the identifier names. +- **Breaking Change**: Removed the `Impl` suffix from the generated + implemenation classes. So the implementation class for an interface named + `Foo` is now simply called `$Foo` instead of `$FooImpl`. +- Added `JImplementer` which enables building an object that implements multiple + Java interfaces. Each interface now has a static `implementIn` method that + takes a `JImplementer` and the implementation object. - Generating identifiers that start with an underscore (`_`) and making them public by prepending a dollar sign. - Fixed an issue where inheriting a generic class could generate incorrect code. diff --git a/pkgs/jnigen/docs/interface_implementation.md b/pkgs/jnigen/docs/interface_implementation.md new file mode 100644 index 000000000..01a25efce --- /dev/null +++ b/pkgs/jnigen/docs/interface_implementation.md @@ -0,0 +1,145 @@ +## Implementing Java interfaces from Dart + +> [!NOTE] +> This feature is experimental, and in +> [active development](https://github.com/dart-lang/native/issues/1569). +> +> To opt in to use this feature, add the following to your JNIgen configuration +> yaml: +> +> ```yaml +> enable_experiment: +> - interface_implementation +> ``` + +Let's take a simple Java interface like `Runnable` that has a single `void` +method called `run`: + +```java +// Java +public interface Runnable { + void run(); +} +``` + +These are the bindings that JNIgen generates for this interface: + +```dart +// Dart Bindings - Boilerplate omitted for clarity. +class Runnable extends JObject { + void run() { /* ... */ } + + factory Runnable.implement($Runnable impl) { /* ... */ } + static void implementIn( + JImplementer implementer, + $Runnable impl, + ) { /* ... */ } +} + +abstract interface class $Runnable { + factory $Runnable({ + required void Function() run, + }) = _$Runnable; + + void run(); +} + +class _$Runnable implements $Runnable { + _$Runnable({ + required void Function() run, + }) : _run = run; + + final void Function() _run; + + void run() { + return _run(); + } +} +``` + +### Implementing interfaces inline + +`Runnable` is used a lot to pass a void callback to a function. To simply this +workflow, Java 8 introduced lambdas. + +```java +// Java +Runnable runnable = () -> System.out.println("hello"); +``` + +To allow the same flexibility in Dart, `$Runnable` has a default factory that +simply gets each method of the interface as a closure argument. So you would do: + +```dart +// Dart +final runnable = Runnable.implement($Runnable(run: () => print('hello'))); +``` + +### Reuse the same implementation + +The reason JNIgen generates the `$Runnable` class is to make it easier to reuse +the same implementation for multiple instances. This is analogous to actually +implementing the interface in Java instead of using the lambdas: + +```java +// Java +public class Printer implements Runnable { + private final String text; + + public Printer(String text) { + this.text = text; + } + + @Override + public void run() { + System.out.println(text); + } +} +``` + +This way you can create multiple such Runnables like `new Printer("hello")` and +`new Printer("world")`. + +You can do the same in Dart by creating a subclass that implements `$Runnable`: + +```dart +// Dart +class Printer implements $Runnable { + final String text; + + Printer(this.text); + + @override + void run() { + print(text); + } +} +``` + +And similarly write `Runnable.implement(Printer('hello'))` and +`Runnable.implement(Printer('world'))`, to create multiple Runnables and share +common logic. + +### Implement multiple interfaces + +To implement more than one interface, use a `JImplementer` from `package:jni`. +`Closable` is another simple Java interface that has a single void `close` +method. Here is how we create an object that implements both `Runnable` and +`Closable`: + +```dart +// Dart +final implementer = JImplementer(); +Runnable.implementIn(implementer, $Runnable(run: () => print('run'))); +Closable.implementIn(implementer, $Closable(close: () => print('close'))); +final object = implementer.implement(Runnable.type); // or Closable.type. +``` + +As the created `object` implements both `Runnable` and `Closable`, it's also +possible to make it a `Closable` by passing in `Closable.type` to +`implementer.implement`. Or simply cast it after creation: + +```dart +// Dart +final closable = object.castTo(Closable.type); +``` diff --git a/pkgs/jnigen/example/in_app_java/lib/android_utils.dart b/pkgs/jnigen/example/in_app_java/lib/android_utils.dart index e490878af..936e3f0ab 100644 --- a/pkgs/jnigen/example/in_app_java/lib/android_utils.dart +++ b/pkgs/jnigen/example/in_app_java/lib/android_utils.dart @@ -7,6 +7,7 @@ // ignore_for_file: constant_identifier_names // ignore_for_file: doc_directive_unknown // ignore_for_file: file_names +// ignore_for_file: inference_failure_on_untyped_parameter // ignore_for_file: invalid_use_of_internal_member // ignore_for_file: lines_longer_than_80_chars // ignore_for_file: no_leading_underscores_for_local_identifiers @@ -25,7 +26,7 @@ // ignore_for_file: use_super_parameters import 'dart:ffi' as ffi; -import 'dart:isolate' show ReceivePort; +import 'dart:isolate' show RawReceivePort, ReceivePort; import 'package:jni/_internal.dart'; import 'package:jni/jni.dart' as jni; diff --git a/pkgs/jnigen/example/kotlin_plugin/lib/kotlin_bindings.dart b/pkgs/jnigen/example/kotlin_plugin/lib/kotlin_bindings.dart index 7d6892d43..70569b65f 100644 --- a/pkgs/jnigen/example/kotlin_plugin/lib/kotlin_bindings.dart +++ b/pkgs/jnigen/example/kotlin_plugin/lib/kotlin_bindings.dart @@ -7,6 +7,7 @@ // ignore_for_file: constant_identifier_names // ignore_for_file: doc_directive_unknown // ignore_for_file: file_names +// ignore_for_file: inference_failure_on_untyped_parameter // ignore_for_file: invalid_use_of_internal_member // ignore_for_file: lines_longer_than_80_chars // ignore_for_file: no_leading_underscores_for_local_identifiers @@ -25,7 +26,7 @@ // ignore_for_file: use_super_parameters import 'dart:ffi' as ffi; -import 'dart:isolate' show ReceivePort; +import 'dart:isolate' show RawReceivePort, ReceivePort; import 'package:jni/_internal.dart'; import 'package:jni/jni.dart' as jni; diff --git a/pkgs/jnigen/example/notification_plugin/lib/notifications.dart b/pkgs/jnigen/example/notification_plugin/lib/notifications.dart index b5908014f..9b8afd1b0 100644 --- a/pkgs/jnigen/example/notification_plugin/lib/notifications.dart +++ b/pkgs/jnigen/example/notification_plugin/lib/notifications.dart @@ -11,6 +11,7 @@ // ignore_for_file: constant_identifier_names // ignore_for_file: doc_directive_unknown // ignore_for_file: file_names +// ignore_for_file: inference_failure_on_untyped_parameter // ignore_for_file: invalid_use_of_internal_member // ignore_for_file: lines_longer_than_80_chars // ignore_for_file: no_leading_underscores_for_local_identifiers @@ -29,7 +30,7 @@ // ignore_for_file: use_super_parameters import 'dart:ffi' as ffi; -import 'dart:isolate' show ReceivePort; +import 'dart:isolate' show RawReceivePort, ReceivePort; import 'package:jni/_internal.dart'; import 'package:jni/jni.dart' as jni; diff --git a/pkgs/jnigen/example/pdfbox_plugin/lib/src/third_party/org/apache/pdfbox/pdmodel/PDDocument.dart b/pkgs/jnigen/example/pdfbox_plugin/lib/src/third_party/org/apache/pdfbox/pdmodel/PDDocument.dart index a413b80b5..d159f5155 100644 --- a/pkgs/jnigen/example/pdfbox_plugin/lib/src/third_party/org/apache/pdfbox/pdmodel/PDDocument.dart +++ b/pkgs/jnigen/example/pdfbox_plugin/lib/src/third_party/org/apache/pdfbox/pdmodel/PDDocument.dart @@ -25,6 +25,7 @@ // ignore_for_file: constant_identifier_names // ignore_for_file: doc_directive_unknown // ignore_for_file: file_names +// ignore_for_file: inference_failure_on_untyped_parameter // ignore_for_file: invalid_use_of_internal_member // ignore_for_file: lines_longer_than_80_chars // ignore_for_file: no_leading_underscores_for_local_identifiers @@ -43,7 +44,7 @@ // ignore_for_file: use_super_parameters import 'dart:ffi' as ffi; -import 'dart:isolate' show ReceivePort; +import 'dart:isolate' show RawReceivePort, ReceivePort; import 'package:jni/_internal.dart'; import 'package:jni/jni.dart' as jni; diff --git a/pkgs/jnigen/example/pdfbox_plugin/lib/src/third_party/org/apache/pdfbox/pdmodel/PDDocumentInformation.dart b/pkgs/jnigen/example/pdfbox_plugin/lib/src/third_party/org/apache/pdfbox/pdmodel/PDDocumentInformation.dart index 0d6170158..784b16f3b 100644 --- a/pkgs/jnigen/example/pdfbox_plugin/lib/src/third_party/org/apache/pdfbox/pdmodel/PDDocumentInformation.dart +++ b/pkgs/jnigen/example/pdfbox_plugin/lib/src/third_party/org/apache/pdfbox/pdmodel/PDDocumentInformation.dart @@ -25,6 +25,7 @@ // ignore_for_file: constant_identifier_names // ignore_for_file: doc_directive_unknown // ignore_for_file: file_names +// ignore_for_file: inference_failure_on_untyped_parameter // ignore_for_file: invalid_use_of_internal_member // ignore_for_file: lines_longer_than_80_chars // ignore_for_file: no_leading_underscores_for_local_identifiers @@ -43,7 +44,7 @@ // ignore_for_file: use_super_parameters import 'dart:ffi' as ffi; -import 'dart:isolate' show ReceivePort; +import 'dart:isolate' show RawReceivePort, ReceivePort; import 'package:jni/_internal.dart'; import 'package:jni/jni.dart' as jni; diff --git a/pkgs/jnigen/example/pdfbox_plugin/lib/src/third_party/org/apache/pdfbox/text/PDFTextStripper.dart b/pkgs/jnigen/example/pdfbox_plugin/lib/src/third_party/org/apache/pdfbox/text/PDFTextStripper.dart index 6ecb5b732..fd97d14f4 100644 --- a/pkgs/jnigen/example/pdfbox_plugin/lib/src/third_party/org/apache/pdfbox/text/PDFTextStripper.dart +++ b/pkgs/jnigen/example/pdfbox_plugin/lib/src/third_party/org/apache/pdfbox/text/PDFTextStripper.dart @@ -25,6 +25,7 @@ // ignore_for_file: constant_identifier_names // ignore_for_file: doc_directive_unknown // ignore_for_file: file_names +// ignore_for_file: inference_failure_on_untyped_parameter // ignore_for_file: invalid_use_of_internal_member // ignore_for_file: lines_longer_than_80_chars // ignore_for_file: no_leading_underscores_for_local_identifiers @@ -43,7 +44,7 @@ // ignore_for_file: use_super_parameters import 'dart:ffi' as ffi; -import 'dart:isolate' show ReceivePort; +import 'dart:isolate' show RawReceivePort, ReceivePort; import 'package:jni/_internal.dart'; import 'package:jni/jni.dart' as jni; diff --git a/pkgs/jnigen/lib/src/bindings/dart_generator.dart b/pkgs/jnigen/lib/src/bindings/dart_generator.dart index a964ec549..4fe850c8b 100644 --- a/pkgs/jnigen/lib/src/bindings/dart_generator.dart +++ b/pkgs/jnigen/lib/src/bindings/dart_generator.dart @@ -125,7 +125,7 @@ class DartGenerator extends Visitor> { 'DO NOT EDIT!\n\n'; static const defaultImports = ''' import 'dart:ffi' as ffi; -import 'dart:isolate' show ReceivePort; +import 'dart:isolate' show RawReceivePort, ReceivePort; import 'package:jni/_internal.dart'; import 'package:jni/jni.dart' as jni; @@ -141,6 +141,7 @@ import 'package:jni/jni.dart' as jni; // ignore_for_file: constant_identifier_names // ignore_for_file: doc_directive_unknown // ignore_for_file: file_names +// ignore_for_file: inference_failure_on_untyped_parameter // ignore_for_file: invalid_use_of_internal_member // ignore_for_file: lines_longer_than_80_chars // ignore_for_file: no_leading_underscores_for_local_identifiers @@ -312,7 +313,7 @@ ${modifier}final $classRef = $_jni.JClass.forName(r'$internalName'); // Class definition. final name = node.finalName; final superName = node.superclass!.accept(_TypeGenerator(resolver)); - final implClassName = '\$${name}Impl'; + final implClassName = '\$$name'; final typeParamsDef = _encloseIfNotEmpty( '<', node.allTypeParams @@ -402,8 +403,6 @@ class $name$typeParamsDef extends $superName { static final Map _\$impls = {}; '''); s.write(r''' - ReceivePort? _$p; - static jni.JObjectPtr _$invoke( int port, jni.JObjectPtr descriptor, @@ -422,7 +421,7 @@ class $name$typeParamsDef extends $superName { static final ffi.Pointer< ffi.NativeFunction< jni.JObjectPtr Function( - ffi.Uint64, jni.JObjectPtr, jni.JObjectPtr)>> + ffi.Int64, jni.JObjectPtr, jni.JObjectPtr)>> _$invokePointer = ffi.Pointer.fromFunction(_$invoke); static ffi.Pointer _$invokeMethod( @@ -444,26 +443,12 @@ class $name$typeParamsDef extends $superName { return jni.nullptr; } - factory $name.implement( + static void implementIn$typeParamsDef( + $_jni.JImplementer implementer, $implClassName$typeParamsCall \$impl, ) { -'''); - final typeClassesCall = typeParams - .map((typeParam) => '\$impl.$typeParam,') - .join(_newLine(depth: 3)); - s.write(''' - final \$p = ReceivePort(); - final \$x = $name.fromReference( - $typeClassesCall - $_protectedExtension.newPortProxy( - r'${node.binaryName}', - \$p, - _\$invokePointer, - ), - ).._\$p = \$p; - final \$a = \$p.sendPort.nativePort; - _\$impls[\$a] = \$impl; - \$p.listen((\$m) { + late final RawReceivePort \$p; + \$p = RawReceivePort((\$m) { if (\$m == null) { _\$impls.remove(\$p.sendPort.nativePort); \$p.close(); @@ -473,7 +458,29 @@ class $name$typeParamsDef extends $superName { final \$r = _\$invokeMethod(\$p.sendPort.nativePort, \$i); $_protectedExtension.returnResult(\$i.result, \$r); }); - return \$x; + implementer.add( + r'${node.binaryName}', + \$p, + _\$invokePointer, + ); + final \$a = \$p.sendPort.nativePort; + _\$impls[\$a] = \$impl; + } + + factory $name.implement( + $implClassName$typeParamsCall \$impl, + ) { +'''); + final typeClassesCall = typeParams + .map((typeParam) => '\$impl.$typeParam,') + .join(_newLine(depth: 3)); + s.write(''' + final \$i = $_jni.JImplementer(); + implementIn(\$i, \$impl); + return $name.fromReference( + $typeClassesCall + \$i.implementReference(), + ); } '''); } diff --git a/pkgs/jnigen/lib/src/bindings/renamer.dart b/pkgs/jnigen/lib/src/bindings/renamer.dart index d8268fd73..c3178e41d 100644 --- a/pkgs/jnigen/lib/src/bindings/renamer.dart +++ b/pkgs/jnigen/lib/src/bindings/renamer.dart @@ -157,6 +157,10 @@ class _ClassRenamer implements Visitor { renamed.add(node); nameCounts[node] = {..._definedSyms}; + if (node.declKind == DeclKind.interfaceKind) { + nameCounts[node]!['implement'] = 1; + nameCounts[node]!['implementIn'] = 1; + } node.methodNumsAfterRenaming = {}; // TODO(https://github.com/dart-lang/native/issues/1516): Nested classes diff --git a/pkgs/jnigen/test/jackson_core_test/third_party/bindings/com/fasterxml/jackson/core/JsonFactory.dart b/pkgs/jnigen/test/jackson_core_test/third_party/bindings/com/fasterxml/jackson/core/JsonFactory.dart index 8b0622ba5..bf24fe2f9 100644 --- a/pkgs/jnigen/test/jackson_core_test/third_party/bindings/com/fasterxml/jackson/core/JsonFactory.dart +++ b/pkgs/jnigen/test/jackson_core_test/third_party/bindings/com/fasterxml/jackson/core/JsonFactory.dart @@ -24,6 +24,7 @@ // ignore_for_file: constant_identifier_names // ignore_for_file: doc_directive_unknown // ignore_for_file: file_names +// ignore_for_file: inference_failure_on_untyped_parameter // ignore_for_file: invalid_use_of_internal_member // ignore_for_file: lines_longer_than_80_chars // ignore_for_file: no_leading_underscores_for_local_identifiers @@ -42,7 +43,7 @@ // ignore_for_file: use_super_parameters import 'dart:ffi' as ffi; -import 'dart:isolate' show ReceivePort; +import 'dart:isolate' show RawReceivePort, ReceivePort; import 'package:jni/_internal.dart'; import 'package:jni/jni.dart' as jni; diff --git a/pkgs/jnigen/test/jackson_core_test/third_party/bindings/com/fasterxml/jackson/core/JsonParser.dart b/pkgs/jnigen/test/jackson_core_test/third_party/bindings/com/fasterxml/jackson/core/JsonParser.dart index 3e4442166..6ffc2b369 100644 --- a/pkgs/jnigen/test/jackson_core_test/third_party/bindings/com/fasterxml/jackson/core/JsonParser.dart +++ b/pkgs/jnigen/test/jackson_core_test/third_party/bindings/com/fasterxml/jackson/core/JsonParser.dart @@ -24,6 +24,7 @@ // ignore_for_file: constant_identifier_names // ignore_for_file: doc_directive_unknown // ignore_for_file: file_names +// ignore_for_file: inference_failure_on_untyped_parameter // ignore_for_file: invalid_use_of_internal_member // ignore_for_file: lines_longer_than_80_chars // ignore_for_file: no_leading_underscores_for_local_identifiers @@ -42,7 +43,7 @@ // ignore_for_file: use_super_parameters import 'dart:ffi' as ffi; -import 'dart:isolate' show ReceivePort; +import 'dart:isolate' show RawReceivePort, ReceivePort; import 'package:jni/_internal.dart'; import 'package:jni/jni.dart' as jni; diff --git a/pkgs/jnigen/test/jackson_core_test/third_party/bindings/com/fasterxml/jackson/core/JsonToken.dart b/pkgs/jnigen/test/jackson_core_test/third_party/bindings/com/fasterxml/jackson/core/JsonToken.dart index 50677f7fc..56551538c 100644 --- a/pkgs/jnigen/test/jackson_core_test/third_party/bindings/com/fasterxml/jackson/core/JsonToken.dart +++ b/pkgs/jnigen/test/jackson_core_test/third_party/bindings/com/fasterxml/jackson/core/JsonToken.dart @@ -24,6 +24,7 @@ // ignore_for_file: constant_identifier_names // ignore_for_file: doc_directive_unknown // ignore_for_file: file_names +// ignore_for_file: inference_failure_on_untyped_parameter // ignore_for_file: invalid_use_of_internal_member // ignore_for_file: lines_longer_than_80_chars // ignore_for_file: no_leading_underscores_for_local_identifiers @@ -42,7 +43,7 @@ // ignore_for_file: use_super_parameters import 'dart:ffi' as ffi; -import 'dart:isolate' show ReceivePort; +import 'dart:isolate' show RawReceivePort, ReceivePort; import 'package:jni/_internal.dart'; import 'package:jni/jni.dart' as jni; diff --git a/pkgs/jnigen/test/kotlin_test/bindings/kotlin.dart b/pkgs/jnigen/test/kotlin_test/bindings/kotlin.dart index 2886e848b..d4db3823f 100644 --- a/pkgs/jnigen/test/kotlin_test/bindings/kotlin.dart +++ b/pkgs/jnigen/test/kotlin_test/bindings/kotlin.dart @@ -11,6 +11,7 @@ // ignore_for_file: constant_identifier_names // ignore_for_file: doc_directive_unknown // ignore_for_file: file_names +// ignore_for_file: inference_failure_on_untyped_parameter // ignore_for_file: invalid_use_of_internal_member // ignore_for_file: lines_longer_than_80_chars // ignore_for_file: no_leading_underscores_for_local_identifiers @@ -29,7 +30,7 @@ // ignore_for_file: use_super_parameters import 'dart:ffi' as ffi; -import 'dart:isolate' show ReceivePort; +import 'dart:isolate' show RawReceivePort, ReceivePort; import 'package:jni/_internal.dart'; import 'package:jni/jni.dart' as jni; diff --git a/pkgs/jnigen/test/renamer_test.dart b/pkgs/jnigen/test/renamer_test.dart index a8bf9e15a..5fea38fc6 100644 --- a/pkgs/jnigen/test/renamer_test.dart +++ b/pkgs/jnigen/test/renamer_test.dart @@ -265,4 +265,34 @@ void main() { final renamedFields = classes.decls[r'_Foo$']!.fields.finalNames; expect(renamedFields, [r'$_foo$$']); }); + + test('Interface implementation methods', () async { + final classes = Classes({ + 'MyInterface': ClassDecl( + binaryName: 'MyInterface', + declKind: DeclKind.interfaceKind, + superclass: TypeUsage.object, + methods: [ + Method(name: 'implement', returnType: TypeUsage.object), + Method(name: 'implementIn', returnType: TypeUsage.object), + ], + ), + 'MyClass': ClassDecl( + binaryName: 'MyClass', + declKind: DeclKind.classKind, + superclass: TypeUsage.object, + methods: [ + Method(name: 'implement', returnType: TypeUsage.object), + Method(name: 'implementIn', returnType: TypeUsage.object), + ], + ), + }); + await rename(classes); + + final interfaceRenamedMethods = + classes.decls['MyInterface']!.methods.finalNames; + expect(interfaceRenamedMethods, [r'implement$1', r'implementIn$1']); + final classRenamedMethods = classes.decls['MyClass']!.methods.finalNames; + expect(classRenamedMethods, [r'implement', r'implementIn']); + }); } 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 4bdb84108..45e33cb00 100644 --- a/pkgs/jnigen/test/simple_package_test/bindings/simple_package.dart +++ b/pkgs/jnigen/test/simple_package_test/bindings/simple_package.dart @@ -11,6 +11,7 @@ // ignore_for_file: constant_identifier_names // ignore_for_file: doc_directive_unknown // ignore_for_file: file_names +// ignore_for_file: inference_failure_on_untyped_parameter // ignore_for_file: invalid_use_of_internal_member // ignore_for_file: lines_longer_than_80_chars // ignore_for_file: no_leading_underscores_for_local_identifiers @@ -29,7 +30,7 @@ // ignore_for_file: use_super_parameters import 'dart:ffi' as ffi; -import 'dart:isolate' show ReceivePort; +import 'dart:isolate' show RawReceivePort, ReceivePort; import 'package:jni/_internal.dart'; import 'package:jni/jni.dart' as jni; @@ -4468,9 +4469,7 @@ class MyInterface<$T extends jni.JObject> extends jni.JObject { } /// Maps a specific port to the implemented interface. - static final Map _$impls = {}; - ReceivePort? _$p; - + static final Map _$impls = {}; static jni.JObjectPtr _$invoke( int port, jni.JObjectPtr descriptor, @@ -4489,8 +4488,8 @@ class MyInterface<$T extends jni.JObject> extends jni.JObject { static final ffi.Pointer< ffi.NativeFunction< jni.JObjectPtr Function( - ffi.Uint64, jni.JObjectPtr, jni.JObjectPtr)>> - _$invokePointer = ffi.Pointer.fromFunction(_$invoke); + ffi.Int64, jni.JObjectPtr, jni.JObjectPtr)>> _$invokePointer = + ffi.Pointer.fromFunction(_$invoke); static ffi.Pointer _$invokeMethod( int $p, @@ -4546,21 +4545,12 @@ class MyInterface<$T extends jni.JObject> extends jni.JObject { return jni.nullptr; } - factory MyInterface.implement( - $MyInterfaceImpl<$T> $impl, + static void implementIn<$T extends jni.JObject>( + jni.JImplementer implementer, + $MyInterface<$T> $impl, ) { - final $p = ReceivePort(); - final $x = MyInterface.fromReference( - $impl.T, - ProtectedJniExtensions.newPortProxy( - r'com.github.dart_lang.jnigen.interfaces.MyInterface', - $p, - _$invokePointer, - ), - ).._$p = $p; - final $a = $p.sendPort.nativePort; - _$impls[$a] = $impl; - $p.listen(($m) { + late final RawReceivePort $p; + $p = RawReceivePort(($m) { if ($m == null) { _$impls.remove($p.sendPort.nativePort); $p.close(); @@ -4570,19 +4560,36 @@ class MyInterface<$T extends jni.JObject> extends jni.JObject { final $r = _$invokeMethod($p.sendPort.nativePort, $i); ProtectedJniExtensions.returnResult($i.result, $r); }); - return $x; + implementer.add( + r'com.github.dart_lang.jnigen.interfaces.MyInterface', + $p, + _$invokePointer, + ); + final $a = $p.sendPort.nativePort; + _$impls[$a] = $impl; } - static Map get $impls => _$impls; + + factory MyInterface.implement( + $MyInterface<$T> $impl, + ) { + final $i = jni.JImplementer(); + implementIn($i, $impl); + return MyInterface.fromReference( + $impl.T, + $i.implementReference(), + ); + } + static Map get $impls => _$impls; } -abstract interface class $MyInterfaceImpl<$T extends jni.JObject> { - factory $MyInterfaceImpl({ +abstract interface class $MyInterface<$T extends jni.JObject> { + factory $MyInterface({ required jni.JObjType<$T> T, required void Function(jni.JString s) voidCallback, required jni.JString Function(jni.JString s) stringCallback, required $T Function($T t) varCallback, required int Function(int a, bool b, int c, double d) manyPrimitives, - }) = _$MyInterfaceImpl; + }) = _$MyInterface; jni.JObjType<$T> get T; @@ -4592,9 +4599,8 @@ abstract interface class $MyInterfaceImpl<$T extends jni.JObject> { int manyPrimitives(int a, bool b, int c, double d); } -class _$MyInterfaceImpl<$T extends jni.JObject> - implements $MyInterfaceImpl<$T> { - _$MyInterfaceImpl({ +class _$MyInterface<$T extends jni.JObject> implements $MyInterface<$T> { + _$MyInterface({ required this.T, required void Function(jni.JString s) voidCallback, required jni.JString Function(jni.JString s) stringCallback, @@ -4886,9 +4892,7 @@ class MyRunnable extends jni.JObject { } /// Maps a specific port to the implemented interface. - static final Map _$impls = {}; - ReceivePort? _$p; - + static final Map _$impls = {}; static jni.JObjectPtr _$invoke( int port, jni.JObjectPtr descriptor, @@ -4907,8 +4911,8 @@ class MyRunnable extends jni.JObject { static final ffi.Pointer< ffi.NativeFunction< jni.JObjectPtr Function( - ffi.Uint64, jni.JObjectPtr, jni.JObjectPtr)>> - _$invokePointer = ffi.Pointer.fromFunction(_$invoke); + ffi.Int64, jni.JObjectPtr, jni.JObjectPtr)>> _$invokePointer = + ffi.Pointer.fromFunction(_$invoke); static ffi.Pointer _$invokeMethod( int $p, @@ -4927,20 +4931,12 @@ class MyRunnable extends jni.JObject { return jni.nullptr; } - factory MyRunnable.implement( - $MyRunnableImpl $impl, + static void implementIn( + jni.JImplementer implementer, + $MyRunnable $impl, ) { - final $p = ReceivePort(); - final $x = MyRunnable.fromReference( - ProtectedJniExtensions.newPortProxy( - r'com.github.dart_lang.jnigen.interfaces.MyRunnable', - $p, - _$invokePointer, - ), - ).._$p = $p; - final $a = $p.sendPort.nativePort; - _$impls[$a] = $impl; - $p.listen(($m) { + late final RawReceivePort $p; + $p = RawReceivePort(($m) { if ($m == null) { _$impls.remove($p.sendPort.nativePort); $p.close(); @@ -4950,20 +4946,37 @@ class MyRunnable extends jni.JObject { final $r = _$invokeMethod($p.sendPort.nativePort, $i); ProtectedJniExtensions.returnResult($i.result, $r); }); - return $x; + implementer.add( + r'com.github.dart_lang.jnigen.interfaces.MyRunnable', + $p, + _$invokePointer, + ); + final $a = $p.sendPort.nativePort; + _$impls[$a] = $impl; + } + + factory MyRunnable.implement( + $MyRunnable $impl, + ) { + final $i = jni.JImplementer(); + implementIn($i, $impl); + return MyRunnable.fromReference( + $i.implementReference(), + ); } + static Map get $impls => _$impls; } -abstract interface class $MyRunnableImpl { - factory $MyRunnableImpl({ +abstract interface class $MyRunnable { + factory $MyRunnable({ required void Function() run, - }) = _$MyRunnableImpl; + }) = _$MyRunnable; void run(); } -class _$MyRunnableImpl implements $MyRunnableImpl { - _$MyRunnableImpl({ +class _$MyRunnable implements $MyRunnable { + _$MyRunnable({ required void Function() run, }) : _run = run; @@ -5238,9 +5251,7 @@ class StringConverter extends jni.JObject { } /// Maps a specific port to the implemented interface. - static final Map _$impls = {}; - ReceivePort? _$p; - + static final Map _$impls = {}; static jni.JObjectPtr _$invoke( int port, jni.JObjectPtr descriptor, @@ -5259,8 +5270,8 @@ class StringConverter extends jni.JObject { static final ffi.Pointer< ffi.NativeFunction< jni.JObjectPtr Function( - ffi.Uint64, jni.JObjectPtr, jni.JObjectPtr)>> - _$invokePointer = ffi.Pointer.fromFunction(_$invoke); + ffi.Int64, jni.JObjectPtr, jni.JObjectPtr)>> _$invokePointer = + ffi.Pointer.fromFunction(_$invoke); static ffi.Pointer _$invokeMethod( int $p, @@ -5281,20 +5292,12 @@ class StringConverter extends jni.JObject { return jni.nullptr; } - factory StringConverter.implement( - $StringConverterImpl $impl, + static void implementIn( + jni.JImplementer implementer, + $StringConverter $impl, ) { - final $p = ReceivePort(); - final $x = StringConverter.fromReference( - ProtectedJniExtensions.newPortProxy( - r'com.github.dart_lang.jnigen.interfaces.StringConverter', - $p, - _$invokePointer, - ), - ).._$p = $p; - final $a = $p.sendPort.nativePort; - _$impls[$a] = $impl; - $p.listen(($m) { + late final RawReceivePort $p; + $p = RawReceivePort(($m) { if ($m == null) { _$impls.remove($p.sendPort.nativePort); $p.close(); @@ -5304,20 +5307,36 @@ class StringConverter extends jni.JObject { final $r = _$invokeMethod($p.sendPort.nativePort, $i); ProtectedJniExtensions.returnResult($i.result, $r); }); - return $x; + implementer.add( + r'com.github.dart_lang.jnigen.interfaces.StringConverter', + $p, + _$invokePointer, + ); + final $a = $p.sendPort.nativePort; + _$impls[$a] = $impl; + } + + factory StringConverter.implement( + $StringConverter $impl, + ) { + final $i = jni.JImplementer(); + implementIn($i, $impl); + return StringConverter.fromReference( + $i.implementReference(), + ); } } -abstract interface class $StringConverterImpl { - factory $StringConverterImpl({ +abstract interface class $StringConverter { + factory $StringConverter({ required int Function(jni.JString s) parseToInt, - }) = _$StringConverterImpl; + }) = _$StringConverter; int parseToInt(jni.JString s); } -class _$StringConverterImpl implements $StringConverterImpl { - _$StringConverterImpl({ +class _$StringConverter implements $StringConverter { + _$StringConverter({ required int Function(jni.JString s) parseToInt, }) : _parseToInt = parseToInt; @@ -5857,9 +5876,7 @@ class JsonSerializable extends jni.JObject { } /// Maps a specific port to the implemented interface. - static final Map _$impls = {}; - ReceivePort? _$p; - + static final Map _$impls = {}; static jni.JObjectPtr _$invoke( int port, jni.JObjectPtr descriptor, @@ -5878,8 +5895,8 @@ class JsonSerializable extends jni.JObject { static final ffi.Pointer< ffi.NativeFunction< jni.JObjectPtr Function( - ffi.Uint64, jni.JObjectPtr, jni.JObjectPtr)>> - _$invokePointer = ffi.Pointer.fromFunction(_$invoke); + ffi.Int64, jni.JObjectPtr, jni.JObjectPtr)>> _$invokePointer = + ffi.Pointer.fromFunction(_$invoke); static ffi.Pointer _$invokeMethod( int $p, @@ -5902,20 +5919,12 @@ class JsonSerializable extends jni.JObject { return jni.nullptr; } - factory JsonSerializable.implement( - $JsonSerializableImpl $impl, + static void implementIn( + jni.JImplementer implementer, + $JsonSerializable $impl, ) { - final $p = ReceivePort(); - final $x = JsonSerializable.fromReference( - ProtectedJniExtensions.newPortProxy( - r'com.github.dart_lang.jnigen.annotations.JsonSerializable', - $p, - _$invokePointer, - ), - ).._$p = $p; - final $a = $p.sendPort.nativePort; - _$impls[$a] = $impl; - $p.listen(($m) { + late final RawReceivePort $p; + $p = RawReceivePort(($m) { if ($m == null) { _$impls.remove($p.sendPort.nativePort); $p.close(); @@ -5925,20 +5934,36 @@ class JsonSerializable extends jni.JObject { final $r = _$invokeMethod($p.sendPort.nativePort, $i); ProtectedJniExtensions.returnResult($i.result, $r); }); - return $x; + implementer.add( + r'com.github.dart_lang.jnigen.annotations.JsonSerializable', + $p, + _$invokePointer, + ); + final $a = $p.sendPort.nativePort; + _$impls[$a] = $impl; + } + + factory JsonSerializable.implement( + $JsonSerializable $impl, + ) { + final $i = jni.JImplementer(); + implementIn($i, $impl); + return JsonSerializable.fromReference( + $i.implementReference(), + ); } } -abstract interface class $JsonSerializableImpl { - factory $JsonSerializableImpl({ +abstract interface class $JsonSerializable { + factory $JsonSerializable({ required JsonSerializable_Case Function() value, - }) = _$JsonSerializableImpl; + }) = _$JsonSerializable; JsonSerializable_Case value(); } -class _$JsonSerializableImpl implements $JsonSerializableImpl { - _$JsonSerializableImpl({ +class _$JsonSerializable implements $JsonSerializable { + _$JsonSerializable({ required JsonSerializable_Case Function() value, }) : _value = value; diff --git a/pkgs/jnigen/test/simple_package_test/generate.dart b/pkgs/jnigen/test/simple_package_test/generate.dart index 70eaf5f1b..587c2bec0 100644 --- a/pkgs/jnigen/test/simple_package_test/generate.dart +++ b/pkgs/jnigen/test/simple_package_test/generate.dart @@ -79,7 +79,10 @@ Config getConfig() { logLevel: Level.INFO, customClassBody: { 'com.github.dart_lang.jnigen.interfaces.MyInterface': r''' - static Map get $impls => _$impls; + static Map get $impls => _$impls; +''', + 'com.github.dart_lang.jnigen.interfaces.MyRunnable': r''' + static Map get $impls => _$impls; ''' }, outputConfig: OutputConfig( diff --git a/pkgs/jnigen/test/simple_package_test/runtime_test_registrant.dart b/pkgs/jnigen/test/simple_package_test/runtime_test_registrant.dart index f59bd24fd..6e6d36b5d 100644 --- a/pkgs/jnigen/test/simple_package_test/runtime_test_registrant.dart +++ b/pkgs/jnigen/test/simple_package_test/runtime_test_registrant.dart @@ -566,7 +566,7 @@ void registerTests(String groupName, TestRunnerCallback test) { // or `self` argument for each one of the callbacks. late final MyInterface myInterface; myInterface = MyInterface.implement( - $MyInterfaceImpl( + $MyInterface( voidCallback: voidCallbackResult.complete, stringCallback: (s) { return (s.toDartString(releaseOriginal: true) * 2).toJString(); @@ -617,7 +617,7 @@ void registerTests(String groupName, TestRunnerCallback test) { expect(manyPrimitives, -1 + 3 + 3.14.toInt() + 1); // Currently we have one implementation of the interface. - expect(MyInterface.$impls, hasLength(1)); + expect(MyInterface.$impls, hasLength(1), skip: Platform.isAndroid); myInterface.release(); if (!Platform.isAndroid) { // Running garbage collection does not work on Android. Skipping this @@ -634,6 +634,75 @@ void registerTests(String groupName, TestRunnerCallback test) { expect(MyInterface.$impls, isEmpty); } }); + test('implementing multiple interfaces', () async { + final implementer = JImplementer(); + MyInterface.implementIn( + implementer, + $MyInterface( + T: JString.type, + voidCallback: (s) {}, + stringCallback: (s) { + return s; + }, + varCallback: (t) { + return t; + }, + manyPrimitives: (a, b, c, d) => 42, + ), + ); + var runnableRan = false; + MyRunnable.implementIn( + implementer, + $MyRunnable( + run: () { + runnableRan = true; + }, + ), + ); + final runnable = implementer.implement(MyRunnable.type); + runnable.run(); + expect(runnableRan, isTrue); + final myInterface = runnable.castTo( + MyInterface.type(JString.type), + releaseOriginal: true, + ); + expect(myInterface.manyPrimitives(1, true, 3, 4), 42); + + expect(MyInterface.$impls, hasLength(1), skip: Platform.isAndroid); + expect(MyRunnable.$impls, hasLength(1), skip: Platform.isAndroid); + myInterface.release(); + if (!Platform.isAndroid) { + // Running garbage collection does not work on Android. Skipping this + // test for android. + _runJavaGC(); + for (var i = 0; i < 8; ++i) { + await Future.delayed(Duration(milliseconds: (1 << i) * 100)); + if (MyInterface.$impls.isEmpty) { + break; + } + } + // Since the interface is now deleted, the cleaner must signal to Dart + // to clean up. + expect(MyInterface.$impls, isEmpty); + expect(MyRunnable.$impls, isEmpty); + } + }); + test('Reuse implementation for multiple instances', () { + using((arena) { + final hexParser = + StringConverter.implement(DartStringToIntParser(radix: 16)) + ..releasedBy(arena); + final decimalParser = + StringConverter.implement(DartStringToIntParser(radix: 10)) + ..releasedBy(arena); + final fifteen = StringConverterConsumer.consumeOnSameThread( + hexParser, 'F'.toJString()..releasedBy(arena)); + expect(fifteen.intValue(releaseOriginal: true), 15); + final fortyTwo = StringConverterConsumer.consumeOnSameThread( + decimalParser, '42'.toJString()..releasedBy(arena)); + expect(fortyTwo.intValue(releaseOriginal: true), 42); + }); + }); } group('Dart exceptions are handled', () { for (final exception in [UnimplementedError(), 'Hello!']) { @@ -641,47 +710,53 @@ void registerTests(String groupName, TestRunnerCallback test) { test( 'on ${sameThread ? 'the same thread' : 'another thread'}' ' throwing $exception', () async { - final runnable = MyRunnable.implement( - $MyRunnableImpl( - run: () { - // ignore: only_throw_errors - throw exception; - }, - ), - ); - final runner = MyRunnableRunner(runnable); - if (sameThread) { - runner.runOnSameThread(); - } else { - runner.runOnAnotherThread(); - } - while (runner.error.isNull) { - await Future.delayed(const Duration(milliseconds: 100)); + await using((arena) async { + final runnable = MyRunnable.implement( + $MyRunnable( + run: () { + // ignore: only_throw_errors + throw exception; + }, + ), + )..releasedBy(arena); + final runner = MyRunnableRunner(runnable)..releasedBy(arena); + if (sameThread) { + runner.runOnSameThread(); + } else { + runner.runOnAnotherThread(); + } + while (runner.error.isNull) { + await Future.delayed(const Duration(milliseconds: 100)); + } + expect( + Jni.env.IsInstanceOf( + runner.error.reference.pointer, + JClass.forName( + 'java/lang/reflect/UndeclaredThrowableException') + .reference + .pointer, + ), + isTrue, + ); + final throwableClass = runner.error.jClass; + final cause = throwableClass + .instanceMethodId('getCause', '()Ljava/lang/Throwable;') + .call(runner.error, JObject.type, []); + expect( + Jni.env.IsInstanceOf( + cause.reference.pointer, + JClass.forName( + 'com/github/dart_lang/jni/PortProxyBuilder\$DartException') + .reference + .pointer, + ), + isTrue, + ); + expect(cause.toString(), contains(exception.toString())); + }); + if (!Platform.isAndroid) { + _runJavaGC(); } - expect( - Jni.env.IsInstanceOf( - runner.error.reference.pointer, - JClass.forName('java/lang/reflect/UndeclaredThrowableException') - .reference - .pointer, - ), - isTrue, - ); - final throwableClass = runner.error.jClass; - final cause = throwableClass - .instanceMethodId('getCause', '()Ljava/lang/Throwable;') - .call(runner.error, JObject.type, []); - expect( - Jni.env.IsInstanceOf( - cause.reference.pointer, - JClass.forName( - 'com/github/dart_lang/jni/PortProxy\$DartException') - .reference - .pointer, - ), - isTrue, - ); - expect(cause.toString(), contains(exception.toString())); }); } } @@ -693,8 +768,7 @@ void registerTests(String groupName, TestRunnerCallback test) { ('the same thread', StringConverterConsumer.consumeOnSameThread), ]) { test('StringConverter.implement on $threading ', () async { - final stringConverter = - StringConverter.implement($StringConverterImpl( + final stringConverter = StringConverter.implement($StringConverter( parseToInt: (s) { final value = int.tryParse(s.toDartString()); if (value == null) { @@ -815,3 +889,14 @@ void registerTests(String groupName, TestRunnerCallback test) { } }); } + +class DartStringToIntParser implements $StringConverter { + final int radix; + + DartStringToIntParser({required this.radix}); + + @override + int parseToInt(JString s) { + return int.parse(s.toDartString(releaseOriginal: true), radix: radix); + } +}