blob: beac5f784939a5dad57ce2cbeaf5602474187c6c [file]
import 'dart:ffi';
import 'dart:io';
import 'package:ffi/ffi.dart';
import 'package:path/path.dart';
import 'third_party/jni_bindings_generated.dart';
import 'extensions.dart';
import 'jvalues.dart';
import 'jni_exceptions.dart';
import 'jni_object.dart';
import 'jni_class.dart';
part 'direct_methods_generated.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 "$base.framework/$base";
} 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 _loadJniHelpersLibrary(
{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);
}
}
/// Jni represents a single running JNI instance.
///
/// It provides convenience functions for looking up and invoking functions
/// without several FFI conversions.
///
/// You can also get access to instance of underlying JavaVM and JniEnv, and
/// then use them in a way similar to JNI C++ API.
class Jni {
final JniBindings _bindings;
Jni._(this._bindings);
static Jni? _instance;
/// Returns the existing Jni object.
///
/// If not running on Android and no Jni is spawned
/// using Jni.spawn(), throws an exception.
///
/// On Dart standalone, when calling for the first time from
/// a new isolate, make sure to pass the library path.
static Jni getInstance() {
if (_instance == null) {
final inst = Jni._(JniBindings(_loadJniHelpersLibrary()));
if (inst.getJavaVM() == nullptr) {
throw StateError("Fatal: No JVM associated with this process!"
" Did you call Jni.spawn?");
}
// If no error, save this singleton.
_instance = inst;
}
return _instance!;
}
/// Initialize instance from custom helper library path.
///
/// On dart standalone, call this in new isolate before
/// doing getInstance().
///
/// (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 load({required String helperDir}) {
if (_instance != null) {
throw StateError('Fatal: a JNI instance already exists in this isolate');
}
final inst = Jni._(JniBindings(_loadJniHelpersLibrary(dir: helperDir)));
if (inst.getJavaVM() == nullptr) {
throw StateError("Fatal: No JVM associated with this process");
}
_instance = inst;
}
/// Spawn an instance of JVM using JNI.
/// This instance will be returned by future calls to [getInstance]
///
/// [helperDir] 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 Jni spawn({
String? helperDir,
int logLevel = JniLogLevel.JNI_INFO,
List<String> jvmOptions = const [],
List<String> classPath = const [],
bool ignoreUnrecognized = false,
int jniVersion = JNI_VERSION_1_6,
}) {
if (_instance != null) {
throw UnsupportedError("Currently only 1 VM is supported.");
}
final dylib = _loadJniHelpersLibrary(dir: helperDir);
final inst = Jni._(JniBindings(dylib));
_instance = inst;
inst._bindings.SetJNILogging(logLevel);
final jArgs = _createVMArgs(
options: jvmOptions,
classPath: classPath,
version: jniVersion,
ignoreUnrecognized: ignoreUnrecognized,
);
inst._bindings.SpawnJvm(jArgs);
_freeVMArgs(jArgs);
return inst;
}
static Pointer<JavaVMInitArgs> _createVMArgs({
List<String> options = const [],
List<String> classPath = const [],
bool ignoreUnrecognized = false,
int version = JNI_VERSION_1_6,
}) {
final args = calloc<JavaVMInitArgs>();
if (options.isNotEmpty || classPath.isNotEmpty) {
final count = options.length + (classPath.isNotEmpty ? 1 : 0);
final optsPtr = (count != 0) ? calloc<JavaVMOption>(count) : nullptr;
args.ref.options = optsPtr;
for (int i = 0; i < options.length; i++) {
optsPtr.elementAt(i).ref.optionString = options[i].toNativeChars();
}
if (classPath.isNotEmpty) {
final classPathString = classPath.join(Platform.isWindows ? ';' : ":");
optsPtr.elementAt(count - 1).ref.optionString =
"-Djava.class.path=$classPathString".toNativeChars();
}
args.ref.nOptions = count;
}
args.ref.ignoreUnrecognized = ignoreUnrecognized ? 1 : 0;
args.ref.version = version;
return args;
}
static void _freeVMArgs(Pointer<JavaVMInitArgs> argPtr) {
final nOptions = argPtr.ref.nOptions;
final options = argPtr.ref.options;
if (nOptions != 0) {
for (var i = 0; i < nOptions; i++) {
calloc.free(options.elementAt(i).ref.optionString);
}
calloc.free(argPtr.ref.options);
}
calloc.free(argPtr);
}
/// Returns pointer to current JNI JavaVM instance
Pointer<JavaVM> getJavaVM() {
return _bindings.GetJavaVM();
}
/// Returns JniEnv* associated with current thread.
///
/// Do not reuse JniEnv between threads, it's only valid
/// in the thread it is obtained.
Pointer<JniEnv> getEnv() {
return _bindings.GetJniEnv();
}
void setJniLogging(int loggingLevel) {
_bindings.SetJNILogging(loggingLevel);
}
/// Returns current application context on Android.
JObject getCachedApplicationContext() {
return _bindings.GetApplicationContext();
}
/// Returns current activity
JObject getCurrentActivity() {
return _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.
JObject getApplicationClassLoader() {
return _bindings.GetClassLoader();
}
/// Returns class reference found through system-specific mechanism
JClass findClass(String qualifiedName) {
final nameChars = qualifiedName.toNativeChars();
final cls = _bindings.LoadClass(nameChars);
calloc.free(nameChars);
if (cls == nullptr) {
getEnv().checkException();
}
return cls;
}
/// Returns class for [qualifiedName] found by platform-specific mechanism,
/// wrapped in a `JniClass`.
JniClass findJniClass(String qualifiedName) {
return JniClass.of(getEnv(), findClass(qualifiedName));
}
/// Constructs an instance of class with given args.
///
/// Use it when you only need one instance, but not the actual class
/// nor any constructor / static methods.
JniObject newInstance(
String qualifiedName, String ctorSignature, List<dynamic> args) {
final cls = findJniClass(qualifiedName);
final ctor = cls.getMethodID("<init>", ctorSignature);
final obj = cls.newObject(ctor, args);
cls.delete();
return obj;
}
/// Wraps a JObject ref in a JniObject.
/// The original ref is stored in JniObject, and
/// deleted with the latter's [delete] method.
///
/// It takes the ownership of the jobject so that it can be used like this:
///
/// ```dart
/// final result = jni.wrap(long_expr_returning_jobject)
/// ```
JniObject wrap(JObject obj) {
return JniObject.of(getEnv(), obj, nullptr);
}
/// Wraps a JObject ref in a JniObject.
/// The original ref is stored in JniObject, and
/// deleted with the latter's [delete] method.
JniClass wrapClass(JClass cls) {
return JniClass.of(getEnv(), cls);
}
/// Converts passed arguments to JValue array
/// for use in methods that take arguments.
///
/// int, bool, double and JObject types are converted out of the box.
/// wrap values in types such as [JValueLong]
/// to convert to other primitive types instead.
static Pointer<JValue> jvalues(List<dynamic> args,
{Allocator allocator = calloc}) {
return toJValues(args, allocator: allocator);
}
}