diff options
Diffstat (limited to 'src/corelib/kernel/qjnienvironment.cpp')
-rw-r--r-- | src/corelib/kernel/qjnienvironment.cpp | 538 |
1 files changed, 538 insertions, 0 deletions
diff --git a/src/corelib/kernel/qjnienvironment.cpp b/src/corelib/kernel/qjnienvironment.cpp new file mode 100644 index 0000000000..1e2826e76b --- /dev/null +++ b/src/corelib/kernel/qjnienvironment.cpp @@ -0,0 +1,538 @@ +// Copyright (C) 2021 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#include "qjnienvironment.h" +#include "qjnihelpers_p.h" + +#include <QtCore/QThread> +#include <QtCore/QThreadStorage> + +QT_BEGIN_NAMESPACE + +/*! + \class QJniEnvironment + \inmodule QtCore + \since 6.1 + \brief The QJniEnvironment class provides access to the JNI Environment (JNIEnv). + + When using JNI, the \l {JNI tips: JavaVM and JNIEnv}{JNIEnv} class is a pointer to a function + table and a member function for each JNI function that indirects through the table. \c JNIEnv + provides most of the JNI functions. Every C++ native function receives a \c JNIEnv as the first + argument. The JNI environment cannot be shared between threads. + + Since \c JNIEnv doesn't do much error checking, such as exception checking and clearing, + QJniEnvironment allows you to do that easily. + + For more information about JNIEnv, see \l {Java: Interface Function Table}. + + \note This API has been designed and tested for use with Android. + It has not been tested for other platforms. +*/ + +class QJniEnvironmentPrivate +{ +public: + JNIEnv *jniEnv = nullptr; +}; + +class QJniEnvironmentPrivateTLS +{ +public: + inline ~QJniEnvironmentPrivateTLS() + { + QtAndroidPrivate::javaVM()->DetachCurrentThread(); + } +}; + +Q_GLOBAL_STATIC(QThreadStorage<QJniEnvironmentPrivateTLS *>, jniEnvTLS) + + +/*! + Constructs a new JNI Environment object and attaches the current thread to the Java VM. +*/ +QJniEnvironment::QJniEnvironment() + : d(new QJniEnvironmentPrivate{}) +{ + d->jniEnv = getJniEnv(); +} + +/*! + Returns the JNIEnv pointer for the current thread. + + The current thread will be attached to the Java VM. +*/ +JNIEnv *QJniEnvironment::getJniEnv() +{ + JNIEnv *jniEnv = nullptr; + + JavaVM *vm = QtAndroidPrivate::javaVM(); + const jint ret = vm->GetEnv((void**)&jniEnv, JNI_VERSION_1_6); + + if (ret == JNI_EDETACHED) { // We need to (re-)attach + const QByteArray threadName = QThread::currentThread()->objectName().toUtf8(); + JavaVMAttachArgs args = { JNI_VERSION_1_6, + threadName.isEmpty() ? "QtThread" : threadName.constData(), + nullptr + }; + if (vm->AttachCurrentThread(&jniEnv, &args) == JNI_OK) { + if (!jniEnvTLS->hasLocalData()) // If we attached the thread we own it. + jniEnvTLS->setLocalData(new QJniEnvironmentPrivateTLS); + } + } + return jniEnv; +} + +/*! + \fn QJniEnvironment::~QJniEnvironment() + + Detaches the current thread from the Java VM and destroys the QJniEnvironment object. + This will clear any pending exception by calling checkAndClearExceptions(). +*/ +QJniEnvironment::~QJniEnvironment() +{ + checkAndClearExceptions(); +} + +/*! + Returns \c true if this instance holds a valid JNIEnv object. + + \since 6.2 +*/ +bool QJniEnvironment::isValid() const +{ + return d->jniEnv; +} + +/*! + Provides access to the JNI Environment's \c JNIEnv pointer. +*/ +JNIEnv *QJniEnvironment::operator->() const +{ + return d->jniEnv; +} + +/*! + Returns the JNI Environment's \c JNIEnv object. +*/ +JNIEnv &QJniEnvironment::operator*() const +{ + return *d->jniEnv; +} + +/*! + Returns the JNI Environment's \c JNIEnv pointer. +*/ +JNIEnv *QJniEnvironment::jniEnv() const +{ + return d->jniEnv; +} + +/*! + Searches for \a className using all available class loaders. Qt on Android + uses a custom class loader to load all the .jar files and it must be used + to find any classes that are created by that class loader because these + classes are not visible when using the default class loader. + + Returns the class pointer or null if \a className is not found. + + A use case for this function is searching for a class to call a JNI method + that takes a \c jclass. This can be useful when doing multiple JNI calls on + the same class object which can a bit faster than using a class name in each + call. Additionally, this call looks for internally cached classes first before + doing a JNI call, and returns such a class if found. The following code snippet + creates an instance of the class \c CustomClass and then calls the + \c printFromJava() method: + + \code + QJniEnvironment env; + jclass javaClass = env.findClass("org/qtproject/example/android/CustomClass"); + QJniObject javaMessage = QJniObject::fromString("findClass example"); + QJniObject::callStaticMethod<void>(javaClass, "printFromJava", + "(Ljava/lang/String;)V", javaMessage.object<jstring>()); + \endcode + + \note This call returns a global reference to the class object from the + internally cached classes. +*/ +jclass QJniEnvironment::findClass(const char *className) +{ + return QtAndroidPrivate::findClass(className, d->jniEnv); +} + +/*! + Searches for an instance method of a class \a clazz. The method is specified + by its \a methodName and \a signature. + + Returns the method ID or \c nullptr if the method is not found. + + A usecase for this method is searching for class methods and caching their + IDs, so that they could later be used for calling the methods. + + \since 6.2 +*/ +jmethodID QJniEnvironment::findMethod(jclass clazz, const char *methodName, const char *signature) +{ + if (clazz) { + jmethodID id = d->jniEnv->GetMethodID(clazz, methodName, signature); + if (!checkAndClearExceptions(d->jniEnv)) + return id; + } + + return nullptr; +} + +/*! + \fn template<typename ...Args> jmethodId QJniEnvironment::findMethod(jclass clazz, const char *methodName) + \since 6.4 + + Searches for an instance method of a class \a clazz. The method is specified + by its \a methodName, the signature is deduced from the template parameters. + + Returns the method ID or \c nullptr if the method is not found. +*/ + +/*! + Searches for a static method of a class \a clazz. The method is specified + by its \a methodName and \a signature. + + Returns the method ID or \c nullptr if the method is not found. + + A usecase for this method is searching for class methods and caching their + IDs, so that they could later be used for calling the methods. + + \code + QJniEnvironment env; + jclass javaClass = env.findClass("org/qtproject/example/android/CustomClass"); + jmethodID methodId = env.findStaticMethod(javaClass, + "staticJavaMethod", + "(Ljava/lang/String;)V"); + QJniObject javaMessage = QJniObject::fromString("findStaticMethod example"); + QJniObject::callStaticMethod<void>(javaClass, + methodId, + javaMessage.object<jstring>()); + \endcode + + \since 6.2 +*/ +jmethodID QJniEnvironment::findStaticMethod(jclass clazz, const char *methodName, const char *signature) +{ + if (clazz) { + jmethodID id = d->jniEnv->GetStaticMethodID(clazz, methodName, signature); + if (!checkAndClearExceptions(d->jniEnv)) + return id; + } + + return nullptr; +} + +/*! + \fn template<typename ...Args> jmethodId QJniEnvironment::findStaticMethod(jclass clazz, const char *methodName) + \since 6.4 + + Searches for an instance method of a class \a clazz. The method is specified + by its \a methodName, the signature is deduced from the template parameters. + + Returns the method ID or \c nullptr if the method is not found. + + \code + QJniEnvironment env; + jclass javaClass = env.findClass("org/qtproject/example/android/CustomClass"); + jmethodID methodId = env.findStaticMethod<void, jstring>(javaClass, "staticJavaMethod"); + QJniObject javaMessage = QJniObject::fromString("findStaticMethod example"); + QJniObject::callStaticMethod<void>(javaClass, + methodId, + javaMessage.object<jstring>()); + \endcode +*/ + +/*! + Searches for a member field of a class \a clazz. The field is specified + by its \a fieldName and \a signature. + + Returns the field ID or \c nullptr if the field is not found. + + A usecase for this method is searching for class fields and caching their + IDs, so that they could later be used for getting/setting the fields. + + \since 6.2 +*/ +jfieldID QJniEnvironment::findField(jclass clazz, const char *fieldName, const char *signature) +{ + if (clazz) { + jfieldID id = d->jniEnv->GetFieldID(clazz, fieldName, signature); + if (!checkAndClearExceptions()) + return id; + } + + return nullptr; +} + +/*! + \fn template<typename T> jfieldID QJniEnvironment::findField(jclass clazz, const char *fieldName) + \since 6.4 + + Searches for a member field of a class \a clazz. The field is specified + by its \a fieldName. The signature of the field is deduced from the template parameter. + + Returns the field ID or \c nullptr if the field is not found. +*/ + +/*! + Searches for a static field of a class \a clazz. The field is specified + by its \a fieldName and \a signature. + + Returns the field ID or \c nullptr if the field is not found. + + A usecase for this method is searching for class fields and caching their + IDs, so that they could later be used for getting/setting the fields. + + \since 6.2 +*/ +jfieldID QJniEnvironment::findStaticField(jclass clazz, const char *fieldName, const char *signature) +{ + if (clazz) { + jfieldID id = d->jniEnv->GetStaticFieldID(clazz, fieldName, signature); + if (!checkAndClearExceptions()) + return id; + } + + return nullptr; +} + +/*! + \fn template<typename T> jfieldID QJniEnvironment::findStaticField(jclass clazz, const char *fieldName) + \since 6.4 + + Searches for a static field of a class \a clazz. The field is specified + by its \a fieldName. The signature of the field is deduced from the template parameter. + + Returns the field ID or \c nullptr if the field is not found. +*/ + +/*! + Returns the Java VM interface for the current process. Although it might + be possible to have multiple Java VMs per process, Android allows only one. + +*/ +JavaVM *QJniEnvironment::javaVM() +{ + return QtAndroidPrivate::javaVM(); +} + +/*! + Registers the Java methods in the array \a methods of size \a size, each of + which can call native C++ functions from class \a className. These methods + must be registered before any attempt to call them. + + Returns \c true if the registration is successful, otherwise \c false. + + Each element in the methods array consists of: + \list + \li The Java method name + \li Method signature + \li The C++ functions that will be executed + \endlist + + \code + const JNINativeMethod methods[] = + {{"callNativeOne", "(I)V", reinterpret_cast<void *>(fromJavaOne)}, + {"callNativeTwo", "(I)V", reinterpret_cast<void *>(fromJavaTwo)}}; + QJniEnvironment env; + env.registerNativeMethods("org/qtproject/android/TestJavaClass", methods, 2); + \endcode +*/ +bool QJniEnvironment::registerNativeMethods(const char *className, const JNINativeMethod methods[], + int size) +{ + const jclass clazz = findClass(className); + + if (!clazz) + return false; + + return registerNativeMethods(clazz, methods, size); +} + +/*! + \fn bool QJniEnvironment::registerNativeMethods(const char *className, std::initializer_list<JNINativeMethod> methods) + \overload + + Registers the native functions methods in \a methods for the Java class \a className. + Returns \c true if the registration is successful, otherwise \c false. +*/ + +#if QT_DEPRECATED_SINCE(6, 2) +/*! + \overload + \deprecated [6.2] Use the overload with a const JNINativeMethod[] instead. + + Registers the Java methods in the array \a methods of size \a size, each of + which can call native C++ functions from class \a className. These methods + must be registered before any attempt to call them. + + Returns \c true if the registration is successful, otherwise \c false. + + Each element in the methods array consists of: + \list + \li The Java method name + \li Method signature + \li The C++ functions that will be executed + \endlist + + \code + JNINativeMethod methods[] = {{"callNativeOne", "(I)V", reinterpret_cast<void *>(fromJavaOne)}, + {"callNativeTwo", "(I)V", reinterpret_cast<void *>(fromJavaTwo)}}; + QJniEnvironment env; + env.registerNativeMethods("org/qtproject/android/TestJavaClass", methods, 2); + \endcode +*/ +bool QJniEnvironment::registerNativeMethods(const char *className, JNINativeMethod methods[], + int size) +{ + return registerNativeMethods(className, const_cast<const JNINativeMethod*>(methods), size); +} +#endif +/*! + \overload + + This overload uses a previously cached jclass instance \a clazz. + + \code + JNINativeMethod methods[] {{"callNativeOne", "(I)V", reinterpret_cast<void *>(fromJavaOne)}, + {"callNativeTwo", "(I)V", reinterpret_cast<void *>(fromJavaTwo)}}; + QJniEnvironment env; + jclass clazz = env.findClass("org/qtproject/android/TestJavaClass"); + env.registerNativeMethods(clazz, methods, 2); + \endcode +*/ +bool QJniEnvironment::registerNativeMethods(jclass clazz, const JNINativeMethod methods[], + int size) +{ + if (d->jniEnv->RegisterNatives(clazz, methods, size) < 0) { + checkAndClearExceptions(); + return false; + } + return true; +} + +/*! + \fn bool QJniEnvironment::registerNativeMethods(jclass clazz, std::initializer_list<JNINativeMethod> methods) + \overload + + Registers the native functions methods in \a methods for the Java class \a clazz. + Returns \c true if the registration is successful, otherwise \c false. +*/ + +/*! + \enum QJniEnvironment::OutputMode + + \value Silent The exceptions are cleaned silently + \value Verbose Prints the exceptions and their stack backtrace as an error + to \c stderr stream. +*/ + +/*! + \fn QJniEnvironment::checkAndClearExceptions(OutputMode outputMode = OutputMode::Verbose) + + Cleans any pending exceptions either silently or reporting stack backtrace, + depending on the \a outputMode. + + In contrast to \l QJniObject, which handles exceptions internally, if you + make JNI calls directly via \c JNIEnv, you need to clear any potential + exceptions after the call using this function. For more information about + \c JNIEnv calls that can throw an exception, see \l {Java: JNI Functions}{JNI Functions}. + + \return \c true when a pending exception was cleared. +*/ +bool QJniEnvironment::checkAndClearExceptions(QJniEnvironment::OutputMode outputMode) +{ + return checkAndClearExceptions(d->jniEnv, outputMode); +} + +namespace { + // Any pending exception need to be cleared before calling this + QString exceptionMessage(JNIEnv *env, jthrowable exception) + { + if (!exception) + return {}; + + auto logError = []() { + qWarning() << "QJniEnvironment: a null object returned or an exception occurred while " + "fetching a prior exception message"; + }; + + auto checkAndClear = [env]() { + if (Q_UNLIKELY(env->ExceptionCheck())) { + env->ExceptionClear(); + return true; + } + return false; + }; + + const jclass logClazz = env->FindClass("android/util/Log"); + if (checkAndClear() || !logClazz) { + logError(); + return {}; + } + + const jmethodID methodId = env->GetStaticMethodID(logClazz, "getStackTraceString", + "(Ljava/lang/Throwable;)Ljava/lang/String;"); + if (checkAndClear() || !methodId) { + logError(); + return {}; + } + + jvalue value; + value.l = static_cast<jobject>(exception); + const jobject messageObj = env->CallStaticObjectMethodA(logClazz, methodId, &value); + const jstring jmessage = static_cast<jstring>(messageObj); + if (checkAndClear()) + return {}; + + char const *utfMessage = env->GetStringUTFChars(jmessage, 0); + const QString message = QString::fromUtf8(utfMessage); + + env->ReleaseStringUTFChars(jmessage, utfMessage); + + return message; + } +} // end namespace + +/*! + Cleans any pending exceptions for \a env, either silently or reporting + stack backtrace, depending on the \a outputMode. This is useful when you + already have a \c JNIEnv pointer such as in a native function implementation. + + In contrast to \l QJniObject, which handles exceptions internally, if you + make JNI calls directly via \c JNIEnv, you need to clear any potential + exceptions after the call using this function. For more information about + \c JNIEnv calls that can throw an exception, see \l {Java: JNI Functions}{JNI Functions}. + + \return \c true when a pending exception was cleared. +*/ +bool QJniEnvironment::checkAndClearExceptions(JNIEnv *env, QJniEnvironment::OutputMode outputMode) +{ + if (Q_UNLIKELY(env->ExceptionCheck())) { + if (outputMode == OutputMode::Verbose) { + if (jthrowable exception = env->ExceptionOccurred()) { + env->ExceptionClear(); + const QString message = exceptionMessage(env, exception); + // Print to QWARN since env->ExceptionDescribe() does the same + if (!message.isEmpty()) + qWarning().noquote() << message; + env->DeleteLocalRef(exception); + } else { + // if the exception object is null for some reason just + env->ExceptionDescribe(); + env->ExceptionClear(); + } + } else { + env->ExceptionClear(); + } + + return true; + } + + return false; +} + +QT_END_NAMESPACE |