JNI: Make declared QtJniTypes classes proper QJniObjects

Instead of having a type that doesn't behave like a QJniObject, which
includes not holding a proper reference on the Java object, make the
QtJniTypes::Object type a QJniObject subclass that can be specialized
via CRTP to provide type-specific constructor and static functions.

QJniObject doesn't have a virtual destructor, but we subclass it only to
add a typed interface, without adding any additional data members.
Add versions of the static functions from QJniObjects to the
QtJniTypes::Object so that they can be called without explicitly
specifying the type or class name. This includes a constructor and named
constructors.

Constructing such objects means constructing a Java object of the class
the object type represents, as per the Q_DECLARE_JNI_CLASS declaration.
This is not without ambiguity, as constructing a type with a jobject
parameter can mean that a type wrapping an existing jobject should be
created, or that a Java object should be created with the provided
jobject as the parameter to the constructor (e.g. a copy constructor).
This ambiguity is for now inevitable; we need to be able to implicitly
convert jobject to such types. However, named constructors are provided
so that client code can avoid the ambiguity.

To prevent unnecessary default constructed QJniObjects that are then
replaced immediately with a properly constructed object, add a protected
QJniObject constructor that creates an uninitialized object (e.g. with
the d-pointer being nullptr), which we can then assign the constructed
jobject to using the available assignment operator. Add the special
copy and move constructor and assignment operators as explicit members
for clarity, even though the can all be defaulted.

Such QJniObject subclasses can then be transparently passed as arguments
into JNI call functions that expect a jobject representation, with the
QtJniTypes::Traits specialization from the type declaration providing the
correct signature.

QJniObject's API includes a lot of legacy overloads: with variadic
arguments, a explicit signature string, and jclass/jmethodID parameters
that are completely unused within Qt itself. In addition the explicit
"Object" member functions to explicitly call the version that returns a
jobject (and then a QJniObject). All this call-side complexity is taken
care of by the compile-time signature generation, implicit class type,
and template argument deduction. Overloads taking a jclass or jmethod
are not used anywhere in Qt, which is perhaps an indicator that they,
while nice to have, are too hard to use even for ourselves.

For the modern QtJniTypes class instantiations, remove all the overhead
and reduce the API to the small set of functions that are used all over
the place, and that don't require an explicit signature, or class/method
lookup.

This is a source incompatible change, as now QJniTypes::Object is no
longer a primitive type, and no longer binary equivalent to jobject.
However, this is acceptable as the API has so far been undocumented,
and is only used internally in Qt (and changes to adapt are largely
already merged).

Change-Id: I6d14c09c8165652095f30511f04dc17217245bf5
Reviewed-by: Juha Vuolle <juha.vuolle@qt.io>
This commit is contained in:
Volker Hilsheimer 2023-09-13 12:03:16 +02:00
parent 0bd3882acd
commit 62cb5589b3
5 changed files with 291 additions and 21 deletions

View File

@ -1386,7 +1386,7 @@ bool QJniObject::isSameObject(const QJniObject &other) const
void QJniObject::assign(jobject obj)
{
if (isSameObject(obj))
if (d && isSameObject(obj))
return;
jobject jobj = static_cast<jobject>(obj);

View File

@ -42,6 +42,8 @@ class Q_CORE_EXPORT QJniObject
using Type = q20::remove_cvref_t<T>;
if constexpr (std::is_same_v<Type, QString>) {
return newLocalRef<jstring>(QJniObject::fromString(value));
} else if constexpr (std::is_base_of_v<QJniObject, Type>) {
return value.object();
} else {
return static_cast<T &&>(value);
}
@ -52,8 +54,11 @@ class Q_CORE_EXPORT QJniObject
using Type = q20::remove_cvref_t<T>;
if constexpr (std::is_same_v<Type, QString>) {
return object.toString();
} else if constexpr (std::is_base_of_v<QJniObject, Type>
&& !std::is_same_v<QJniObject, Type>) {
return T{std::move(object)};
} else {
return object;
return std::move(object);
}
}
};
@ -67,9 +72,12 @@ public:
#endif
>
explicit QJniObject(const char *className, Args &&...args)
: QJniObject(className, QtJniTypes::constructorSignature<Args...>().data(),
std::forward<Args>(args)...)
{}
: QJniObject(Qt::Uninitialized)
{
LocalFrame<Args...> localFrame;
*this = QJniObject(className, QtJniTypes::constructorSignature<Args...>().data(),
localFrame.convertToJni(std::forward<Args>(args))...);
}
explicit QJniObject(jclass clazz);
explicit QJniObject(jclass clazz, const char *signature, ...);
template<typename ...Args
@ -82,6 +90,12 @@ public:
std::forward<Args>(args)...)
{}
QJniObject(jobject globalRef);
QJniObject(const QJniObject &other) noexcept = default;
QJniObject(QJniObject &&other) noexcept = default;
QJniObject &operator=(const QJniObject &other) noexcept = default;
QJniObject &operator=(QJniObject &&other) noexcept = default;
~QJniObject();
template<typename Class, typename ...Args>
@ -527,6 +541,9 @@ public:
return *this;
}
protected:
QJniObject(Qt::Initialization) {}
private:
static jclass loadClass(const QByteArray &className, JNIEnv *env);

View File

@ -13,25 +13,176 @@ QT_BEGIN_NAMESPACE
namespace QtJniTypes
{
// A generic thin wrapper around jobject, convertible to jobject.
// We need this as a baseclass so that QJniObject can be implicitly
// constructed from the various subclasses. We can also pass instances
// of this type (or of any of the generated subclasses) as if it was
// a jobject.
struct Object
// A generic base class for specialized QJniObject types, to be used by
// subclasses via CRTP. It's implicitly convertible to and from jobject, which
// allows the QJniObject implementation to implicitly pass instances of this
// type through the variadic argument JNI APIs.
template<typename Type>
struct Object : QJniObject
{
jobject _object;
constexpr operator jobject() const { return _object; }
operator QJniObject() const { return QJniObject(_object); }
using Class = Type;
operator jobject() const noexcept { return object(); }
Q_IMPLICIT Object(jobject object) : QJniObject(object) {}
Q_IMPLICIT Object(const QJniObject &object) : QJniObject(object) {}
Q_IMPLICIT Object(QJniObject &&object) : QJniObject(std::move(object)) {}
// Compiler-generated copy/move semantics based on QJniObject's shared d-pointer are fine!
Object(const Object<Type> &other) = default;
Object(Object<Type> &&other) = default;
Object<Type> &operator=(const Object<Type> &other) = default;
Object<Type> &operator=(Object<Type> &&other) = default;
// avoid ambiguities with deleted const char * constructor
Q_IMPLICIT Object(std::nullptr_t) : QJniObject() {}
// this intentionally includes the default constructor
template<typename ...Args
, ValidSignatureTypes<Args...> = true
>
explicit Object(Args &&...args)
: QJniObject(Qt::Initialization::Uninitialized)
{
*this = Object<Type>{QJniObject::construct<Class>(std::forward<Args>(args)...)};
}
// named constructors avoid ambiguities
static Object<Type> fromJObject(jobject object) { return Object<Type>(object); }
template <typename ...Args>
static Object<Type> construct(Args &&...args)
{
return Object<Type>{QJniObject::construct<Class, Args...>(std::forward<Args>(args)...)};
}
// public API forwarding to QJniObject, with the implicit Class template parameter
template <typename Ret, typename ...Args
#ifndef Q_QDOC
, QtJniTypes::ValidSignatureTypes<Ret, Args...> = true
#endif
>
static auto callStaticMethod(const char *name, Args &&...args)
{
return QJniObject::callStaticMethod<Class, Ret, Args...>(name,
std::forward<Args>(args)...);
}
template <typename T
#ifndef Q_QDOC
, QtJniTypes::ValidFieldType<T> = true
#endif
>
static auto getStaticField(const char *field)
{
return QJniObject::getStaticField<Class, T>(field);
}
template <typename T
#ifndef Q_QDOC
, QtJniTypes::ValidFieldType<T> = true
#endif
>
static void setStaticField(const char *field, T &&value)
{
QJniObject::setStaticField<Class, T>(field, std::forward<T>(value));
}
// keep only these overloads, the rest is made private
template <typename Ret, typename ...Args
#ifndef Q_QDOC
, QtJniTypes::ValidSignatureTypes<Ret, Args...> = true
#endif
>
auto callMethod(const char *method, Args &&...args) const
{
return QJniObject::callMethod<Ret>(method, std::forward<Args>(args)...);
}
template <typename T
#ifndef Q_QDOC
, QtJniTypes::ValidFieldType<T> = true
#endif
>
auto getField(const char *fieldName) const
{
return QJniObject::getField<T>(fieldName);
}
template <typename T
#ifndef Q_QDOC
, QtJniTypes::ValidFieldType<T> = true
#endif
>
void setField(const char *fieldName, T &&value)
{
QJniObject::setField(fieldName, std::forward<T>(value));
}
private:
// The following declutters the API of these types compared to the QJniObject API.
// 1) 'Object' methods; the functions we have have return type auto and will return
// the type specified by the template parameter.
using QJniObject::callObjectMethod;
using QJniObject::callStaticObjectMethod;
using QJniObject::getObjectField;
using QJniObject::getStaticObjectField;
// 2) Constructors that take a class name, signature string, or class template argument
explicit Object(const char *className) = delete;
explicit Object(const char *className, const char *signature, ...) = delete;
template<typename ...Args>
explicit Object(const char *className, Args &&...args) = delete;
explicit Object(jclass clazz, const char *signature, ...) = delete;
template<typename Class, typename ...Args>
static QJniObject construct(Args &&...args) = delete;
// 3) Overloads that take a class name/jclass, methodID, signature string, or an
// explicit class template argument
template <typename Ret, typename ...Args>
auto callMethod(const char *methodName, const char *signature, Args &&...args) const = delete;
template <typename Ret, typename ...Args>
static auto callStaticMethod(const char *className, const char *methodName,
const char *signature, Args &&...args) = delete;
template <typename Ret, typename ...Args>
static auto callStaticMethod(jclass clazz, const char *methodName,
const char *signature, Args &&...args) = delete;
template <typename Ret, typename ...Args>
static auto callStaticMethod(jclass clazz, jmethodID methodId, Args &&...args) = delete;
template <typename Ret, typename ...Args>
static auto callStaticMethod(const char *className, const char *methodName,
Args &&...args) = delete;
template <typename Ret, typename ...Args>
static auto callStaticMethod(jclass clazz, const char *methodName, Args &&...args) = delete;
template <typename Klass, typename Ret, typename ...Args>
static auto callStaticMethod(const char *methodName, Args &&...args) = delete;
template <typename T>
static auto getStaticField(const char *className, const char *fieldName) = delete;
template <typename T>
static auto getStaticField(jclass clazz, const char *fieldName) = delete;
template <typename Klass, typename T>
static auto getStaticField(const char *fieldName) = delete;
template <typename T>
void setField(const char *fieldName, const char *signature, T value) = delete;
template <typename T>
static void setStaticField(const char *className, const char *fieldName, T value) = delete;
template <typename T>
static void setStaticField(const char *className, const char *fieldName,
const char *signature, T value) = delete;
template <typename T>
static void setStaticField(jclass clazz, const char *fieldName,
const char *signature, T value) = delete;
template <typename T>
static void setStaticField(jclass clazz, const char *fieldName, T value) = delete;
template <typename Klass, typename T>
static void setStaticField(const char *fieldName, T value) = delete;
};
} // namespace QtJniTypes
#define Q_DECLARE_JNI_TYPE_HELPER(Type) \
namespace QtJniTypes { \
struct Type : Object \
struct Type : Object<Type> \
{ \
constexpr Type(jobject o) noexcept : Object{o} {} \
using Object::Object; \
}; \
} \

View File

@ -12,6 +12,7 @@ using namespace Qt::StringLiterals;
static const char testClassName[] = "org/qtproject/qt/android/testdatapackage/QtJniObjectTestClass";
Q_DECLARE_JNI_CLASS(QtJniObjectTestClass, testClassName)
using TestClass = QtJniTypes::QtJniObjectTestClass;
static const jbyte A_BYTE_VALUE = 127;
static const jshort A_SHORT_VALUE = 32767;
@ -152,6 +153,16 @@ void tst_QJniObject::ctor()
QVERIFY(object.isValid());
}
{
// from Qt 6.7 on we can construct declared classes through the helper type
QJniObject object = TestClass::construct();
QVERIFY(object.isValid());
// or even directly
TestClass testObject;
QVERIFY(testObject.isValid());
}
{
QJniObject string = QJniObject::fromString(QLatin1String("Hello, Java"));
QJniObject object("java/lang/String", "(Ljava/lang/String;)V", string.object<jstring>());
@ -311,10 +322,15 @@ void tst_QJniObject::className()
}
{
QJniObject strObject = QJniObject("java/lang/String", jString.object<jstring>());
QJniObject strObject = QJniObject("java/lang/String", str);
QCOMPARE(strObject.className(), "java/lang/String");
QCOMPARE(strObject.toString(), str);
}
{
TestClass test;
QCOMPARE(test.className(), testClassName);
}
}
void tst_QJniObject::callStaticObjectMethodClassName()
@ -1119,8 +1135,8 @@ void setStaticField(const char *fieldName, T testValue)
// use template overload to reset to default
T defaultValue = {};
QJniObject::setStaticField<QtJniTypes::QtJniObjectTestClass, T>(fieldName, defaultValue);
res = QJniObject::getStaticField<QtJniTypes::QtJniObjectTestClass, T>(fieldName);
TestClass::setStaticField(fieldName, defaultValue);
res = TestClass::getStaticField<T>(fieldName);
QCOMPARE(res, defaultValue);
}
@ -1185,8 +1201,8 @@ void tst_QJniObject::setStaticObjectField()
// as of Qt 6.7, we can set and get strings directly
using namespace QtJniTypes;
QJniObject::setStaticField<QtJniObjectTestClass>("S_STRING_OBJECT_VAR", qString);
QCOMPARE((QJniObject::getStaticField<QtJniObjectTestClass, QString>("S_STRING_OBJECT_VAR")), qString);
QtJniObjectTestClass::setStaticField("S_STRING_OBJECT_VAR", qString);
QCOMPARE(QtJniObjectTestClass::getStaticField<QString>("S_STRING_OBJECT_VAR"), qString);
}
void tst_QJniObject::templateApiCheck()
@ -1473,11 +1489,17 @@ void tst_QJniObject::templateApiCheck()
QJniObject res = QJniObject::callStaticObjectMethod<jobjectArray>(testClassName,
"staticObjectArrayMethod");
QVERIFY(res.isValid());
const auto array = TestClass::callStaticMethod<jobject[]>("staticObjectArrayMethod");
QVERIFY(array.isValid());
}
{
QJniObject res = testClass.callObjectMethod<jobjectArray>("objectArrayMethod");
QVERIFY(res.isValid());
const auto array = testClass.callMethod<jobject[]>("objectArrayMethod");
QVERIFY(array.isValid());
}
// jbooleanArray ------------------------------------------------------------------------------
@ -1485,11 +1507,17 @@ void tst_QJniObject::templateApiCheck()
QJniObject res = QJniObject::callStaticObjectMethod<jbooleanArray>(testClassName,
"staticBooleanArrayMethod");
QVERIFY(res.isValid());
const auto array = TestClass::callStaticMethod<jboolean[]>("staticBooleanArrayMethod");
QVERIFY(array.isValid());
}
{
QJniObject res = testClass.callObjectMethod<jbooleanArray>("booleanArrayMethod");
QVERIFY(res.isValid());
const auto array = testClass.callMethod<jboolean[]>("booleanArrayMethod");
QVERIFY(array.isValid());
}
// jbyteArray ---------------------------------------------------------------------------------
@ -1497,11 +1525,17 @@ void tst_QJniObject::templateApiCheck()
QJniObject res = QJniObject::callStaticObjectMethod<jbyteArray>(testClassName,
"staticByteArrayMethod");
QVERIFY(res.isValid());
const auto array = TestClass::callStaticMethod<jbyte[]>("staticByteArrayMethod");
QVERIFY(array.isValid());
}
{
QJniObject res = testClass.callObjectMethod<jbyteArray>("byteArrayMethod");
QVERIFY(res.isValid());
const auto array = testClass.callMethod<jbyte[]>("byteArrayMethod");
QVERIFY(array.isValid());
}
// jcharArray ---------------------------------------------------------------------------------
@ -1509,11 +1543,17 @@ void tst_QJniObject::templateApiCheck()
QJniObject res = QJniObject::callStaticObjectMethod<jcharArray>(testClassName,
"staticCharArrayMethod");
QVERIFY(res.isValid());
const auto array = TestClass::callStaticMethod<jchar[]>("staticCharArrayMethod");
QVERIFY(array.isValid());
}
{
QJniObject res = testClass.callObjectMethod<jcharArray>("charArrayMethod");
QVERIFY(res.isValid());
const auto array = testClass.callMethod<jchar[]>("charArrayMethod");
QVERIFY(array.isValid());
}
// jshortArray --------------------------------------------------------------------------------
@ -1521,11 +1561,17 @@ void tst_QJniObject::templateApiCheck()
QJniObject res = QJniObject::callStaticObjectMethod<jshortArray>(testClassName,
"staticShortArrayMethod");
QVERIFY(res.isValid());
const auto array = TestClass::callStaticMethod<jshort[]>("staticShortArrayMethod");
QVERIFY(array.isValid());
}
{
QJniObject res = testClass.callObjectMethod<jshortArray>("shortArrayMethod");
QVERIFY(res.isValid());
const auto array = testClass.callMethod<jshort[]>("shortArrayMethod");
QVERIFY(array.isValid());
}
// jintArray ----------------------------------------------------------------------------------
@ -1533,11 +1579,17 @@ void tst_QJniObject::templateApiCheck()
QJniObject res = QJniObject::callStaticObjectMethod<jintArray>(testClassName,
"staticIntArrayMethod");
QVERIFY(res.isValid());
const auto array = TestClass::callStaticMethod<jint[]>("staticIntArrayMethod");
QVERIFY(array.isValid());
}
{
QJniObject res = testClass.callObjectMethod<jintArray>("intArrayMethod");
QVERIFY(res.isValid());
const auto array = testClass.callMethod<jint[]>("intArrayMethod");
QVERIFY(array.isValid());
}
// jlongArray ---------------------------------------------------------------------------------
@ -1545,11 +1597,17 @@ void tst_QJniObject::templateApiCheck()
QJniObject res = QJniObject::callStaticObjectMethod<jlongArray>(testClassName,
"staticLongArrayMethod");
QVERIFY(res.isValid());
const auto array = TestClass::callStaticMethod<jlong[]>("staticLongArrayMethod");
QVERIFY(array.isValid());
}
{
QJniObject res = testClass.callObjectMethod<jlongArray>("longArrayMethod");
QVERIFY(res.isValid());
const auto array = testClass.callMethod<jlong[]>("longArrayMethod");
QVERIFY(array.isValid());
}
// jfloatArray --------------------------------------------------------------------------------
@ -1557,11 +1615,17 @@ void tst_QJniObject::templateApiCheck()
QJniObject res = QJniObject::callStaticObjectMethod<jfloatArray>(testClassName,
"staticFloatArrayMethod");
QVERIFY(res.isValid());
const auto array = TestClass::callStaticMethod<jfloat[]>("staticFloatArrayMethod");
QVERIFY(array.isValid());
}
{
QJniObject res = testClass.callObjectMethod<jfloatArray>("floatArrayMethod");
QVERIFY(res.isValid());
const auto array = testClass.callMethod<jfloat[]>("floatArrayMethod");
QVERIFY(array.isValid());
}
// jdoubleArray -------------------------------------------------------------------------------
@ -1569,11 +1633,17 @@ void tst_QJniObject::templateApiCheck()
QJniObject res = QJniObject::callStaticObjectMethod<jdoubleArray>(testClassName,
"staticDoubleArrayMethod");
QVERIFY(res.isValid());
const auto array = TestClass::callStaticMethod<jdouble[]>("staticDoubleArrayMethod");
QVERIFY(array.isValid());
}
{
QJniObject res = testClass.callObjectMethod<jdoubleArray>("doubleArrayMethod");
QVERIFY(res.isValid());
const auto array = testClass.callMethod<jdouble[]>("doubleArrayMethod");
QVERIFY(array.isValid());
}
}

View File

@ -5,6 +5,8 @@
#include <QtCore/qjnitypes.h>
using namespace Qt::StringLiterals;
class tst_QJniTypes : public QObject
{
Q_OBJECT
@ -15,6 +17,7 @@ public:
private slots:
void initTestCase();
void nativeMethod();
void construct();
};
struct QtJavaWrapper {};
@ -159,6 +162,35 @@ void tst_QJniTypes::nativeMethod()
QCOMPARE(method.signature, "(ILjava/lang/String;J)Z");
}
void tst_QJniTypes::construct()
{
using namespace QtJniTypes;
const QString text = u"Java String"_s;
String str(text);
QVERIFY(str.isValid());
QCOMPARE(str.toString(), text);
jobject jref = nullptr; // must be jobject, not jstring
{
// if jref would be a jstring, then this would call the
// Java String copy constructor!
String jstr(jref);
QVERIFY(!jstr.isValid());
}
jref = str.object<jstring>();
{
String jstr(jref);
QVERIFY(jstr.isValid());
QCOMPARE(jstr.toString(), text);
}
String str2 = str;
QCOMPARE(str.toString(), text);
String str3 = std::move(str2);
QCOMPARE(str3.toString(), text);
}
QTEST_MAIN(tst_QJniTypes)
#include "tst_qjnitypes.moc"