JNI: Implicitly convert QString to jstring in API calls

Support this both for parameters and return types, in all template
functions for calling methods or accessing fields.

To manage the life-time of the temporary objects, create a local stack
frame in the JVM around the calling function. Popping that implicilty
releases all local refernces, so that we don't have to worry about
doing so explicilty. Adding a local reference to the temporary jstring
objects is then enough for the object to live as long as it's needed.

The LocalFrame RAII-like type needs a QJniEnvironment to push and pop
the frame in the JVM, so we don't need a local QJniEnvironment anymore.
Reduce code duplication a bit as a drive-by, and add test coverage.

Change-Id: I801a1b006eea5af02f57d8bb7cca089508dadd1d
Reviewed-by: Tinja Paavoseppä <tinja.paavoseppa@qt.io>
This commit is contained in:
Volker Hilsheimer 2023-09-14 12:47:09 +02:00
parent 4702208b40
commit f826d1615a
3 changed files with 130 additions and 64 deletions

View File

@ -16,6 +16,47 @@ class QJniObjectPrivate;
class Q_CORE_EXPORT QJniObject class Q_CORE_EXPORT QJniObject
{ {
template <typename ...Args>
struct LocalFrame {
QJniEnvironment env;
bool hasFrame = false;
~LocalFrame() {
if (hasFrame)
env->PopLocalFrame(nullptr);
}
template <typename T>
auto newLocalRef(QJniObject &&object) {
if (!hasFrame) {
if (env->PushLocalFrame(sizeof...(Args)) < 0)
return T{}; // JVM is out of memory, avoid making matters worse
hasFrame = true;
}
return static_cast<T>(env->NewLocalRef(object.template object<T>()));
}
JNIEnv *jniEnv() const { return env.jniEnv(); }
bool checkAndClearExceptions() { return env.checkAndClearExceptions(); }
template <typename T>
auto convertToJni(T &&value)
{
using Type = q20::remove_cvref_t<T>;
if constexpr (std::is_same_v<Type, QString>) {
return newLocalRef<jstring>(QJniObject::fromString(value));
} else {
return static_cast<T &&>(value);
}
}
template <typename T>
auto convertFromJni(QJniObject &&object)
{
using Type = q20::remove_cvref_t<T>;
if constexpr (std::is_same_v<Type, QString>) {
return object.toString();
} else {
return object;
}
}
};
public: public:
QJniObject(); QJniObject();
explicit QJniObject(const char *className); explicit QJniObject(const char *className);
@ -46,9 +87,10 @@ public:
template<typename Class, typename ...Args> template<typename Class, typename ...Args>
static inline QJniObject construct(Args &&...args) static inline QJniObject construct(Args &&...args)
{ {
LocalFrame<Args...> frame;
return QJniObject(QtJniTypes::Traits<Class>::className().data(), return QJniObject(QtJniTypes::Traits<Class>::className().data(),
QtJniTypes::constructorSignature<Args...>().data(), QtJniTypes::constructorSignature<Args...>().data(),
std::forward<Args>(args)...); frame.convertToJni(std::forward<Args>(args))...);
} }
jobject object() const; jobject object() const;
@ -68,19 +110,22 @@ public:
> >
auto callMethod(const char *methodName, const char *signature, Args &&...args) const auto callMethod(const char *methodName, const char *signature, Args &&...args) const
{ {
LocalFrame<Args...> frame;
if constexpr (QtJniTypes::isObjectType<Ret>()) { if constexpr (QtJniTypes::isObjectType<Ret>()) {
return callObjectMethod(methodName, signature, std::forward<Args>(args)...); return frame.template convertFromJni<Ret>(callObjectMethod(methodName, signature,
frame.convertToJni(std::forward<Args>(args))...));
} else { } else {
QJniEnvironment env; jmethodID id = getCachedMethodID(frame.jniEnv(), methodName, signature);
jmethodID id = getCachedMethodID(env.jniEnv(), methodName, signature);
if (id) { if (id) {
if constexpr (std::is_same_v<Ret, void>) { if constexpr (std::is_same_v<Ret, void>) {
callVoidMethodV(env.jniEnv(), id, std::forward<Args>(args)...); callVoidMethodV(frame.jniEnv(), id,
env.checkAndClearExceptions(); frame.convertToJni(std::forward<Args>(args))...);
frame.checkAndClearExceptions();
} else { } else {
Ret res{}; Ret res{};
callMethodForType<Ret>(env.jniEnv(), res, object(), id, std::forward<Args>(args)...); callMethodForType<Ret>(frame.jniEnv(), res, object(), id,
if (env.checkAndClearExceptions()) frame.convertToJni(std::forward<Args>(args))...);
if (frame.checkAndClearExceptions())
res = {}; res = {};
return res; return res;
} }
@ -139,18 +184,21 @@ public:
> >
static auto callStaticMethod(jclass clazz, jmethodID methodId, Args &&...args) static auto callStaticMethod(jclass clazz, jmethodID methodId, Args &&...args)
{ {
LocalFrame<Args...> frame;
if constexpr (QtJniTypes::isObjectType<Ret>()) { if constexpr (QtJniTypes::isObjectType<Ret>()) {
return callStaticObjectMethod(clazz, methodId, std::forward<Args>(args)...); return frame.template convertFromJni<Ret>(callStaticObjectMethod(clazz, methodId,
frame.convertToJni(std::forward<Args>(args))...));
} else { } else {
QJniEnvironment env;
if (clazz && methodId) { if (clazz && methodId) {
if constexpr (std::is_same_v<Ret, void>) { if constexpr (std::is_same_v<Ret, void>) {
callStaticMethodForVoid(env.jniEnv(), clazz, methodId, std::forward<Args>(args)...); callStaticMethodForVoid(frame.jniEnv(), clazz, methodId,
env.checkAndClearExceptions(); frame.convertToJni(std::forward<Args>(args))...);
frame.checkAndClearExceptions();
} else { } else {
Ret res{}; Ret res{};
callStaticMethodForType<Ret>(env.jniEnv(), res, clazz, methodId, std::forward<Args>(args)...); callStaticMethodForType<Ret>(frame.jniEnv(), res, clazz, methodId,
if (env.checkAndClearExceptions()) frame.convertToJni(std::forward<Args>(args))...);
if (frame.checkAndClearExceptions())
res = {}; res = {};
return res; return res;
} }
@ -216,7 +264,9 @@ public:
{ {
QtJniTypes::assertObjectType<Ret>(); QtJniTypes::assertObjectType<Ret>();
constexpr auto signature = QtJniTypes::methodSignature<Ret, Args...>(); constexpr auto signature = QtJniTypes::methodSignature<Ret, Args...>();
return callStaticObjectMethod(className, methodName, signature.data(), std::forward<Args>(args)...); LocalFrame<Args...> frame;
return frame.template convertFromJni<Ret>(callStaticObjectMethod(className, methodName, signature.data(),
frame.convertToJni(std::forward<Args>(args))...));
} }
template <typename Ret, typename ...Args template <typename Ret, typename ...Args
@ -228,7 +278,9 @@ public:
{ {
QtJniTypes::assertObjectType<Ret>(); QtJniTypes::assertObjectType<Ret>();
constexpr auto signature = QtJniTypes::methodSignature<Ret, Args...>(); constexpr auto signature = QtJniTypes::methodSignature<Ret, Args...>();
return callStaticObjectMethod(clazz, methodName, signature.data(), std::forward<Args>(args)...); LocalFrame<Args...> frame;
return frame.template convertFromJni<Ret>(callStaticObjectMethod(clazz, methodName, signature.data(),
frame.convertToJni(std::forward<Args>(args))...));
} }
template <typename T template <typename T
@ -238,16 +290,16 @@ public:
> >
auto getField(const char *fieldName) const auto getField(const char *fieldName) const
{ {
LocalFrame<T> frame;
if constexpr (QtJniTypes::isObjectType<T>()) { if constexpr (QtJniTypes::isObjectType<T>()) {
return getObjectField<T>(fieldName); return frame.template convertFromJni<T>(getObjectField<T>(fieldName));
} else { } else {
QJniEnvironment env;
T res{}; T res{};
constexpr auto signature = QtJniTypes::fieldSignature<T>(); constexpr auto signature = QtJniTypes::fieldSignature<T>();
jfieldID id = getCachedFieldID(env.jniEnv(), fieldName, signature); jfieldID id = getCachedFieldID(frame.jniEnv(), fieldName, signature);
if (id) { if (id) {
getFieldForType<T>(env.jniEnv(), res, object(), id); getFieldForType<T>(frame.jniEnv(), res, object(), id);
if (env.checkAndClearExceptions()) if (frame.checkAndClearExceptions())
res = {}; res = {};
} }
return res; return res;
@ -261,27 +313,14 @@ public:
> >
static auto getStaticField(const char *className, const char *fieldName) static auto getStaticField(const char *className, const char *fieldName)
{ {
LocalFrame<T> frame;
if constexpr (QtJniTypes::isObjectType<T>()) { if constexpr (QtJniTypes::isObjectType<T>()) {
return getStaticObjectField<T>(className, fieldName); return frame.template convertFromJni<T>(getStaticObjectField<T>(className, fieldName));
} else { } else {
QJniEnvironment env; jclass clazz = QJniObject::loadClass(className, frame.jniEnv());
jclass clazz = QJniObject::loadClass(className, env.jniEnv());
T res{};
if (!clazz) if (!clazz)
return res; return T{};
return getStaticField<T>(clazz, fieldName);
constexpr auto signature = QtJniTypes::fieldSignature<T>();
jfieldID id = getCachedFieldID(env.jniEnv(), clazz,
QJniObject::toBinaryEncClassName(className),
fieldName,
signature, true);
if (!id)
return res;
getStaticFieldForType<T>(env.jniEnv(), res, clazz, id);
if (env.checkAndClearExceptions())
res = {};
return res;
} }
} }
@ -292,16 +331,16 @@ public:
> >
static auto getStaticField(jclass clazz, const char *fieldName) static auto getStaticField(jclass clazz, const char *fieldName)
{ {
LocalFrame<T> frame;
if constexpr (QtJniTypes::isObjectType<T>()) { if constexpr (QtJniTypes::isObjectType<T>()) {
return getStaticObjectField<T>(clazz, fieldName); return frame.template convertFromJni<T>(getStaticObjectField<T>(clazz, fieldName));
} else { } else {
QJniEnvironment env;
T res{}; T res{};
constexpr auto signature = QtJniTypes::fieldSignature<T>(); constexpr auto signature = QtJniTypes::fieldSignature<T>();
jfieldID id = getFieldID(env.jniEnv(), clazz, fieldName, signature, true); jfieldID id = getFieldID(frame.jniEnv(), clazz, fieldName, signature, true);
if (id) { if (id) {
getStaticFieldForType<T>(env.jniEnv(), res, clazz, id); getStaticFieldForType<T>(frame.jniEnv(), res, clazz, id);
if (env.checkAndClearExceptions()) if (frame.checkAndClearExceptions())
res = {}; res = {};
} }
return res; return res;
@ -398,19 +437,19 @@ public:
> >
static void setStaticField(const char *className, const char *fieldName, T value) static void setStaticField(const char *className, const char *fieldName, T value)
{ {
QJniEnvironment env; LocalFrame<T> frame;
jclass clazz = QJniObject::loadClass(className, env.jniEnv()); jclass clazz = QJniObject::loadClass(className, frame.jniEnv());
if (!clazz) if (!clazz)
return; return;
constexpr auto signature = QtJniTypes::fieldSignature<T>(); constexpr auto signature = QtJniTypes::fieldSignature<T>();
jfieldID id = getCachedFieldID(env.jniEnv(), clazz, className, fieldName, jfieldID id = getCachedFieldID(frame.jniEnv(), clazz, className, fieldName,
signature, true); signature, true);
if (!id) if (!id)
return; return;
setStaticFieldForType<T>(env.jniEnv(), clazz, id, value); setStaticFieldForType<T>(frame.jniEnv(), clazz, id, value);
env.checkAndClearExceptions(); frame.checkAndClearExceptions();
} }
template <typename T template <typename T
@ -459,13 +498,7 @@ public:
> >
static void setStaticField(jclass clazz, const char *fieldName, T value) static void setStaticField(jclass clazz, const char *fieldName, T value)
{ {
QJniEnvironment env; setStaticField(clazz, fieldName, QtJniTypes::fieldSignature<T>(), value);
constexpr auto signature = QtJniTypes::fieldSignature<T>();
jfieldID id = getFieldID(env.jniEnv(), clazz, fieldName, signature, true);
if (id) {
setStaticFieldForType<T>(env.jniEnv(), clazz, id, value);
env.checkAndClearExceptions();
}
} }
template <typename Klass, typename T template <typename Klass, typename T
@ -662,6 +695,7 @@ private:
static constexpr void setFieldForType(JNIEnv *env, jobject obj, static constexpr void setFieldForType(JNIEnv *env, jobject obj,
jfieldID id, T value) jfieldID id, T value)
{ {
LocalFrame<T> frame;
if constexpr (std::is_same_v<T, jboolean>) if constexpr (std::is_same_v<T, jboolean>)
env->SetBooleanField(obj, id, value); env->SetBooleanField(obj, id, value);
else if constexpr (likeIntegerType<T, jbyte>) else if constexpr (likeIntegerType<T, jbyte>)
@ -678,8 +712,8 @@ private:
env->SetFloatField(obj, id, value); env->SetFloatField(obj, id, value);
else if constexpr (std::is_same_v<T, jdouble>) else if constexpr (std::is_same_v<T, jdouble>)
env->SetDoubleField(obj, id, value); env->SetDoubleField(obj, id, value);
else if constexpr (std::is_convertible_v<T, jobject>) else if constexpr (QtJniTypes::isObjectType<T>())
env->SetObjectField(obj, id, value); env->SetObjectField(obj, id, static_cast<jobject>(frame.convertToJni(value)));
else else
QtJniTypes::staticAssertTypeMismatch(); QtJniTypes::staticAssertTypeMismatch();
} }
@ -688,6 +722,7 @@ private:
static constexpr void setStaticFieldForType(JNIEnv *env, jclass clazz, static constexpr void setStaticFieldForType(JNIEnv *env, jclass clazz,
jfieldID id, T value) jfieldID id, T value)
{ {
LocalFrame<T> frame;
if constexpr (std::is_same_v<T, jboolean>) if constexpr (std::is_same_v<T, jboolean>)
env->SetStaticBooleanField(clazz, id, value); env->SetStaticBooleanField(clazz, id, value);
else if constexpr (likeIntegerType<T, jbyte>) else if constexpr (likeIntegerType<T, jbyte>)
@ -704,8 +739,8 @@ private:
env->SetStaticFloatField(clazz, id, value); env->SetStaticFloatField(clazz, id, value);
else if constexpr (std::is_same_v<T, jdouble>) else if constexpr (std::is_same_v<T, jdouble>)
env->SetStaticDoubleField(clazz, id, value); env->SetStaticDoubleField(clazz, id, value);
else if constexpr (std::is_convertible_v<T, jobject>) else if constexpr (QtJniTypes::isObjectType<T>())
env->SetStaticObjectField(clazz, id, value); env->SetStaticObjectField(clazz, id, static_cast<jobject>(frame.convertToJni(value)));
else else
QtJniTypes::staticAssertTypeMismatch(); QtJniTypes::staticAssertTypeMismatch();
} }

View File

@ -235,6 +235,8 @@ struct Traits {
return CTString("V"); return CTString("V");
} else if constexpr (std::is_enum_v<T>) { } else if constexpr (std::is_enum_v<T>) {
return Traits<std::underlying_type_t<T>>::signature(); return Traits<std::underlying_type_t<T>>::signature();
} else if constexpr (std::is_same_v<T, QString>) {
return CTString("Ljava/lang/String;");
} }
// else: return void -> not implemented // else: return void -> not implemented
} }

View File

@ -187,8 +187,10 @@ void tst_QJniObject::ctor()
void tst_QJniObject::callMethodTest() void tst_QJniObject::callMethodTest()
{ {
{ {
QJniObject jString1 = QJniObject::fromString(QLatin1String("Hello, Java")); const QString qString1 = u"Hello, Java"_s;
QJniObject jString2 = QJniObject::fromString(QLatin1String("hELLO, jAVA")); const QString qString2 = u"hELLO, jAVA"_s;
QJniObject jString1 = QJniObject::fromString(qString1);
QJniObject jString2 = QJniObject::fromString(qString2);
QVERIFY(jString1 != jString2); QVERIFY(jString1 != jString2);
const jboolean isEmpty = jString1.callMethod<jboolean>("isEmpty"); const jboolean isEmpty = jString1.callMethod<jboolean>("isEmpty");
@ -201,6 +203,10 @@ void tst_QJniObject::callMethodTest()
ret = jString1.callMethod<jint>("compareToIgnoreCase", jString2.object<jstring>()); ret = jString1.callMethod<jint>("compareToIgnoreCase", jString2.object<jstring>());
QVERIFY(0 == ret); QVERIFY(0 == ret);
// as of Qt 6.7, we can pass QString directly
ret = jString1.callMethod<jint>("compareToIgnoreCase", qString2);
QVERIFY(0 == ret);
} }
{ {
@ -219,6 +225,13 @@ void tst_QJniObject::callMethodTest()
QJniObject subString = jString.callMethod<jstring>("substring", 0, 4); QJniObject subString = jString.callMethod<jstring>("substring", 0, 4);
QCOMPARE(subString.toString(), qString.mid(0, 4)); QCOMPARE(subString.toString(), qString.mid(0, 4));
// and as of Qt 6.7, we can return and take QString directly
QCOMPARE(jString.callMethod<QString>("substring", 0, 4), qString.mid(0, 4));
QCOMPARE(jString.callMethod<jstring>("substring", 0, 7)
.callMethod<jstring>("toUpperCase")
.callMethod<QString>("concat", u"C++"_s), u"HELLO, C++"_s);
} }
} }
@ -346,6 +359,11 @@ void tst_QJniObject::callStaticObjectMethod()
QVERIFY(returnValue.isValid()); QVERIFY(returnValue.isValid());
QCOMPARE(returnValue.toString(), string); QCOMPARE(returnValue.toString(), string);
// from 6.7 we can pass QString directly, both as parameters and return type
QString result = QJniObject::callStaticMethod<jstring, QString>("format",
string,
jobjectArray(0));
QCOMPARE(result, string);
} }
void tst_QJniObject::callStaticObjectMethodById() void tst_QJniObject::callStaticObjectMethodById()
@ -1053,11 +1071,16 @@ void tst_QJniObject::setObjectField()
QJniObject obj(testClassName); QJniObject obj(testClassName);
QVERIFY(obj.isValid()); QVERIFY(obj.isValid());
QJniObject testValue = QJniObject::fromString(QStringLiteral("Hello")); const QString qString = u"Hello"_s;
QJniObject testValue = QJniObject::fromString(qString);
obj.setField("STRING_OBJECT_VAR", testValue.object<jstring>()); obj.setField("STRING_OBJECT_VAR", testValue.object<jstring>());
QJniObject res = obj.getObjectField<jstring>("STRING_OBJECT_VAR"); QJniObject res = obj.getObjectField<jstring>("STRING_OBJECT_VAR");
QCOMPARE(res.toString(), testValue.toString()); QCOMPARE(res.toString(), testValue.toString());
// as of Qt 6.7, we can set and get strings directly
obj.setField("STRING_OBJECT_VAR", qString);
QCOMPARE(obj.getField<QString>("STRING_OBJECT_VAR"), qString);
} }
template <typename T> template <typename T>
@ -1127,11 +1150,17 @@ void tst_QJniObject::setStaticBooleanField()
void tst_QJniObject::setStaticObjectField() void tst_QJniObject::setStaticObjectField()
{ {
QJniObject testValue = QJniObject::fromString(QStringLiteral("Hello")); const QString qString = u"Hello"_s;
QJniObject testValue = QJniObject::fromString(qString);
QJniObject::setStaticField(testClassName, "S_STRING_OBJECT_VAR", testValue.object<jstring>()); QJniObject::setStaticField(testClassName, "S_STRING_OBJECT_VAR", testValue.object<jstring>());
QJniObject res = QJniObject::getStaticObjectField<jstring>(testClassName, "S_STRING_OBJECT_VAR"); QJniObject res = QJniObject::getStaticObjectField<jstring>(testClassName, "S_STRING_OBJECT_VAR");
QCOMPARE(res.toString(), testValue.toString()); QCOMPARE(res.toString(), testValue.toString());
// 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);
} }
void tst_QJniObject::templateApiCheck() void tst_QJniObject::templateApiCheck()