blob: 0698b84dc55ad7f7bfeb08160b67635d291708f1 [file]
// 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:ffi/ffi.dart';
import 'package:path/path.dart';
import 'jexceptions.dart';
import 'jobject.dart';
import 'jreference.dart';
import 'third_party/generated_bindings.dart';
import 'jvalues.dart';
import 'accessors.dart';
String _getLibraryFileName(String base) {
if (Platform.isLinux || Platform.isAndroid) {
return "lib$base.so";
} else if (Platform.isWindows) {
return "$base.dll";
} else if (Platform.isMacOS) {
return "lib$base.dylib";
} else {
throw UnsupportedError("cannot derive library name: unsupported platform");
}
}
/// Load Dart-JNI Helper library.
///
/// If path is provided, it's used to load the library.
/// Else just the platform-specific filename is passed to DynamicLibrary.open
DynamicLibrary _loadDartJniLibrary({String? dir, String baseName = "dartjni"}) {
final fileName = _getLibraryFileName(baseName);
final libPath = (dir != null) ? join(dir, fileName) : fileName;
try {
final dylib = DynamicLibrary.open(libPath);
return dylib;
} on Error {
throw HelperNotFoundException(libPath);
}
}
/// Utilities to spawn and manage JNI.
abstract class Jni {
static final DynamicLibrary _dylib = _loadDartJniLibrary(dir: _dylibDir);
static final JniBindings _bindings = JniBindings(_dylib);
static final _getJniEnvFn = _dylib.lookup<Void>('GetJniEnv');
static final _getJniContextFn = _dylib.lookup<Void>('GetJniContextPtr');
/// Store dylibDir if any was used.
static String? _dylibDir;
/// Sets the directory where dynamic libraries are looked for.
/// On dart standalone, call this in new isolate before doing
/// any JNI operation.
///
/// (The reason is that dylibs need to be loaded in every isolate.
/// On flutter it's done by library. On dart standalone we don't
/// know the library path.)
static void setDylibDir({required String dylibDir}) {
_dylibDir = dylibDir;
}
static void initDLApi() {
// Initializing DartApiDL used for Continuations.
final result = _bindings.InitDartApiDL(NativeApi.initializeApiDLData);
assert(result == 0);
}
/// 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.
///
/// [dylibDir] is path of the directory where the wrapper library is found.
/// This parameter needs to be passed manually on __Dart standalone target__,
/// since we have no reliable way to bundle it with the package.
///
/// [jvmOptions], [ignoreUnrecognized], & [jniVersion] are passed to the JVM.
/// Strings in [classPath], if any, are used to construct an additional
/// JVM option of the form "-Djava.class.path={paths}".
static void spawn({
String? dylibDir,
List<String> jvmOptions = const [],
List<String> classPath = const [],
bool ignoreUnrecognized = false,
int jniVersion = JniVersions.JNI_VERSION_1_6,
}) {
final status = spawnIfNotExists(
dylibDir: dylibDir,
jvmOptions: jvmOptions,
classPath: classPath,
ignoreUnrecognized: ignoreUnrecognized,
jniVersion: jniVersion,
);
if (status == false) {
throw JvmExistsException();
}
}
/// Same as [spawn] but if a JVM exists, returns silently instead of
/// throwing [JvmExistsException].
///
/// If the options are different than that of existing VM, the existing VM's
/// options will remain in effect.
static bool spawnIfNotExists({
String? dylibDir,
List<String> jvmOptions = const [],
List<String> classPath = const [],
bool ignoreUnrecognized = false,
int jniVersion = JniVersions.JNI_VERSION_1_6,
}) =>
using((arena) {
_dylibDir = dylibDir;
final jvmArgs = _createVMArgs(
options: jvmOptions,
classPath: classPath,
version: jniVersion,
dylibPath: dylibDir,
ignoreUnrecognized: ignoreUnrecognized,
allocator: arena,
);
final status = _bindings.SpawnJvm(jvmArgs);
if (status == JniErrorCode.JNI_OK) {
return true;
} else if (status == DART_JNI_SINGLETON_EXISTS) {
return false;
} else {
throw SpawnException.of(status);
}
});
static Pointer<JavaVMInitArgs> _createVMArgs({
List<String> options = const [],
List<String> classPath = const [],
String? dylibPath,
bool ignoreUnrecognized = false,
int version = JniVersions.JNI_VERSION_1_6,
required Allocator allocator,
}) {
final args = allocator<JavaVMInitArgs>();
if (options.isNotEmpty || classPath.isNotEmpty) {
final count = options.length +
(dylibPath != null ? 1 : 0) +
(classPath.isNotEmpty ? 1 : 0);
final optsPtr = (count != 0) ? allocator<JavaVMOption>(count) : nullptr;
args.ref.options = optsPtr;
for (int i = 0; i < options.length; i++) {
optsPtr.elementAt(i).ref.optionString =
options[i].toNativeChars(allocator);
}
if (dylibPath != null) {
optsPtr
.elementAt(count - 1 - (classPath.isNotEmpty ? 1 : 0))
.ref
.optionString =
"-Djava.library.path=$dylibPath".toNativeChars(allocator);
}
if (classPath.isNotEmpty) {
final classPathString = classPath.join(Platform.isWindows ? ';' : ":");
optsPtr.elementAt(count - 1).ref.optionString =
"-Djava.class.path=$classPathString".toNativeChars(allocator);
}
args.ref.nOptions = count;
}
args.ref.ignoreUnrecognized = ignoreUnrecognized ? 1 : 0;
args.ref.version = version;
return args;
}
/// Returns pointer to current JNI JavaVM instance
Pointer<JavaVM> getJavaVM() {
return _bindings.GetJavaVM();
}
/// Returns the instance of [GlobalJniEnvStruct], which is an abstraction over JNIEnv
/// without the same-thread restriction.
static Pointer<GlobalJniEnvStruct> _fetchGlobalEnv() {
final env = _bindings.GetGlobalEnv();
if (env == nullptr) {
throw NoJvmInstanceException();
}
return env;
}
/// Points to a process-wide shared instance of [GlobalJniEnv].
///
/// It provides an indirection over [JniEnv] so that it can be used from
/// any thread, and always returns global object references.
static final env = GlobalJniEnv(_fetchGlobalEnv());
static final accessors = JniAccessors(_bindings.GetAccessors());
/// Returns a new PortContinuation.
static JObjectPtr newPortContinuation(ReceivePort port) {
return _bindings.PortContinuation__ctor(port.sendPort.nativePort).object;
}
/// Returns current application context on Android.
static JObjectPtr getCachedApplicationContext() {
return _bindings.GetApplicationContext();
}
/// Returns current activity
static JObjectPtr getCurrentActivity() => _bindings.GetCurrentActivity();
/// Get the initial classLoader of the application.
///
/// This is especially useful on Android, where
/// JNI threads cannot access application classes using
/// the usual `JniEnv.FindClass` method.
static JObjectPtr getApplicationClassLoader() => _bindings.GetClassLoader();
/// Returns class reference found through system-specific mechanism
static JClassPtr findClass(String qualifiedName) => using((arena) {
final cls = accessors.getClass(qualifiedName.toNativeChars(arena));
return cls.checkedClassRef;
});
/// Returns class for [qualifiedName] found by platform-specific mechanism,
/// wrapped in a [JClass].
static JClass findJClass(String qualifiedName) =>
JClass.fromRef(findClass(qualifiedName));
/// Constructs an instance of class with given arguments.
///
/// Use it when one instance is needed, but the constructor or class aren't
/// required themselves.
static JObject newInstance(
String qualifiedName, String ctorSignature, List<dynamic> args) {
final cls = findJClass(qualifiedName);
final ctor = cls.getCtorID(ctorSignature);
final obj = cls.newInstance(ctor, args);
cls.delete();
return obj;
}
/// Converts passed arguments to JValue array.
///
/// long, bool, double and JObject types are converted out of the box.
/// Wrap values in types such as [JValueInt] to convert to other primitive
/// types such as `int`, `short` and `char`.
static Pointer<JValue> jvalues(List<dynamic> args,
{Allocator allocator = calloc}) {
return toJValues(args, allocator: allocator);
}
/// Returns the value of static field identified by [fieldName] & [signature].
///
/// See [JObject.getField] for more explanations about [callType] and [T].
static T retrieveStaticField<T>(
String className, String fieldName, String signature,
[int? callType]) {
final cls = findJClass(className);
final result = cls.getStaticFieldByName<T>(fieldName, signature, callType);
cls.delete();
return result;
}
/// Calls static method identified by [methodName] and [signature]
/// on [className] with [args] as and [callType].
///
/// For more explanation on [args] and [callType], see [JObject.getField]
/// and [JObject.callMethod] respectively.
static T invokeStaticMethod<T>(
String className, String methodName, String signature, List<dynamic> args,
[int? callType]) {
final cls = findJClass(className);
final result =
cls.callStaticMethodByName<T>(methodName, signature, args, callType);
cls.delete();
return result;
}
/// Delete all references in [objects].
static void deleteAll(List<JReference> objects) {
for (var object in objects) {
object.delete();
}
}
}
typedef _SetJniGettersNativeType = Void Function(Pointer<Void>, Pointer<Void>);
typedef _SetJniGettersDartType = void Function(Pointer<Void>, Pointer<Void>);
/// Extensions for use by `jnigen` generated code.
extension ProtectedJniExtensions on Jni {
static Pointer<T> Function<T extends NativeType>(String) initGeneratedLibrary(
String name) {
var path = _getLibraryFileName(name);
if (Jni._dylibDir != null) {
path = join(Jni._dylibDir!, path);
}
final dl = DynamicLibrary.open(path);
final setJniGetters =
dl.lookupFunction<_SetJniGettersNativeType, _SetJniGettersDartType>(
'setJniGetters');
setJniGetters(Jni._getJniContextFn, Jni._getJniEnvFn);
final lookup = dl.lookup;
return lookup;
}
}
extension AdditionalEnvMethods on GlobalJniEnv {
/// Convenience method for converting a [JStringPtr]
/// to dart string.
/// if [deleteOriginal] is specified, jstring passed will be deleted using
/// DeleteLocalRef.
String toDartString(JStringPtr jstringPtr, {bool deleteOriginal = false}) {
if (jstringPtr == nullptr) {
throw NullJStringException();
}
final chars = GetStringUTFChars(jstringPtr, nullptr);
if (chars == nullptr) {
throw InvalidJStringException(jstringPtr);
}
final result = chars.cast<Utf8>().toDartString();
ReleaseStringUTFChars(jstringPtr, chars);
if (deleteOriginal) {
DeleteGlobalRef(jstringPtr);
}
return result;
}
/// Return a new [JStringPtr] from contents of [s].
JStringPtr toJStringPtr(String s) => using((arena) {
final utf = s.toNativeUtf8().cast<Char>();
final result = NewStringUTF(utf);
malloc.free(utf);
return result;
});
/// Deletes all references in [refs].
void deleteAllRefs(List<JObjectPtr> refs) {
for (final ref in refs) {
DeleteGlobalRef(ref);
}
}
}
extension StringMethodsForJni on String {
/// Returns a Utf-8 encoded Pointer<Char> with contents same as this string.
Pointer<Char> toNativeChars([Allocator allocator = malloc]) {
return toNativeUtf8(allocator: allocator).cast<Char>();
}
}
extension CharPtrMethodsForJni on Pointer<Char> {
/// Same as calling `cast<Utf8>` followed by `toDartString`.
String toDartString() {
return cast<Utf8>().toDartString();
}
}