1+1=10

扬长避短 vs 取长补短

QJSEngine未文档化的哪些事

接前面

继续看看QJSEngine(只关注QJSEngine,不关注QQmlEngine)文档中没有提及的一些内容。我只是走马观花似的看了看手册,可能很多东西手册中有,只是我没看到。

本文内容在Qt6.5下验证。

纠结

真想将QJSEngine用起来,雷还是很多,看来一时半会是趟不完了...

  • QSize,QPoint,QRect 在js下不能直接用,如何设置它们的值?
  • Qt命名空间中有大量的枚举量,在js下如何才能使用?
  • 有多少种方式定义自己的枚举量,并引入到js环境下?
  • 如何把QByteArray传到js中,如何在js下使用?
  • Int8Array在Qt手册没有却可以用。那么QJSEngine支持的内置对象,到底有哪些?
  • 浏览器和Nodejs中都有setTimeout和setInterval,怎么给QJSEngine加上?
  • ...

字符串值("400x300")?

一个小问题:在JS下,我想修改一个Widget的大小和位置,怎么办?

手册中提了,但又没提

Qt手册中提了这么一句:

For example, the Image::sourceSize property is of type size (which automatically translates to the QSize type) and can be specified by a string value formatted as "widthxheight", or by the Qt.size() function

而且,在qml各个类型中,确实也有详细说明:

  • https://doc.qt.io/qt-6/qml-size.html
  • https://doc.qt.io/qt-6/qml-point.html
  • https://doc.qt.io/qt-6/qml-rect.html
  • https://doc.qt.io/qt-6/qml-date.html
  • https://doc.qt.io/qt-6/qml-color.html
  • https://doc.qt.io/qt-6/qml-font.html

但是这些说法都是针对QQmlEngine的,而不是QJSEngine。

源码

要在js中使用QSize的话,Qt.size()这个写法,需要依赖QQmlEngine,不过"widthxheight"这个写法在QJSEgnine可以工作:

label.size = "400x300"

到底还有多少类型可以这么用,具体格式什么样子。手册靠不住,只能上源码:

  • QSize, QSizeF:"widthxheight"
  • QPoint, QPointF:"x,y"
  • QRect, QRectF:”x,y,widthxheight“
  • QDate, QTime, QDateTime:符合 Qt::ISODate 要求的字符串,比如:”yyyy-MM-ddTHH:mm:ss“

注:具体请参照源码 qtdeclarative/src/qml/qml/qqmlstringconverters.cpp

Qt::staticMetaObject 用途?

我们知道,Qt命令空间中有大量的枚举量,比如

  • Qt::Checked
  • Qt::QueuedConnection
  • Qt::white
  • ...

但是,在js中无法直接使用。怎么办?

引入到js

这是Qt6引入的一个东西,在Qt5中,有一个类似的东西叫QObject::staticQtMetaObjectQObject::qt_getQtMetaObject()

使用如下语句可以将其引入到js引擎:

engine.globalObject().setProperty("Db", engine.newQMetaObject(&Qt::staticMetaObject));

而后可以在一定程度放飞自我,比如,在js中可以使用(不用Qt是我怕以后会冲突):

  • Db.Checked
  • Db.QuquedConnectionn
  • Db.white
  • ...

这部分内容和 Q_NAMESPACE 以及 Q_ENUM_NS 相关。

自定义枚举量

js引擎使用Qt的staticMetaObject实现反射功能。那么需要思考,哪些方法可以实现元对象:

  • QObject派生类与Q_OBJECT
  • 非QObject派生类与Q_GADGET
  • 不创建类 用Q_NAMESPACE
  • 直接动态创建(需要借助Qt中private的API,详见corelib/kernel/qmetaobjectbuilder_p.h

前面三个,官方有文档,构建系统都会自动调用moc来自动生成元对象。此处简单关注一下最后一个。

一个完整的例子如下:

#include <QCoreApplication>
#include <QJSEngine>
#include <private/qmetaobjectbuilder_p.h>
#include <QMetaEnum>


const char *js = R"js(
console.log(test.second) // 1
console.log(test.third) // 2
)js";

int main(int argc, char *argv[])
{
    QCoreApplication app(argc, argv);

    QMetaObjectBuilder b;
    b.setClassName("Test");
    b.setSuperClass(&QObject::staticMetaObject);
    auto eb = b.addEnumerator("DEBAO");
    eb.addKey("first", 0);
    eb.addKey("second", 1);
    eb.addKey("third", 2);

    auto test = b.toMetaObject();

    QJSEngine engine;
    engine.installExtensions(QJSEngine::ConsoleExtension);
    engine.globalObject().setProperty("test", engine.newQMetaObject(test));

    auto ret = engine.evaluate(js);
    if (ret.isError())
        qDebug()<<ret.toString();

    return 0;
}

输出结果:

js: 1
js: 2

注意,因为用到私有API。qmake下需要QT+=core-private,cmake下需要在链接target中加入CorePrivate

ArrayBuffer怎么用?

Qt手册函数列表中提到了ArrayBuffer和DataView,但是QJSEngine手册中没有newArrayBuffer()的API,也没有说如何从Qt中传进来一个ArrayBuffer。

这页手册提到一句 :QByteArray和JavsScript中的 ArrayBuffer 可以互相转换。但没有例子。

不妨写个小例子试试看:

#include <QCoreApplication>
#include <QJSEngine>

const char *js = R"js(
console.log(typeof data);
console.log(data instanceof ArrayBuffer);
console.log("data: " + data);

let dataView = new DataView(data);
for (let i = 0; i < dataView.byteLength; i++) {
    console.log("dataView[" + i + "]: " + dataView.getUint8(i));
}
)js";

int main(int argc, char *argv[])
{
    QCoreApplication app(argc, argv);

    QJSEngine engine;
    engine.installExtensions(QJSEngine::ConsoleExtension);

    QByteArray data("1+1=10");
    engine.globalObject().setProperty("data", engine.toScriptValue(data));

    auto ret = engine.evaluate(js);
    if (ret.isError())
        qDebug()<<ret.toString();

    return 0;
}
  • 使用QJSEngine::toScriptValue() 将QByteArray传入到js中
  • 在JS中使用DataView来处理数据

程序结果如下:

js: object
js: true
js: data: 1+1=10
js: dataView[0]: 49
js: dataView[1]: 43
js: dataView[2]: 49
js: dataView[3]: 61
js: dataView[4]: 49
js: dataView[5]: 48

秘密

为什么QByteArray使用QJSEngine::toScriptValue()传进来以后,能作为ArrayBuffer使用。秘密在qtdeclarative/src/qml/jsruntime/qv4engine.cpp的QV4::ReturnedValue ExecutionEngine::fromData()中:

  • 遇到QByteArray会使用newArrayBuffer()
  • 遇到QString、QChar、Char16会使用newString()
  • 遇到QRegularExpression:会使用newRegExpObj()
  • 遇到QDateTime、QDate、QTime会使用newDateObject()
  • 遇到QStringList、QVariantList、QJsonArray会直接或间接使用 newArrayObject()
  • 遇到QVariantMap、QJsonObject会直接或间接使用newObject()
  • 遇到QPIxmap、QImage,它创建了VariantObject(Object派生类)对象??
  • 遇到QVariant会解包,再处理
  • ...

其他

注意,例子中js中DataView部分也可以用Int8Array,不同于DataView,这个标准类型在Qt手册中完全没有出现:

let dataView = new Int8Array(data);
for (let i = 0; i < dataView.byteLength; i++) {
    console.log("dataView[" + i + "]: " + dataView[i]);
}

既然和手册对不上,那么需要思考:QJSEngine中到底实现了哪些内置对象?

内置对象有哪些?

Qt手册中给出了内置对象的列表,但是不全,比如Int8Array等,手册中完全没有:

#include <QCoreApplication>
#include <QJSEngine>
#include <QJSValueIterator>

const char *js = R"js(
let myGlobal = this;
console.log(myGlobal.Math === Math)
for (let k in myGlobal) {
    console.log(k);
}
)js";

int main(int argc, char *argv[])
{
    QCoreApplication app(argc, argv);

    QJSEngine engine;
    QJSValueIterator it(engine.globalObject());
    while (it.hasNext()) {
        it.next();
        qDebug() << it.name();
    }

    return 0;
}

输出结果

true
"Object"
"String"
"Symbol"
"Number"
"Boolean"
"Array"
"Function"
"Date"
"RegExp"
"Error"
"EvalError"
"RangeError"
"ReferenceError"
"SyntaxError"
"TypeError"
"URIError"
"Promise"
"URL"
"URLSearchParams"
"SharedArrayBuffer"
"ArrayBuffer"
"DataView"
"WeakSet"
"Set"
"WeakMap"
"Map"
"Int8Array"
"Uint8Array"
"Int16Array"
"Uint16Array"
"Int32Array"
"Uint32Array"
"Uint8ClampedArray"
"Float32Array"
"Float64Array"
"Atomics"
"Math"
"JSON"
"Reflect"
"Proxy"
"undefined"
"NaN"
"Infinity"
"eval"
"parseInt"
"parseFloat"
"isNaN"
"isFinite"
"decodeURI"
"decodeURIComponent"
"encodeURI"
"encodeURIComponent"
"escape"
"unescape"

缺失的 setTimeout() ?

我有这么一个javascript程序:

setTimeout(function() {
    console.log("Hello 1+1=2!");
}, 2000);

setTimeout(function(name) {
    console.log("Hello", name);
}, 1000, "1+1=10!");

let id = setTimeout(function() {
    console.log("Hello, you cannot see me!");
}, 1000);
clearTimeout(id);

console.log("Hello Debao!");

在Node.js下和在浏览器下都能常常运行,但是QJSEngine下,无法运行。怎么办??

直接原因

尽管,无论是浏览器Web API还是Node.js,都提供了以下几个全局函数:

  • setTimeout()
  • clearTimeout()
  • setInterval()
  • clearInterval()

但是这几个函数不属于ECMAScript规范,QJSEngine并也没有提供它们,也完全没问题。

QJSEngine对 控制台输出console、国际化qsTr等提供了内置的扩展,但没有为setTimeout()提供对应扩展。

能不能手动扩展 Extension

早期的QScriptEngine提供有自定义globalObject的接口,但是 QJSEngine 似乎并没有为用户提供 扩展接口的机制。

可以看看代码,位于qtdeclarative/src/qml/qml/qqmlbuiltinfunctions.cpp

void QV4::GlobalExtensions::init(Object *globalObject, QJSEngine::Extensions extensions)
{
    ExecutionEngine *v4 = globalObject->engine();
    Scope scope(v4);
    if (extensions.testFlag(QJSEngine::TranslationExtension)) {
        //...
        globalObject->defineDefaultProperty(QStringLiteral("qsTranslate"), 
       //...
    }
    if (extensions.testFlag(QJSEngine::ConsoleExtension)) {
        globalObject->defineDefaultProperty(QStringLiteral("print"), QV4::ConsoleObject::method_log);
        QV4::ScopedObject console(scope, globalObject->engine()->memoryManager->allocate<QV4::ConsoleObject>());
        globalObject->defineDefaultProperty(QStringLiteral("console"), console);
    }
    if (extensions.testFlag(QJSEngine::GarbageCollectionExtension)) {
        globalObject->defineDefaultProperty(QStringLiteral("gc"), QV4::GlobalExtensions::method_gc);
    }
}

用 QTimer 封装一下

尽管 Qt中的QTimer拥有上面四个函数的功能——单次触发设置/解除、多次触发设置/接触,但是QTimer在js也不能直接使用。

把QTimer封装一下,然后再暴露给QJSEngine,不难实现:

class Utils : public QObject
{
    Q_OBJECT

    QMap<int, QTimer *> m_timers;
public:
    Q_INVOKABLE QTimer *newQTimer(int msecs, bool isSingleShot) {
        auto t = new QTimer(this);
        t->setInterval(msecs);
        t->setSingleShot(isSingleShot);
        t->start();
        m_timers.insert(t->timerId(), t);
        return t;
    }

    Q_INVOKABLE bool killQTimer(QTimer *t) {
        if (t) {
            t->stop();
            t->deleteLater();
            m_timers.remove(t->timerId());
        }
        return true;
    }

    Q_INVOKABLE int getQTimerId(QTimer *timer) {
        return timer->timerId();
    }

    Q_INVOKABLE bool killQTimerById(int id) {
        auto t = m_timers.value(id, nullptr);
        return killQTimer(t);
    }
};

而后,我们可以用javascript定义我们想要的函数setTimeout()和clearTimeout():

const char *initJs = R"js(
function setTimeout(func, delay, ...args) {
    var t = utils.newQTimer(delay, true);
    t.timeout.connect(function() {
        func.apply(this, args);
        utils.killQTimer(t);
    });
    return utils.getQTimerId(t);
}

function clearTimeout(id) {
    utils.killQTimerById(id);
}
)js";

有了这两个东西,我们就可以直接用Qt来跑本节开头提到的Javascript程序了:

#include <QCoreApplication>
#include <QJSEngine>
#include <QTimer>
#include <QMap>

// ...
// ...

const char *js = R"js(
setTimeout(function() {
    console.log("Hello 1+1=2!");
}, 2000);

setTimeout(function(name) {
    console.log("Hello", name);
}, 1000, "1+1=10!");

let id = setTimeout(function() {
    console.log("Hello, you cannot see me!");
}, 1000);
clearTimeout(id);

console.log("Hello Debao!");
)js";

int main(int argc, char *argv[])
{
    QCoreApplication app(argc, argv);

    QJSEngine engine;
    engine.installExtensions(QJSEngine::ConsoleExtension);
    engine.globalObject().setProperty("utils", engine.newQObject(new Utils));
    auto ret = engine.evaluate(initJs);
    if (ret.isError())
        qDebug()<<ret.toString();

    ret = engine.evaluate(js);
    if (ret.isError())
        qDebug()<<ret.toString();

    QTimer::singleShot(5*1000, &app, &QCoreApplication::quit);
    return app.exec();
}

#include "main.moc"

结果如下:

js: Hello Debao!
js: Hello 1+1=10!
js: Hello 1+1=2!
  1. setInterval() 与 clearInterval()的实现方式和这个完全一样,demo中不再重复。
  2. 可以从QJSEngine派生一下,将这部分内容放到派生类中。
  3. 借助事件循环,我们也可以给utils添加一个不卡死线程的sleep()函数
Q_INVOKABLE void sleep(int msecs)
{
    QEventLoop evtLoop;
    QTimer::singleShot(ms, &evtLoop, &QEventLoop::quit);
    evtLoop.exec();
}

setObjectOwnership()?

挺关键的一个函数,即使对10行左右的demo程序,一个QObject对象如果在栈上,不设置合适的的Ownership程序退出时会崩溃的。

这个,在Qt6中,其实是有文档的:

void QJSEngine::setObjectOwnership(QObject *object, QJSEngine::ObjectOwnership ownership)

但是,我想说的时,在Qt5.x中,它却位于QQmlEnine的静态函数:

void QQmlEngine::setObjectOwnership(QObject *object, QQmlEngine::ObjectOwnership ownership)

尽管有些古怪,在Qt5下使用QJSEngine时,是可以直接使用QQmlEngine的setObjectOwnership()成员的。

  • https://stackoverflow.com/questions/26177693/qjsengine-deletes-my-qobject-how-to-change-ownership-after-qjsenginenewqobjec

参考

  • https://doc.qt.io/qt-6/qjsengine.html
  • https://doc.qt.io/qt-6/qtqml-cppintegration-data.html
  • https://doc.qt.io/qt-6/qml-qtqml-qt.html
  • https://codebrowser.dev/qt6/qtdeclarative/src/qml/jsruntime/qv4engine.cpp.html
  • https://www.kdab.com/diy-moc-dynamic-meta-objects/
  • https://stackoverflow.com/questions/11236970/what-is-the-equivalent-of-javascripts-settimeout-on-qtscript
  • https://developer.mozilla.org/en-US/docs/Web/API/setTimeout
  • https://nodejs.org/en/learn/asynchronous-work/discover-javascript-timers

Comments