| // 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:meta/meta.dart' show internal; | |
| import 'package:path/path.dart'; | |
| import 'accessors.dart'; | |
| import 'errors.dart'; | |
| import 'jobject.dart'; | |
| import 'jreference.dart'; | |
| import 'third_party/generated_bindings.dart'; | |
| import 'types.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'); | |
| } | |
| } | |
| /// Loads dartjni helper library. | |
| DynamicLibrary _loadDartJniLibrary({String? dir, String baseName = 'dartjni'}) { | |
| var fileName = _getLibraryFileName(baseName); | |
| if (!Platform.isAndroid) { | |
| if (dir != null) { | |
| fileName = join(dir, fileName); | |
| } | |
| final file = File(fileName); | |
| if (!file.existsSync()) { | |
| throw HelperNotFoundError(fileName); | |
| } | |
| } | |
| try { | |
| return DynamicLibrary.open(fileName); | |
| } catch (_) { | |
| throw DynamicLibraryLoadError(fileName); | |
| } | |
| } | |
| /// Utilities to spawn and manage JNI. | |
| abstract final class Jni { | |
| static final DynamicLibrary _dylib = _loadDartJniLibrary(dir: _dylibDir); | |
| static final JniBindings _bindings = JniBindings(_dylib); | |
| /// Store dylibDir if any was used. | |
| static String _dylibDir = join('build', 'jni_libs'); | |
| /// 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}) { | |
| if (!Platform.isAndroid) { | |
| _dylibDir = dylibDir; | |
| } | |
| } | |
| /// Whether to capture the stack trace when an object is released. | |
| /// | |
| /// This is useful for debugging [DoubleReleaseError] and | |
| /// [UseAfterReleaseError]. | |
| /// | |
| /// Defaults to `false`. | |
| static bool get captureStackTraceOnRelease => | |
| _bindings.getCaptureStackTraceOnRelease() != 0; | |
| static set captureStackTraceOnRelease(bool value) => | |
| _bindings.setCaptureStackTraceOnRelease(value ? 1 : 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, | |
| JniVersions jniVersion = JniVersions.VERSION_1_6, | |
| }) { | |
| final status = spawnIfNotExists( | |
| dylibDir: dylibDir, | |
| jvmOptions: jvmOptions, | |
| classPath: classPath, | |
| ignoreUnrecognized: ignoreUnrecognized, | |
| jniVersion: jniVersion, | |
| ); | |
| if (status == false) { | |
| throw JniVmExistsError(); | |
| } | |
| } | |
| /// Same as [spawn] but if a JVM exists, returns silently instead of | |
| /// throwing [JniVmExistsError]. | |
| /// | |
| /// 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, | |
| JniVersions jniVersion = JniVersions.VERSION_1_6, | |
| }) => | |
| using((arena) { | |
| _dylibDir = 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.OK) { | |
| return true; | |
| } else if (status == JniErrorCode.SINGLETON_EXISTS) { | |
| return false; | |
| } else { | |
| throw JniError.of(status); | |
| } | |
| }); | |
| static Pointer<JavaVMInitArgs> _createVMArgs({ | |
| List<String> options = const [], | |
| List<String> classPath = const [], | |
| String? dylibPath, | |
| bool ignoreUnrecognized = false, | |
| JniVersions version = JniVersions.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 (var i = 0; i < options.length; i++) { | |
| (optsPtr + i).ref.optionString = options[i].toNativeChars(allocator); | |
| } | |
| if (dylibPath != null) { | |
| (optsPtr + count - 1 - (classPath.isNotEmpty ? 1 : 0)) | |
| .ref | |
| .optionString = | |
| '-Djava.library.path=$dylibPath'.toNativeChars(allocator); | |
| } | |
| if (classPath.isNotEmpty) { | |
| final classPathString = classPath.join(Platform.isWindows ? ';' : ':'); | |
| (optsPtr + count - 1).ref.optionString = | |
| '-Djava.class.path=$classPathString'.toNativeChars(allocator); | |
| } | |
| args.ref.nOptions = count; | |
| } | |
| args.ref.ignoreUnrecognized = ignoreUnrecognized ? 1 : 0; | |
| args.ref.version = version.value; | |
| return args; | |
| } | |
| /// Returns pointer to current JNI JavaVM instance | |
| Pointer<JavaVM> getJavaVM() { | |
| return _bindings.JniGetJavaVM(); | |
| } | |
| /// Finds the class from its [name]. | |
| /// | |
| /// Uses the correct class loader on Android. | |
| /// Prefer this over `Jni.env.FindClass`. | |
| static JClassPtr findClass(String name) { | |
| // TODO(https://github.com/dart-lang/native/issues/3174): Remove this hack. | |
| if (name.startsWith('L') && name.endsWith(';')) { | |
| name = name.substring(1, name.length - 1); | |
| } | |
| return using((arena) => _bindings.JniFindClass(name.toNativeChars(arena))) | |
| .checkedClassRef; | |
| } | |
| /// Throws an exception. | |
| @internal | |
| static void throwException(JThrowablePtr exception) { | |
| final details = _bindings.GetExceptionDetails(exception); | |
| final env = Jni.env; | |
| final message = env.toDartString(details.message); | |
| final stacktrace = env.toDartString(details.stacktrace); | |
| env.DeleteGlobalRef(details.message); | |
| env.DeleteGlobalRef(details.stacktrace); | |
| throw JThrowable.fromReference( | |
| JGlobalReference(exception), message, stacktrace); | |
| } | |
| /// 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 NoJvmInstanceError(); | |
| } | |
| 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. | |
| @internal | |
| static final env = GlobalJniEnv(_fetchGlobalEnv()); | |
| } | |
| /// Extensions for use by JNIgen generated code. | |
| @internal | |
| extension ProtectedJniExtensions on Jni { | |
| static bool _initialized = false; | |
| /// Initializes DartApiDL used for Continuations and interface implementation. | |
| static void ensureInitialized() { | |
| if (!_initialized) { | |
| assert(NativeApi.majorVersion == 2); | |
| assert(NativeApi.minorVersion >= 3); | |
| final result = Jni._bindings.InitDartApiDL(NativeApi.initializeApiDLData); | |
| _initialized = result == 0; | |
| assert(_initialized); | |
| } | |
| } | |
| static int getMainPortId() { | |
| return Jni._bindings.GetMainPortId(); | |
| } | |
| static int getCurrentIsolateId() { | |
| return Jni._bindings.GetCurrentIsolateId(); | |
| } | |
| static final _jThrowableClass = JClass.forName('java/lang/Throwable'); | |
| /// Returns a new DartException. | |
| static Pointer<Void> newDartException(Object exception) { | |
| JObjectPtr? cause; | |
| if (exception is JObject) { | |
| final exceptionRef = exception.reference; | |
| if (Jni.env.IsInstanceOf( | |
| exceptionRef.pointer, _jThrowableClass.reference.pointer)) { | |
| cause = exceptionRef.pointer; | |
| } | |
| } | |
| return Jni._bindings | |
| .DartException__ctor( | |
| Jni.env.toJStringPtr(exception.toString()), cause ?? nullptr) | |
| .objectPointer; | |
| } | |
| /// Returns a new PortContinuation. | |
| static JReference newPortContinuation(ReceivePort port) { | |
| ensureInitialized(); | |
| return JGlobalReference( | |
| Jni._bindings | |
| .PortContinuation__ctor(port.sendPort.nativePort) | |
| .objectPointer, | |
| ); | |
| } | |
| /// Returns the result of a callback. | |
| static void returnResult( | |
| Pointer<CallbackResult> result, JObjectPtr object) async { | |
| // The result is `nullptr` when the callback is a listener. | |
| if (result != nullptr) { | |
| Jni._bindings.resultFor(result, object); | |
| } | |
| } | |
| static Pointer<T> Function<T extends NativeType>(String) get lookup => | |
| Jni._dylib.lookup; | |
| } | |
| /// Used only inside `package:jni`. | |
| @internal | |
| extension InternalJniExtension on Jni { | |
| static Dart_FinalizableHandle newJObjectFinalizableHandle( | |
| Object object, | |
| Pointer<Void> reference, | |
| JObjectRefType refType, | |
| ) { | |
| ProtectedJniExtensions.ensureInitialized(); | |
| return Jni._bindings | |
| .newJObjectFinalizableHandle(object, reference, refType); | |
| } | |
| static Dart_FinalizableHandle newBooleanFinalizableHandle( | |
| Object object, | |
| Pointer<Bool> reference, | |
| ) { | |
| ProtectedJniExtensions.ensureInitialized(); | |
| return Jni._bindings.newBooleanFinalizableHandle(object, reference); | |
| } | |
| static Dart_FinalizableHandle newStackTraceFinalizableHandle( | |
| Object object, | |
| Pointer<Char> reference, | |
| ) { | |
| ProtectedJniExtensions.ensureInitialized(); | |
| return Jni._bindings.newStackTraceFinalizableHandle(object, reference); | |
| } | |
| static void deleteFinalizableHandle( | |
| Dart_FinalizableHandle finalizableHandle, Object object) { | |
| ProtectedJniExtensions.ensureInitialized(); | |
| Jni._bindings.deleteFinalizableHandle(finalizableHandle, object); | |
| } | |
| } | |
| extension AdditionalEnvMethods on GlobalJniEnv { | |
| /// Convenience method for converting a [JStringPtr] to dart string. | |
| /// if [releaseOriginal] is specified, jstring passed will be deleted using | |
| /// DeleteGlobalRef. | |
| String toDartString(JStringPtr jstringPtr, {bool releaseOriginal = false}) { | |
| if (jstringPtr == nullptr) { | |
| throw JNullError(); | |
| } | |
| final chars = GetStringChars(jstringPtr, nullptr); | |
| if (chars == nullptr) { | |
| throw ArgumentError('Not a valid jstring pointer.'); | |
| } | |
| final length = GetStringLength(jstringPtr); | |
| final result = chars.cast<Utf16>().toDartString(length: length); | |
| ReleaseStringChars(jstringPtr, chars); | |
| if (releaseOriginal) { | |
| DeleteGlobalRef(jstringPtr); | |
| } | |
| return result; | |
| } | |
| /// Returns a new [JStringPtr] from contents of [s]. | |
| JStringPtr toJStringPtr(String s) => using((arena) { | |
| final utf = s.toNativeUtf16(allocator: arena).cast<Uint16>(); | |
| final result = NewString(utf, s.length); | |
| if (utf == nullptr) { | |
| throw JniNewStringException(s); | |
| } | |
| return result; | |
| }); | |
| } | |
| @internal | |
| extension StringMethodsForJni on String { | |
| /// Returns a Utf-8 encoded `Pointer<Char>` with contents same as this string. | |
| Pointer<Char> toNativeChars(Allocator allocator) { | |
| return toNativeUtf8(allocator: allocator).cast<Char>(); | |
| } | |
| } |