bytedeco / javacpp

The missing bridge between Java and native C++
Other
4.44k stars 576 forks source link

Qt virtual QObject #419

Open drieks opened 3 years ago

drieks commented 3 years ago

Hi,

I'm trying to implement a custom event function in a class extending QApplication.

public native @Cast("bool") boolean eventFilter(QObject watched, QEvent event);

When overriding this function in java, the new implementation is not called.

Also, this function is missing on the java side:

@Virtual protected native void customEvent(QEvent event);

To fix this, I added this function to qt/src/main/java/org/bytedeco/qt/presets/Qt5Core.java:

@Override
protected String[] virtual() {
return new String[]{"QObject"};
}

I'm using mvn install -DskipTests=true -Djavacpp.cppbuild.skip to update the generated files. This changes will be created in qt/src/gen/java/org/bytedeco/qt/Qt5Core/QObject.java:

-    public native @Cast("bool") boolean event(QEvent event);
-    public native @Cast("bool") boolean eventFilter(QObject watched, QEvent event);
+    @Virtual public native @Cast("bool") boolean event(QEvent event);
+    @Virtual public native @Cast("bool") boolean eventFilter(QObject watched, QEvent event);

+    @Virtual protected native void timerEvent(QTimerEvent event);
+    @Virtual protected native void childEvent(QChildEvent event);
+    @Virtual protected native void customEvent(QEvent event);

But compiling fails:

/(...)/javacpp-presets/qt/target/native/org/bytedeco/qt/linux-x86_64/jniQt5Core.cpp: In function 'jboolean Java_org_bytedeco_qt_Qt5Core_QCoreApplication_event(JNIEnv*, jobject, jobject)':
/(...)/javacpp-presets/qt/target/native/org/bytedeco/qt/linux-x86_64/jniQt5Core.cpp:9555:152: error: 'virtual bool QCoreApplication::event(QEvent*)' is protected within this context
 9555 |         bool rval = (bool)(dynamic_cast<JavaCPP_QCoreApplication*>(ptr) != NULL ? ((JavaCPP_QCoreApplication*)ptr)->super_event(ptr0) : ptr->event(ptr0));
      |                                                                                                                                                        ^
In file included from /(...)/javacpp-presets/qt/target/native/org/bytedeco/qt/linux-x86_64/jniQt5Core.cpp:103:
/(...)/javacpp-presets/qt/cppbuild/linux-x86_64/include/QtCore/qcoreapplication.h:190:10: note: declared protected here
  190 |     bool event(QEvent *) override;
      |          ^~~~~

qobject.h:

public:
  virtual bool event(QEvent *event);

qcoreapplication.h:

protected:
    bool event(QEvent *) override;

Because it is protected in QCoreApplication, it can not be called here (jniQt5Core.cpp):

JNIEXPORT jboolean JNICALL Java_org_bytedeco_qt_Qt5Core_QCoreApplication_event(JNIEnv* env, jobject obj, jobject arg0) {
    QCoreApplication* ptr = (QCoreApplication*)jlong_to_ptr(env->GetLongField(obj, JavaCPP_addressFID));
    if (ptr == NULL) {
        env->ThrowNew(JavaCPP_getClass(env, 6), "This pointer address is NULL.");
        return 0;
    }
    jlong position = env->GetLongField(obj, JavaCPP_positionFID);
    ptr += position;
    QEvent* ptr0 = arg0 == NULL ? NULL : (QEvent*)jlong_to_ptr(env->GetLongField(arg0, JavaCPP_addressFID));
    jlong position0 = arg0 == NULL ? 0 : env->GetLongField(arg0, JavaCPP_positionFID);
    ptr0 += position0;
    jboolean rarg = 0;
    jthrowable exc = NULL;
    try {
        bool rval = (bool)(dynamic_cast<JavaCPP_QCoreApplication*>(ptr) != NULL ? ((JavaCPP_QCoreApplication*)ptr)->super_event(ptr0) : ptr->event(ptr0));
        rarg = (jboolean)rval;
    } catch (...) {
        exc = JavaCPP_handleException(env, 9);
    }

    if (exc != NULL) {
        env->Throw(exc);
    }
    return rarg;
}

In this line: bool rval = (bool)(dynamic_cast<JavaCPP_QCoreApplication*>(ptr) != NULL ? ((JavaCPP_QCoreApplication*)ptr)->super_event(ptr0) : ptr->event(ptr0));, when ptr is of type QCoreApplication, ptr->event is not valid, because it is protected.

I tried this changes: In file org.bytedeco.qt.presets.Qt5Core, in function public void map(InfoMap infoMap) I added

.put(new Info("QObject::event").javaNames("someExampleName"))

this will rename the function as expected. But changing it to:

.put(new Info("QCoreApplication::event").javaNames("someExampleName"))

nothing will be changed. Also

.put(new Info("QCoreApplication::event").skip())

and adding "QCoreApplication::event" to protected String[] skip() is not possible.

Maybe changing ptr->event(ptr0) to ((QObject*)ptr)->event(ptr0) will work?

Is there a way to fix this?

saudet commented 3 years ago

Maybe changing ptr->event(ptr0) to ((QObject*)ptr)->event(ptr0) will work?

Wow, yes, that does work! We'll just need to figure out where to plug this in here: https://github.com/bytedeco/javacpp/blob/master/src/main/java/org/bytedeco/javacpp/tools/Generator.java And make sure it doesn't break anything else, but you can leave that to me. :)

saudet commented 3 years ago

I've fixed this issue and virtualized QObject. Please give it a try with 1.5.4-SNAPSHOT: http://bytedeco.org/builds/ And thanks for reporting and for investigating!

drieks commented 3 years ago

Hi @saudet,

thank you for the patch!

I already tried a manually patched version and I found another problem. My Plan is to write a class MyApplication that extends QApplication and override customEvent. main will call QApplication.exec() (It's a static function). exec will call event from C++ on the custom QApplication instance. The Problem here is, that the Pointer to MyApplication is no instance of JavaCPP_QObject, so the dynamic cast is not possible. But when casting to QObject and invoking event there, the same JNI function is called again - stack overflow.

A way to fix this is extending all JavaCPP_* classes by the JavaCPP-Class of the base class:

-class JavaCPP_hidden JavaCPP_QCoreApplication : public QCoreApplication {
+class JavaCPP_hidden JavaCPP_QCoreApplication : public QCoreApplication, public JavaCPP_QObject {

In this case, my MyApplication extending (java-)QApplication will point to JavaCPP_QCoreApplication, but the dynamic cast to JavaCPP_QObject is possible. This can be implemented by adding @Virtual to all classes having virtual functions (currently, @Virtual is only allowed for functions). When generating the code from the diff, we can check if (java-)QApplication (or one of this super classes) is annotated with @Virtual. Because in this case, it is annotated, we can just add a second base class pointing to the JavCPP version of the superclass (JavaCPP_QObject in the diff).

saudet commented 3 years ago

If you're subclassing QApplication, then we need to virtualize() that one as well, and it should work fine.

drieks commented 3 years ago

Hi @saudet ,

I added an example, please have a look. It should just print "customEvent", but it will crash with a stack overflow.

The Event Loop QApplication::exec() will deliver the new Event() to application by invoking event.

Because event is virtual, this JNI function will be called:

JNIEXPORT jboolean JNICALL Java_org_bytedeco_qt_Qt5Core_QCoreApplication_event(JNIEnv* env, jobject obj, jobject arg0) {
    QCoreApplication* ptr = (QCoreApplication*)jlong_to_ptr(env->GetLongField(obj, JavaCPP_addressFID));
    if (ptr == NULL) {
        env->ThrowNew(JavaCPP_getClass(env, 6), "This pointer address is NULL.");
        return 0;
    }
    jlong position = env->GetLongField(obj, JavaCPP_positionFID);
    ptr += position;
    QEvent* ptr0 = arg0 == NULL ? NULL : (QEvent*)jlong_to_ptr(env->GetLongField(arg0, JavaCPP_addressFID));
    jlong position0 = arg0 == NULL ? 0 : env->GetLongField(arg0, JavaCPP_positionFID);
    ptr0 += position0;
    jboolean rarg = 0;
    jthrowable exc = NULL;
    try {
        bool rval = (bool)(dynamic_cast<JavaCPP_QCoreApplication*>(ptr) != NULL ? ((JavaCPP_QCoreApplication*)ptr)->super_event(ptr0) : ((QObject*)ptr)->event(ptr0));
        rarg = (jboolean)rval;
    } catch (...) {
        exc = JavaCPP_handleException(env, 9);
    }

    if (exc != NULL) {
        env->Throw(exc);
    }
    return rarg;
}

Here, QCoreApplication* ptr is a Pointer to a instance of JavaCPP_QCoreApplication (because JavaCPP_QCoreApplication extends QCoreApplication). The dynamic cast to JavaCPP_JObject will not work, beause JavaCPP_QCoreApplication implements QObject, but not JavaCPP_JObject. So the fallback cast of ptr to QObject* will be used, this is valid. But invoking this function will lead in a stack overflow, because it is a pointer to the actually running function.

saudet commented 3 years ago

Actually, there was another issue with JavaCPP not picking up virtual methods specified with override instead of virtual. I've fixed that in the latest commit, and I've also updated the presets for Qt in commit https://github.com/bytedeco/javacpp-presets/commit/0b485f5ecca1e1c90cecfff11c9441abc9a6245f to virtualize all subclasses of QObject.

Your ExampleApplication seems to work fine with those changes. Could you confirm and let me know if you encounter any other problems? Thanks!

drieks commented 3 years ago

Hi @saudet, thank you very much, the virtual stuff is working now!

But there is still another problem. When you set a breakpoint in function customEvent in my example application, you can see, that the event argument is of type QEvent, not of expected type Event. But the address is the same, so the passed pointer seems to be fine. (I updated my example code)

drieks commented 3 years ago

https://raw.githubusercontent.com/drieks/javacpp-presets/master/qt/samples/ExampleApplication.java

saudet commented 3 years ago

Right, that's expected. We'd have to add some sort of introspection mechanism.

saudet commented 3 years ago

The easiest thing to do for now is to use the address as a key to a HashMap, like this: https://github.com/bytedeco/javacv/blob/master/src/main/java/org/bytedeco/javacv/FFmpegFrameRecorder.java#L311-L327