Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Can I use should.js with QtScript?

I'm using QtScript to automate parts of my application for development and testing purposes. I've come to the point where I want to test assertions, and based on "standalone assertion libraries?" and what I could find in Debian repositories, I went for Should.js.

I'm having trouble loading it into my Qt application, as it depends on Node's require() function. I tried implementing a version of this, starting from "Supporting require() of CommonJS" and ending up with the code below.

Can it be made to work, or am I doomed in this approach? Would I perhaps be better off copying the bits of should.js into a single file? I'd prefer not to make myself responsible for keeping a fork up to date. (Licensing is a non-issue, as I don't intend to redistribute this code).

Here's my MCVE; sorry I couldn't get it any shorter!

should.cpp

#include <QCoreApplication>
#include <QDateTime>
#include <QDebug>
#include <QDir>
#include <QFile>
#include <QFileInfo>
#include <QScriptEngine>
#include <QScriptContext>
#include <QScriptContextInfo>
#include <QTextStream>


// Primitive implementation of Node.js require().
// N.B. Supports only .js sources.
QScriptValue require(QScriptContext* context, QScriptEngine* engine)
{
    const QString moduleName = context->argument(0).toString();

    // First, look in our modules cache
    QScriptValue modules = engine->globalObject().property("$MODULES");
    QScriptValue module = modules.property(moduleName);
    if (module.isValid()) {
        auto cached_file = module.property("filename");
        auto time_stamp = module.property("timestamp");
        auto code = module.property("code");
        if (code.isObject() && cached_file.isString() && time_stamp.isDate()) {
            if (QFileInfo(cached_file.toString()).lastModified() == time_stamp.toDateTime()) {
                qDebug() << "found up-to-date module for require of" << moduleName;
                return code;
            } else {
                qDebug() << "cache stale for" << moduleName;
            }
        }
    } else {
        // Prepare a cache entry, as some modules recursively include each
        // other.  This way, they at least get the partial definition of the
        // other, rather than a stack overflow.
        module = engine->newObject();
        modules.setProperty(moduleName, module);
    }

    qDebug() << "require" << moduleName;

    // resolve filename relative to the calling script
    QString filename = moduleName + ".js";
    for (auto *p = context;  p;  p = p->parentContext()) {
        QScriptContextInfo info(p);
        auto parent_file = info.fileName();
        if (parent_file.isEmpty())
            continue;
        // else, we reached a context with a filename
        QDir base_dir = QFileInfo(parent_file).dir();
        filename = base_dir.filePath(filename);
        if (QFile::exists(filename)) {
            break;
        }
    }

    QFile file(filename);
    if (!file.open(QIODevice::ReadOnly)) {
        return context->throwValue(QString("Failed to open %0").arg(moduleName));
    }

    QTextStream in(&file);
    in.setCodec("UTF-8");
    auto script = in.readAll();
    file.close();

#if 0
    // I had to disable this, because it barfs on "get not()" definition - is
    // that a Node extension?  Will it cause me problems even if I get require()
    // working?
    auto syntax_check = QScriptEngine::checkSyntax(script);
    if (syntax_check.state() != QScriptSyntaxCheckResult::Valid) {
        return context->throwValue(QString("%2:%0:%1: Syntax error: %3")
                                   .arg(syntax_check.errorLineNumber())
                                   .arg(syntax_check.errorColumnNumber())
                                   .arg(filename, syntax_check.errorMessage()));
    }
#endif

    // create a new context, and capture the module's exports
    QScriptContext* newContext = engine->pushContext();
    QScriptValue exports = engine->newObject();
    newContext->activationObject().setProperty("exports", exports);
    module.setProperty("code", exports);
    module.setProperty("filename", filename);
    module.setProperty("timestamp", engine->newDate(QFileInfo(filename).lastModified()));
    // run the script
    engine->evaluate(script, filename);
    // get the exports
    module.setProperty("code", newContext->activationObject().property("exports"));
    engine->popContext();
    if (engine->hasUncaughtException())
        return engine->uncaughtException();
    qDebug() << "loaded" << moduleName;
    return exports;
}


int main(int argc, char **argv)
{
    QCoreApplication app(argc, argv);
    QScriptEngine engine;

    // register global require() function
    auto global = engine.globalObject();
    global.setProperty("require", engine.newFunction(require));
    global.setProperty("$MODULES", engine.newObject());

    engine.evaluate("var should = require('/usr/lib/nodejs/should/lib/should');");

    if (engine.hasUncaughtException()) {
        qCritical() << engine.uncaughtException().toString().toStdString().c_str();
        qWarning() << engine.uncaughtExceptionBacktrace().join("\n").toStdString().c_str();
        return 1;
    }
    return 0;
}

Makefile

check: should
    ./should

CXXFLAGS += -std=c++11 -Wall -Wextra -Werror
CXXFLAGS += -fPIC
CXXFLAGS += $(shell pkg-config --cflags Qt5Script)
LDLIBS += $(shell pkg-config --libs Qt5Script)

The output is

require "/usr/lib/nodejs/should/lib/should" 
require "./util" 
require "./inspect" 
found up-to-date module for require of "./util" 
loaded "./inspect" 
require "assert" 
Failed to open assert 
<eval>() at /usr/lib/nodejs/should/lib/./util.js:126
<native>() at -1
<native>('./util') at -1
<eval>() at /usr/lib/nodejs/should/lib/should.js:8
<native>() at -1
<native>('/usr/lib/nodejs/should/lib/should') at -1
<global>() at 1

(In passing - how do I get the actual function name require in the stack trace instead of <native>? Slots manage this, so I should be able to, right?)

like image 650
Toby Speight Avatar asked Nov 26 '15 10:11

Toby Speight


1 Answers

I've looked into it with little more detail and rewriting C++ Qt require system is a little bit more time consuming for me then initially thought. There is also issue with libraries having require-ing core modules (which in turn require native modules which would result in undefined behavior - read: probably won't work).

Approach #1 - C++ require() implementation:

Implement custom node require() in C++ Qt like it has been started in your question and link. Details of workings of node.js require() can be found here. You'll need to have core node modules in your require() search path (you can get them from node.js source repository).

Approach #2 - Use browserify

Since the problem we are trying to solve in #1 is basically loading and caching javascript files, why not use something that already exists for the same purpose. This way we can avoid manual work and bundle javascript we have strong indication it will work on a browser (more limited environment then node.js).

$ npm install -g browserify
$ npm install expect

index.js

var expect = require('expect');
expect(1).toEqual(1);

And run browserify:

$ browserify index.js -o bundle.js

In your Qt C++:

QString script = loadFile("/path/to/bundle.js");
engine.evaluate(script);

We have come to a workaround for require() however I'm not certain of interop. with Qt. Also, I have encountered some Syntax Error from QtScript for some js modules so this is no silver bullet even on first it may seem so.

Note: this is also interesting project: https://github.com/svalaskevicius/qtjs-generator.

like image 149
edin-m Avatar answered Oct 13 '22 19:10

edin-m