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!
#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;
}
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?)
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).
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).
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With