diff --git a/pkgs/jni/lib/jni.dart b/pkgs/jni/lib/jni.dart index 49ce91ddd8..e5a8d44eec 100644 --- a/pkgs/jni/lib/jni.dart +++ b/pkgs/jni/lib/jni.dart @@ -93,3 +93,4 @@ export 'src/lang/lang.dart'; export 'src/nio/nio.dart'; export 'src/types.dart' hide JTypeBase, lowestCommonSuperType; export 'src/util/util.dart'; +export 'src/converter.dart'; diff --git a/pkgs/jni/lib/src/converter.dart b/pkgs/jni/lib/src/converter.dart new file mode 100644 index 0000000000..7ca97dc94c --- /dev/null +++ b/pkgs/jni/lib/src/converter.dart @@ -0,0 +1,235 @@ +// 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 +// BSD-style license that can be found in the LICENSE file. + +import 'jarray.dart'; +import 'jni.dart'; +import 'jobject.dart'; +import 'lang/lang.dart'; +import 'types.dart'; +import 'util/util.dart'; + +JObject _defaultJNIConverter(Object o) => + throw UnimplementedError('No conversion for $o'); + +/// Converts a Dart object to a JNI object. +JObject? toJObject( + Object? dartObject, { + JObject Function(Object) convertOther = _defaultJNIConverter, +}) => + switch (dartObject) { + null => null, + JObject() => dartObject, + bool() => dartObject.toJBoolean(), + int() => dartObject.toJLong(), + double() => dartObject.toJDouble(), + String() => dartObject.toJString(), + List() => dartObject.toJList(convertOther: convertOther), + Set() => dartObject.toJSet(convertOther: convertOther), + Map() => dartObject.toJMap(convertOther: convertOther), + _ => convertOther(dartObject), + }; + +extension DartListToJList on List { + JList toJList({ + JObject Function(Object) convertOther = _defaultJNIConverter, + }) { + final list = JList.array(JObject.nullableType); + for (final element in this) { + list.add(toJObject(element, convertOther: convertOther)); + } + return list; + } +} + +extension DartSetToJSet on Set { + JSet toJSet({ + JObject Function(Object) convertOther = _defaultJNIConverter, + }) { + final set = JSet.hash(JObject.nullableType); + for (final element in this) { + set.add(toJObject(element, convertOther: convertOther)); + } + return set; + } +} + +extension DartMapToJMap on Map { + JMap toJMap({ + JObject Function(Object) convertOther = _defaultJNIConverter, + }) { + final map = JMap.hash(JObject.nullableType, JObject.nullableType); + for (final entry in entries) { + map[toJObject(entry.key, convertOther: convertOther)] = + toJObject(entry.value, convertOther: convertOther); + } + return map; + } +} + +extension DartListToJBooleanArray on List { + JBooleanArray toJBooleanArray() => + JBooleanArray(length)..setRange(0, length, this); +} + +extension DartListToJByteArray on List { + JByteArray toJByteArray() => JByteArray(length)..setRange(0, length, this); +} + +extension DartListToJCharArray on List { + JCharArray toJCharArray() => JCharArray(length)..setRange(0, length, this); +} + +extension DartListToJShortArray on List { + JShortArray toJShortArray() => JShortArray(length)..setRange(0, length, this); +} + +extension DartListToJIntArray on List { + JIntArray toJIntArray() => JIntArray(length)..setRange(0, length, this); +} + +extension DartListToJLongArray on List { + JLongArray toJLongArray() => JLongArray(length)..setRange(0, length, this); +} + +extension DartListToJFloatArray on List { + JFloatArray toJFloatArray() => JFloatArray(length)..setRange(0, length, this); +} + +extension DartListToJDoubleArray on List { + JDoubleArray toJDoubleArray() => + JDoubleArray(length)..setRange(0, length, this); +} + +extension DartListToJObjectArray on List { + JArray toJObjectArray(JType elementType) => + JArray.of(elementType, this); +} + +Object? _defaultDartConverter(JObject? o) => o; + +/// Converts a JNI object to a Dart object. +Object? toDartObject( + JObject? jObject, { + Object? Function(JObject?) convertOther = _defaultDartConverter, +}) { + if (jObject == null) return null; + + if (jObject.isA(JString.type)) { + return jObject.as(JString.type).toDartString(); + } + if (jObject.isA(JBoolean.type)) { + return jObject.as(JBoolean.type).booleanValue(); + } + if (jObject.isA(JInteger.type)) { + return jObject.as(JInteger.type).intValue(); + } + if (jObject.isA(JLong.type)) { + return jObject.as(JLong.type).longValue(); + } + if (jObject.isA(JDouble.type)) { + return jObject.as(JDouble.type).doubleValue(); + } + if (jObject.isA(JFloat.type)) { + return jObject.as(JFloat.type).floatValue(); + } + if (jObject.isA(JShort.type)) { + return jObject.as(JShort.type).shortValue(); + } + if (jObject.isA(JByte.type)) { + return jObject.as(JByte.type).byteValue(); + } + if (jObject.isA(JCharacter.type)) { + return jObject.as(JCharacter.type).charValue(); + } + + if (jObject.isA(JBooleanArray.type)) { + return jObject.as(JBooleanArray.type).toList(); + } + if (jObject.isA(JByteArray.type)) { + return jObject.as(JByteArray.type).toList(); + } + if (jObject.isA(JCharArray.type)) { + return jObject.as(JCharArray.type).toList(); + } + if (jObject.isA(JShortArray.type)) { + return jObject.as(JShortArray.type).toList(); + } + if (jObject.isA(JIntArray.type)) { + return jObject.as(JIntArray.type).toList(); + } + if (jObject.isA(JLongArray.type)) { + return jObject.as(JLongArray.type).toList(); + } + if (jObject.isA(JFloatArray.type)) { + return jObject.as(JFloatArray.type).toList(); + } + if (jObject.isA(JDoubleArray.type)) { + return jObject.as(JDoubleArray.type).toList(); + } + + if (jObject.isA(JArray.type(JObject.nullableType))) { + return jObject + .as(JArray.type(JObject.nullableType)) + .toDeepDartList(convertOther: convertOther); + } + + if (jObject.isA(JList.type(JObject.nullableType))) { + return jObject + .as(JList.type(JObject.nullableType)) + .toDeepDartList(convertOther: convertOther); + } + if (jObject.isA(JSet.type(JObject.nullableType))) { + return jObject + .as(JSet.type(JObject.nullableType)) + .toDeepDartSet(convertOther: convertOther); + } + if (jObject.isA(JMap.type(JObject.nullableType, JObject.nullableType))) { + return jObject + .as(JMap.type(JObject.nullableType, JObject.nullableType)) + .toDeepDartMap(convertOther: convertOther); + } + + return convertOther(jObject); +} + +Object? toNullableDartObject( + JObject? jObject, { + Object? Function(JObject?) convertOther = _defaultDartConverter, +}) => + toDartObject(jObject, convertOther: convertOther); + +extension JArrayToDartList on JArray { + List toDeepDartList({ + Object? Function(JObject?) convertOther = _defaultDartConverter, + }) => + map((e) => toDartObject(e, convertOther: convertOther)).toList(); +} + +extension JListToDartList on JList { + List toDeepDartList({ + Object? Function(JObject?) convertOther = _defaultDartConverter, + }) => + map((e) => toDartObject(e, convertOther: convertOther)).toList(); +} + +extension JSetToDartSet on JSet { + Set toDeepDartSet({ + Object? Function(JObject?) convertOther = _defaultDartConverter, + }) => + map((e) => toDartObject(e, convertOther: convertOther)).toSet(); +} + +extension JMapToDartMap on JMap { + Map toDeepDartMap({ + Object? Function(JObject?) convertOther = _defaultDartConverter, + }) => + Map.fromEntries( + entries.map( + (e) => MapEntry( + toDartObject(e.key, convertOther: convertOther), + toDartObject(e.value, convertOther: convertOther), + ), + ), + ); +} diff --git a/pkgs/jni/test/converter_test.dart b/pkgs/jni/test/converter_test.dart new file mode 100644 index 0000000000..283c104abb --- /dev/null +++ b/pkgs/jni/test/converter_test.dart @@ -0,0 +1,101 @@ +// 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 +// BSD-style license that can be found in the LICENSE file. + +import 'dart:io'; + +import 'package:jni/jni.dart'; +import 'package:test/test.dart'; + +import 'test_util/test_util.dart'; + +void main() { + if (!Platform.isAndroid) { + checkDylibIsUpToDate(); + spawnJvm(); + } + run(testRunner: test); +} + +void run({required TestRunnerCallback testRunner}) { + testRunner('toJObject basic types', () { + using((arena) { + expect(toJObject(null), isNull); + expect(toJObject(true)!.isA(JBoolean.type), isTrue); + expect(toJObject(1)!.isA(JLong.type), isTrue); + expect(toJObject(1.5)!.isA(JDouble.type), isTrue); + expect(toJObject('abc')!.isA(JString.type), isTrue); + }); + }); + + testRunner('toDartObject basic types', () { + using((arena) { + expect(toDartObject(null), isNull); + expect(toDartObject(true.toJBoolean()..releasedBy(arena)), true); + expect(toDartObject(1.toJLong()..releasedBy(arena)), 1); + expect(toDartObject(1.5.toJDouble()..releasedBy(arena)), 1.5); + expect(toDartObject('abc'.toJString()..releasedBy(arena)), 'abc'); + }); + }); + + testRunner('Deep conversion List', () { + using((arena) { + final list = [1, 'abc', true]; + final jList = toJObject(list)!..releasedBy(arena); + expect(jList.isA(JList.type(JObject.nullableType)), isTrue); + + final back = toDartObject(jList); + expect(back, isA()); + final backList = back as List; + expect(backList[0], 1); + expect(backList[1], 'abc'); + expect(backList[2], true); + }); + }); + + testRunner('Deep conversion Map', () { + using((arena) { + final map = {'a': 1, 'b': 2}; + final jMap = toJObject(map)!..releasedBy(arena); + expect(jMap.isA(JMap.type(JObject.nullableType, JObject.nullableType)), + isTrue); + + final back = toDartObject(jMap); + expect(back, isA()); + final backMap = back as Map; + expect(backMap['a'], 1); + expect(backMap['b'], 2); + }); + }); + + testRunner('Primitive arrays toJ*Array', () { + using((arena) { + final bools = [true, false].toJBooleanArray()..releasedBy(arena); + expect(bools.isA(JBooleanArray.type), isTrue); + expect(toDartObject(bools), [true, false]); + + final ints = [1, 2, 3].toJIntArray()..releasedBy(arena); + expect(ints.isA(JIntArray.type), isTrue); + expect(toDartObject(ints), [1, 2, 3]); + + final doubles = [1.5, 2.5].toJDoubleArray()..releasedBy(arena); + expect(doubles.isA(JDoubleArray.type), isTrue); + expect(toDartObject(doubles), [1.5, 2.5]); + }); + }); + + testRunner('Object array toJObjectArray', () { + using((arena) { + final strings = [ + 'a'.toJString()..releasedBy(arena), + 'b'.toJString()..releasedBy(arena) + ]; + final jArray = strings.toJObjectArray(JString.type)..releasedBy(arena); + // Signature for JArray is [Ljava/lang/String; + expect(jArray.isA(JArray.type(JString.type)), isTrue); + + final back = toDartObject(jArray); + expect(back, ['a', 'b']); + }); + }); +}