Search code examples
c++multithreadingqtcallbackqtwebengine

QtWebEngine - synchronously execute JavaScript to read function result


I have the following method in one of my C++ classes (using QtWebEngine):

    QString get()
    {
        QString result;

        view->page()->runJavaScript("test();", [this](const QVariant &v)
            {
                result = v.toString();
            });

        return result;
    }

It is to execute test() JS function and return the result of this invocation.

Unfortunately, the callback is asynchronous and the program crashes. How can I make it work?


Solution

  • The callback is asynchronous because the JavaScript execution occurs not only in another thread but in another process. So there is no way to make it fully synchronous.

    The best possible solution would be to migrate your C++ code to work asynchronously. If you can't do that, the only feasible solution is to use QEventLoop, somewhat like this:

    void ranJavaScript()
    {
        emit notifyRanJavaScript();
    }
    
    QString get()
    {
        QString result;
        QEventLoop loop;
        QObject::connect(this, SIGNAL(notifyRanJavaScript()), &loop, SLOT(quit()));
        view->page()->runJavaScript("test();", [this](const QVariant &v)
            {
                result = v.toString();
                this.ranJavaScript();
            });
    
        loop.exec();
        return result;
    }
    

    However, note that this example is oversimplified for a real-world usage: you need to ensure the JavaScript was not ran before the event loop is started. The most proper way to do that would involve implementing a proper slot instead of a lambda + factoring out the call to view->page()->runJavaScript() into another slot which would be called asynchronously after starting the event loop. It is a lot of glue code for such a seemingly simple task but that's what it takes. Here's an example:

    MainWindow.h

    #ifndef TMP_MAIN_WINDOW_H
    #define TMP_MAIN_WINDOW_H
    
    #include <QMainWindow>
    #include <QVariant>
    
    class QWebEngineView;
    class QPushButton;
    
    class MainWindow : public QMainWindow
    {
        Q_OBJECT
    public:
        MainWindow(QWidget * parent = 0);
    
        QString get();
    
        void onScriptEnded(const QVariant & data);
    
    Q_SIGNALS:
        void notifyRanJavaScript();
    
    private Q_SLOTS:
        void onButtonPressed();
    
        void startScript();
    
    private:
        QWebEngineView *    m_view;
        QPushButton *       m_button;
        QString             m_scriptResult;
    };
    
    #endif // TMP_MAIN_WINDOW_H
    

    MainWindow.cpp

    #include "MainWindow.h"
    #include <QWebEngineView>
    #include <QPushButton>
    #include <QHBoxLayout>
    #include <QVBoxLayout>
    #include <QMessageBox>
    #include <QEventLoop>
    #include <QDebug>
    #include <QTimer>
    
    MainWindow::MainWindow(QWidget * parent) :
        QMainWindow(parent)
    {
        m_view = new QWebEngineView;
        QWebEnginePage * page = new QWebEnginePage(m_view);
        m_view->setPage(page);
    
        QString html = QStringLiteral("<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01//EN\""
                                      "\"http://www.w3.org/TR/html4/strict.dtd\"><html>"
                                      "<head><h3>head</h3>\n</head>"
                                      "<script type=\"text/javascript\">function test() { return \"A!\"; }</script>"
                                      "<body>text\n</body></html>");
        m_view->page()->setHtml(html);
    
        m_button = new QPushButton;
        m_button->setMinimumWidth(35);
        m_button->setText(QStringLiteral("Test"));
        QObject::connect(m_button, SIGNAL(pressed()), this, SLOT(onButtonPressed()));
    
        QHBoxLayout * buttonLayout = new QHBoxLayout;
        buttonLayout->addWidget(m_button);
        buttonLayout->addStretch();
    
        QVBoxLayout * viewLayout = new QVBoxLayout;
        viewLayout->addLayout(buttonLayout);
        viewLayout->addWidget(m_view);
    
        QWidget * widget = new QWidget(this);
        widget->setLayout(viewLayout);
    
        setCentralWidget(widget);
    }
    
    QString MainWindow::get()
    {
        QEventLoop loop;
        QObject::connect(this, SIGNAL(notifyRanJavaScript()), &loop, SLOT(quit()));
    
        // Schedule the slot to run in 0 seconds but not right now
        QTimer::singleShot(0, this, SLOT(startScript()));
    
        // The event loop would block until onScriptEnded slot is executed
        loop.exec();
    
        // If we got here, the script has been executed and the result was saved in m_scriptResult
        return m_scriptResult;
    }
    
    void MainWindow::onScriptEnded(const QVariant & data)
    {
        qDebug() << QStringLiteral("Script ended: ") << data;
        m_scriptResult = data.toString();
        emit notifyRanJavaScript();
    }
    
    void MainWindow::onButtonPressed()
    {
        QString str = get();
        QMessageBox::information(this, QStringLiteral("Script result"), str,
                                 QMessageBox::StandardButton::Ok);
    }
    
    struct Functor
    {
        Functor(MainWindow & window) : m_window(window) {}
        void operator()(const QVariant & data)
        {
            m_window.onScriptEnded(data);
        }
        MainWindow & m_window;
    };
    
    void MainWindow::startScript()
    {
        qDebug() << QStringLiteral("Start script");
        m_view->page()->runJavaScript(QStringLiteral("test();"), Functor(*this));
    }