blob: cc0c3a25fbb426e25dd4ae6edce9efad431f1c15 [file] [log] [blame]
// 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:io';
import 'dart:isolate';
import 'package:test/test.dart';
import 'package:jni/jni.dart';
import 'test_util/test_util.dart';
const maxLongInJava = 9223372036854775807;
void main() {
// Don't forget to initialize JNI.
if (!Platform.isAndroid) {
checkDylibIsUpToDate();
Jni.spawnIfNotExists(dylibDir: "build/jni_libs", jvmOptions: ["-Xmx128m"]);
}
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
// JObject 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 JObject", () {
// JClass wraps a local class reference, and
// provides convenience functions.
final longClass = Jni.findJClass("java/lang/Long");
// looks for a constructor with given signature.
// equivalently you can lookup a method with name <init>
final longCtor = longClass.getCtorID("(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 = longClass.newInstance(longCtor, [176]);
final intValue = long.callMethodByName<int>(
"intValue",
"()I",
[],
JniCallType.intType,
);
expect(intValue, equals(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 = Jni.findJClass("java/lang/Integer");
final result = integerClass.callStaticMethodByName<JString>(
"toHexString", "(I)Ljava/lang/String;", [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, equals("1f"));
// Also don't forget to release the class.
integerClass.release();
});
testRunner("Call method with null argument, expect exception", () {
final integerClass = Jni.findJClass("java/lang/Integer");
expect(
() => integerClass.callStaticMethodByName<int>(
"parseInt", "(Ljava/lang/String;)I", [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(() => Jni.findJClass("java/lang/NotExists"), throwsException);
});
}
// [callMethodByName] will be expensive if making same call many times.
// Use [getMethodID] to get a method ID and use it in subsequent calls.
testRunner("Example for using getMethodID", () {
final longClass = Jni.findJClass("java/lang/Long");
final bitCountMethod = longClass.getStaticMethodID("bitCount", "(J)I");
// Use newInstance if you want only one instance.
// It finds the class, gets constructor ID and constructs an instance.
final random = Jni.newInstance("java/util/Random", "()V", []);
// You don't need a [JClass] reference to get instance method IDs.
final nextIntMethod = random.getMethodID("nextInt", "(I)I");
for (int i = 0; i < 100; i++) {
int r = random.callMethod<int>(
nextIntMethod,
[JValueInt(256 * 256)],
JniCallType.intType,
);
int bits = 0;
final jbc = longClass.callStaticMethod<int>(
bitCountMethod,
[r],
JniCallType.intType,
);
while (r != 0) {
bits += r % 2;
r = (r / 2).floor();
}
expect(jbc, equals(bits));
}
random.release();
longClass.release();
});
// One-off invocation of static method in single call.
testRunner("invoke_", () {
final m = Jni.invokeStaticMethod<int>(
"java/lang/Short",
"compare",
"(SS)I",
[JValueShort(1234), JValueShort(1324)],
JniCallType.intType,
);
expect(m, equals(1234 - 1324));
});
testRunner("Java char from string", () {
final m = Jni.invokeStaticMethod<bool>("java/lang/Character", "isLowerCase",
"(C)Z", [JValueChar.fromString('X')]);
expect(m, isFalse);
});
// One-off access of static field in single call.
testRunner("Get static field directly", () {
final maxLong = Jni.retrieveStaticField<int>(
"java/lang/Short", "MAX_VALUE", "S", JniCallType.shortType);
expect(maxLong, equals(32767));
});
// Use [callStringMethod] if all you care about is a string result
testRunner("callStaticStringMethod", () {
final longClass = Jni.findJClass("java/lang/Long");
const n = 1223334444;
final strFromJava = longClass.callStaticMethodByName<String>(
"toOctalString", "(J)Ljava/lang/String;", [n]);
expect(strFromJava, equals(n.toRadixString(8)));
longClass.release();
});
// In [JObject], [JClass], and retrieve_/invoke_ methods
// you can also pass Dart strings, apart from range of types
// allowed by [Jni.jvalues].
// They will be converted automatically.
testRunner(
"Passing strings in arguments",
() {
final out = Jni.retrieveStaticField<JObject>(
"java/lang/System", "out", "Ljava/io/PrintStream;");
// uncomment next line to see output
// (\n because test runner prints first char at end of the line)
//out.callMethodByName<Null>(
// "println", "(Ljava/lang/Object;)V", ["\nWorks (Apparently)"]);
out.release();
},
);
testRunner("Passing strings in arguments 2", () {
final twelve = Jni.invokeStaticMethod<int>("java/lang/Byte", "parseByte",
"(Ljava/lang/String;)B", ["12"], JniCallType.byteType);
expect(twelve, equals(12));
});
// You can use() method on JObject for using once and deleting.
testRunner("use() method", () {
final randomInt = Jni.newInstance("java/util/Random", "()V", []).use(
(random) => random.callMethodByName<int>(
"nextInt",
"(I)I",
[JValueInt(15)],
JniCallType.intType,
),
);
expect(randomInt, lessThan(15));
});
// The JObject and JClass have NativeFinalizer. However, it's possible to
// explicitly use `Arena`.
testRunner('Using arena', () {
final objects = <JObject>[];
using((arena) {
final r = Jni.findJClass('java/util/Random')..releasedBy(arena);
final ctor = r.getCtorID("()V");
for (int i = 0; i < 10; i++) {
objects.add(r.newInstance(ctor, [])..releasedBy(arena));
}
});
for (var object in objects) {
expect(object.isReleased, isTrue);
}
});
testRunner("enums", () {
// Don't forget to escape $ in nested type names
final ordinal = Jni.retrieveStaticField<JObject>(
"java/net/Proxy\$Type", "HTTP", "Ljava/net/Proxy\$Type;")
.use(
(f) => f.callMethodByName<int>(
"ordinal",
"()I",
[],
JniCallType.intType,
),
);
expect(ordinal, equals(1));
});
testRunner("casting", () {
using((arena) {
final str = "hello".toJString()..releasedBy(arena);
final obj = str.castTo(JObject.type)..releasedBy(arena);
final backToStr = obj.castTo(JString.type);
expect(backToStr.toDartString(), str.toDartString());
final _ = backToStr.castTo(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) {
// On standalone target, make sure to call [setDylibDir] before accessing
// any JNI function in a new isolate.
//
// otherwise subsequent JNI calls will throw a "library not found" exception.
Jni.setDylibDir(dylibDir: "build/jni_libs");
final random = Jni.newInstance("java/util/Random", "()V", []);
final result = random.callMethodByName<int>(
"nextInt", "(I)I", [256], JniCallType.intType);
random.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 JniException", () {
expect(
() => Jni.invokeStaticMethod<int>(
"java/lang/Integer", "parseInt", "(Ljava/lang/String;)I", ["X"]),
throwsA(isA<JniException>()),
);
});
testRunner("Passing long integer values to JNI", () {
final maxLongStr = Jni.invokeStaticMethod<String>(
"java/lang/Long",
"toString",
"(J)Ljava/lang/String;",
[maxLongInJava],
);
expect(maxLongStr, equals('$maxLongInJava'));
});
testRunner('Returning long integers from JNI', () {
final maxLong = Jni.retrieveStaticField<int>(
"java/lang/Long",
"MAX_VALUE",
"J",
JniCallType.longType,
);
expect(maxLong, equals(maxLongInJava));
});
}