c++qtqt5qjsengine

QJSEngine - exposing classes and throwing errors


I am trying to create a standard JS library that is mostly shaped like Qbs (which uses deprecated QScriptEngine) with QJSEngine, so people who make Qt software can add things like file-operations to their plugin JS environment.

You can see the repo here

I've got basic classes exposed to the JS engine, like this:

QJSEngine jsEngine;
jsEngine.installExtensions(QJSEngine::AllExtensions);

jsEngine.globalObject().setProperty("BinaryFile", jsEngine.newQMetaObject(&Qbs4QJS::BinaryFile::staticMetaObject));

but I can's seem to figure out how to get a reference to the QJSEngine, inside a function, so I can throw an error:

Q_INVOKABLE BinaryFile(const QString &filePath, QIODevice::OpenModeFlag mode = QIODevice::ReadOnly) {
    m_file = new QFile(filePath);
    if (!m_file->open(mode)) {
        // how do I get jsEngine, here
        jsEngine->throwError(m_file->errorString());
    }
}

I'd like it if I could somehow derive the calling engine from inside the function, so the class could be exposed to several separate engine instances, for example.

I saw QScriptable and it's engine() method, but couldn't figure out how to use it.

I added

Depends { name: "Qt.script" }

in my qbs file, and

#include <QtScript>

but it still isn't throwing the error with this (just fails silently):

#include <QObject>
#include <QString>
#include <QFile>
#include <QIODevice>
#include <QFileInfo>
#include <QtScript>

namespace Qbs4QJS {

class BinaryFile :  public QObject, protected QScriptable
{
    Q_OBJECT

public:
    Q_ENUM(QIODevice::OpenModeFlag)

    Q_INVOKABLE BinaryFile(const QString &filePath, QIODevice::OpenModeFlag mode = QIODevice::ReadOnly) {
        m_file = new QFile(filePath);
        // should check for false and throw error with jsEngine->throwError(m_file->errorString());
        if (!m_file->open(mode)){
            context()->throwError(m_file->errorString());
        }
    }

private:
    QFile *m_file = nullptr;
};

} // end namespace Qbs4QJS

I may be confused about it, too, but it seems like it's using QScriptEngine, which I'm trying to get away from.

What is the best way to accomplish the task of adding a class that QJSEngine can use, which has cpp-defined methods that can throw errors in the calling engine?


Solution

  • The object under construction does not have any association with QJSEngine yet. So you can only do one of the following alternatives:

    1. Store the engine instance in a static variable if you can ensure that there is only ever one instance of QJSEngine in your whole application.
    2. Store the engine instance in a thread-local variable (QThreadStorage) if you can ensure that there is only one engine per thread.
    3. Set the current active engine in the current thread right before evaluating your JS code since. This might be the easiest and yet robust solution.
    4. Retrieve the engine from a QJSValue parameter.
    5. Implement a JS wrapper for the constructor

    Solution 4.: Passing the engine implicitly via a QJSValue parameter.

    I assume that your throwing constructor always has a parameter. QJSValue has a (deprecated) method engine() which you then could use. You can replace any parameter in a Q_INVOKABLE method with QJSValue instead of using QString and friends.

    class TextFileJsExtension : public QObject
    {
        Q_OBJECT
    public:
        Q_INVOKABLE TextFileJsExtension(const QJSValue &filename);
    };
    
    TextFileJsExtension::TextFileJsExtension(const QJSValue &filename)
    {
        QJSEngine *engine = filename.engine();
        if (engine)
            engine->throwError(QLatin1String("blabla"));
    }
    

    I guess there is a reason why it is deprecated, so you could ask the QML team, why and what alternative you could use.

    Solution 5 Implement a JS wrapper for the constructor

    This builds upon solution 4. and works even for parameter-less constructors. Instead of registering your helper class directly like this:

        engine->globalObject().setProperty("TextFile", engine->newQMetaObject(&TextFile::staticMetaObject));
    

    You could write an additional generator class and a constructor wrapper in JS. Evaluate the wrapper and register this function as the constructor for your class. This wrapper function would pass all desired arguments to the factory method. Something like this:

    engine->evaluate("function TextFile(path) { return TextFileCreator.createObject(path);
    

    TextFileCreator is a helper class that you would register as singleton. The createObject() method would then finally create the TextFile object and pass the engine as a paremter:

    QJSValue TextFileCreator::createObject(const QString &path)
    {
        QJSEngine *engine = qmlEngine(this);
        return engine->createQObject(new TextFile(engine, filePath));
    }
    

    This gives you access to the QJSEngine in the TextFile constructor and you can call throwError().