// 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 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 = []; 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())); expect(backToStr.release, throwsA(isA())); }); }); 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()), ); }); 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().having( (e) => e.toString(), 'toString()', contains('java/lang/Integer'))), ); }); testRunner('Disallow construction of null JObject', () { expect( () => JObject.fromReference(jNullReference), throwsA(isA()), ); }); }