// 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 jvmOptions = const [], List 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 jvmOptions = const [], List 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 _createVMArgs({ List options = const [], List classPath = const [], String? dylibPath, bool ignoreUnrecognized = false, JniVersions version = JniVersions.VERSION_1_6, required Allocator allocator, }) { final args = allocator(); if (options.isNotEmpty || classPath.isNotEmpty) { final count = options.length + (dylibPath != null ? 1 : 0) + (classPath.isNotEmpty ? 1 : 0); final optsPtr = (count != 0) ? allocator(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 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 _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 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 result, JObjectPtr object) async { // The result is `nullptr` when the callback is a listener. if (result != nullptr) { Jni._bindings.resultFor(result, object); } } static Pointer Function(String) get lookup => Jni._dylib.lookup; } /// Used only inside `package:jni`. @internal extension InternalJniExtension on Jni { static Dart_FinalizableHandle newJObjectFinalizableHandle( Object object, Pointer reference, JObjectRefType refType, ) { ProtectedJniExtensions.ensureInitialized(); return Jni._bindings .newJObjectFinalizableHandle(object, reference, refType); } static Dart_FinalizableHandle newBooleanFinalizableHandle( Object object, Pointer reference, ) { ProtectedJniExtensions.ensureInitialized(); return Jni._bindings.newBooleanFinalizableHandle(object, reference); } static Dart_FinalizableHandle newStackTraceFinalizableHandle( Object object, Pointer 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().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(); 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` with contents same as this string. Pointer toNativeChars(Allocator allocator) { return toNativeUtf8(allocator: allocator).cast(); } }