| // 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 | |
| // BSD-style license that can be found in the LICENSE file. | |
| import 'dart:ffi'; | |
| import 'dart:io'; | |
| import 'dart:isolate'; | |
| import 'package:jni/jni.dart'; | |
| import 'package:test/test.dart'; | |
| import 'test_util/test_util.dart'; | |
| const maxLongInJava = 9223372036854775807; | |
| void main() { | |
| // Don't forget to initialize JNI. | |
| if (!Platform.isAndroid) { | |
| checkDylibIsUpToDate(); | |
| spawnJvm(); | |
| } | |
| run(testRunner: test); | |
| } | |
| void run({required TestRunnerCallback testRunner}) { | |
| // The API based on JniEnv is intended to closely mimic C API of JNI, | |
| // And thus can be too verbose for simple experimenting and one-off uses | |
| // JClass API provides an easier way to perform some common operations. | |
| // | |
| // However, if binding generation using JNIgen is possible, that should be | |
| // the first choice. | |
| testRunner('Long.intValue() using JClass', () { | |
| // JClass wraps a local class reference, and | |
| // provides convenience functions. | |
| final longClass = JClass.forName('java/lang/Long'); | |
| // Looks for a constructor with given signature. | |
| // equivalently you can lookup a method with name <init> | |
| final longCtor = longClass.constructorId('(J)V'); | |
| // Note that the arguments are just passed as a list. | |
| // Allowed argument types are primitive types, JObject and its subclasses, | |
| // and raw JNI references (JObject). Strings will be automatically converted | |
| // to JNI strings. | |
| final long = longCtor(longClass, [176]); | |
| final intValueMethod = longClass.instanceMethodId('intValue', '()I'); | |
| final intValue = intValueMethod( | |
| long, | |
| jint.type, | |
| [], | |
| ); | |
| expect(intValue, 176); | |
| // Release any JObject and JClass instances using `.release()` after use. | |
| // This is not strictly required since JNI objects / classes have | |
| // a [NativeFinalizer]. But deleting them after use is a good practice. | |
| long.release(); | |
| longClass.release(); | |
| }); | |
| testRunner('call a static method using JClass APIs', () { | |
| final integerClass = JClass.forName('java/lang/Integer'); | |
| final result = | |
| integerClass.staticMethodId('toHexString', '(I)Ljava/lang/String;')( | |
| integerClass, JString.type, [JValueInt(31)]); | |
| // If the object is supposed to be a Java string you can call | |
| // [toDartString] on it. | |
| final resultString = result.toDartString(); | |
| // Dart string is a copy, original object can be released. | |
| result.release(); | |
| expect(resultString, '1f'); | |
| // Also don't forget to release the class. | |
| integerClass.release(); | |
| }); | |
| testRunner('Call method with null argument, expect exception', () { | |
| final integerClass = JClass.forName('java/lang/Integer'); | |
| expect( | |
| () => integerClass.staticMethodId('parseInt', '(Ljava/lang/String;)I')( | |
| integerClass, jint.type, [nullptr]), | |
| throwsException); | |
| integerClass.release(); | |
| }); | |
| // Skip this test on Android integration test because it fails there, possibly | |
| // due to a CheckJNI precondition check. | |
| if (!Platform.isAndroid) { | |
| testRunner('Try to find a non-exisiting class, expect exception', () { | |
| expect(() => JClass.forName('java/lang/NotExists'), throwsException); | |
| }); | |
| } | |
| testRunner('Example for using methods', () { | |
| final longClass = JClass.forName('java/lang/Long'); | |
| final bitCountMethod = longClass.staticMethodId('bitCount', '(J)I'); | |
| final randomClass = JClass.forName('java/util/Random'); | |
| final random = randomClass.constructorId('()V').call(randomClass, []); | |
| final nextIntMethod = randomClass.instanceMethodId('nextInt', '(I)I'); | |
| for (var i = 0; i < 100; i++) { | |
| var r = nextIntMethod( | |
| random, | |
| jint.type, | |
| [JValueInt(256 * 256)], | |
| ); | |
| var bits = 0; | |
| final jbc = bitCountMethod( | |
| longClass, | |
| jint.type, | |
| [r], | |
| ); | |
| while (r != 0) { | |
| bits += r % 2; | |
| r = (r / 2).floor(); | |
| } | |
| expect(jbc, bits); | |
| } | |
| random.release(); | |
| longClass.release(); | |
| }); | |
| testRunner('Static method with multiple args', () { | |
| final shortClass = JShort.type.jClass; | |
| final m = shortClass.staticMethodId('compare', '(SS)I').call( | |
| shortClass, | |
| jint.type, | |
| [JValueShort(1234), JValueShort(1324)], | |
| ); | |
| expect(m, 1234 - 1324); | |
| shortClass.release(); | |
| }); | |
| testRunner('Get static field', () { | |
| final shortClass = JShort.type.jClass; | |
| final maxLong = | |
| shortClass.staticFieldId('MAX_VALUE', 'S').get(shortClass, jshort.type); | |
| expect(maxLong, 32767); | |
| shortClass.release(); | |
| }); | |
| testRunner('Call static method on Long', () { | |
| final longClass = JClass.forName('java/lang/Long'); | |
| const n = 1223334444; | |
| final strFromJava = longClass | |
| .staticMethodId('toOctalString', '(J)Ljava/lang/String;') | |
| .call(longClass, JString.type, [n]); | |
| expect(strFromJava.toDartString(releaseOriginal: true), n.toRadixString(8)); | |
| longClass.release(); | |
| }); | |
| testRunner('Passing strings in arguments', () { | |
| final byteClass = JByte.type.jClass; | |
| final parseByte = | |
| byteClass.staticMethodId('parseByte', '(Ljava/lang/String;)B'); | |
| final twelve = parseByte(byteClass, jbyte.type, ['12'.toJString()]); | |
| expect(twelve, 12); | |
| byteClass.release(); | |
| }); | |
| // You can use() method on JObject for using once and deleting. | |
| testRunner('use() method', () { | |
| final randomInt = JClass.forName('java/util/Random').use((randomClass) { | |
| return randomClass | |
| .constructorId('()V') | |
| .call(randomClass, []).use((random) { | |
| return randomClass | |
| .instanceMethodId('nextInt', '(I)I') | |
| .call(random, jint.type, [JValueInt(15)]); | |
| }); | |
| }); | |
| expect(randomInt, lessThan(15)); | |
| const JObject? nullableJObject = null; | |
| expect(nullableJObject.use((_) => 'foo'), equals('foo')); | |
| }); | |
| // The JObject and JClass have NativeFinalizer. However, it's possible to | |
| // explicitly use `Arena`. | |
| testRunner('Using arena', () { | |
| final objects = <JObject>[]; | |
| using((arena) { | |
| final randomClass = JClass.forName('java/util/Random')..releasedBy(arena); | |
| final constructor = randomClass.constructorId('()V'); | |
| for (var i = 0; i < 10; i++) { | |
| objects.add(constructor(randomClass, [])..releasedBy(arena)); | |
| } | |
| }); | |
| for (var object in objects) { | |
| expect(object.isReleased, isTrue); | |
| } | |
| }); | |
| testRunner('enums', () { | |
| // Don't forget to escape $ in nested type names | |
| final proxyTypeClass = JClass.forName('java/net/Proxy\$Type'); | |
| final ordinal = proxyTypeClass | |
| .staticFieldId('HTTP', 'Ljava/net/Proxy\$Type;') | |
| .get(proxyTypeClass, JObject.type) | |
| .use( | |
| (http) => proxyTypeClass | |
| .instanceMethodId('ordinal', '()I') | |
| .call(http, jint.type, []), | |
| ); | |
| expect(ordinal, 1); | |
| proxyTypeClass.release(); | |
| }); | |
| testRunner('casting', () { | |
| using((arena) { | |
| final str = 'hello'.toJString()..releasedBy(arena); | |
| final obj = str.as(JObject.type)..releasedBy(arena); | |
| final backToStr = obj.as(JString.type); | |
| expect(backToStr.toDartString(), str.toDartString()); | |
| final _ = backToStr.as(JObject.type, releaseOriginal: true) | |
| ..releasedBy(arena); | |
| expect(backToStr.toDartString, throwsA(isA<UseAfterReleaseError>())); | |
| expect(backToStr.release, throwsA(isA<DoubleReleaseError>())); | |
| }); | |
| }); | |
| testRunner('Isolate', () async { | |
| final receivePort = ReceivePort(); | |
| await Isolate.spawn((sendPort) { | |
| final randomClass = JClass.forName('java/util/Random'); | |
| final random = randomClass.constructorId('()V').call(randomClass, []); | |
| final result = randomClass | |
| .instanceMethodId('nextInt', '(I)I') | |
| .call(random, jint.type, [256]); | |
| random.release(); | |
| randomClass.release(); | |
| // A workaround for `--pause-isolates-on-exit`. Otherwise getting test | |
| // with coverage pauses indefinitely here. | |
| // https://github.com/dart-lang/coverage/issues/472 | |
| sendPort.send(result); | |
| Isolate.current.kill(); | |
| }, receivePort.sendPort); | |
| final random = await receivePort.first as int; | |
| expect(random, greaterThanOrEqualTo(0)); | |
| expect(random, lessThan(256)); | |
| }); | |
| testRunner('Methods rethrow exceptions in Java as JThrowable', () { | |
| expect( | |
| () { | |
| final integerClass = JInteger.type.jClass; | |
| return JClass.forName('java/lang/Integer') | |
| .staticMethodId('parseInt', '(Ljava/lang/String;)I') | |
| .call(integerClass, jint.type, ['X'.toJString()]); | |
| }, | |
| throwsA(isA<JThrowable>()), | |
| ); | |
| }); | |
| testRunner('Passing long integer values to JNI', () { | |
| final longClass = JLong.type.jClass; | |
| final maxLongStr = longClass | |
| .staticMethodId( | |
| 'toString', | |
| '(J)Ljava/lang/String;', | |
| ) | |
| .call(longClass, JString.type, [maxLongInJava]); | |
| expect(maxLongStr.toDartString(), '$maxLongInJava'); | |
| longClass.release(); | |
| maxLongStr.release(); | |
| }); | |
| testRunner('Returning long integers from JNI', () { | |
| final longClass = JLong.type.jClass; | |
| final maxLong = | |
| longClass.staticFieldId('MAX_VALUE', 'J').get(longClass, jlong.type); | |
| expect(maxLong, maxLongInJava); | |
| }); | |
| testRunner('isA returns true', () { | |
| final long = JLong(1); | |
| expect(long.isA(JLong.type), isTrue); | |
| expect(long.isA(JNumber.type), isTrue); | |
| expect(long.isA(JObject.type), isTrue); | |
| }); | |
| testRunner('isA returns false', () { | |
| final long = JLong(1); | |
| expect(long.isA(JInteger.type), isFalse); | |
| expect(long.isA(JString.type), isFalse); | |
| }); | |
| testRunner('Casting correctly succeeds', () { | |
| final long = JLong(1); | |
| final long2 = long.as(JLong.type, releaseOriginal: true); | |
| expect(long2.toDartInt(releaseOriginal: true), 1); | |
| }); | |
| testRunner('Casting incorrectly fails', () { | |
| final long = JLong(1); | |
| expect( | |
| () => long.as(JInteger.type, releaseOriginal: true), | |
| throwsA(isA<CastError>().having( | |
| (e) => e.toString(), 'toString()', contains('java/lang/Integer'))), | |
| ); | |
| }); | |
| testRunner('Disallow construction of null JObject', () { | |
| expect( | |
| () => JObject.fromReference(jNullReference), | |
| throwsA(isA<JNullError>()), | |
| ); | |
| }); | |
| } |