I'm migrating QScriptEngine
code over to QJSEngine
, and have come across a problem where I can't call functions after evaluating scripts:
#include <QCoreApplication>
#include <QtQml>
int main(int argc, char *argv[])
{
QCoreApplication a(argc, argv);
QJSEngine engine;
QJSValue evaluationResult = engine.evaluate("function foo() { return \"foo\"; }");
if (evaluationResult.isError()) {
qWarning() << evaluationResult.toString();
return 1;
}
if (!evaluationResult.hasProperty("foo")) {
qWarning() << "Script has no \"foo\" function";
return 1;
}
if (!evaluationResult.property("foo").isCallable()) {
qWarning() << "\"foo\" property of script is not callable";
return 1;
}
QJSValue callResult = evaluationResult.property("foo").call();
if (callResult.isError()) {
qWarning() << "Error calling \"foo\" function:" << callResult.toString();
return 1;
}
qDebug() << "Result of call:" << callResult.toString();
return 0;
}
The output of this script is:
Script has no "activate" function
That same function could be called when I was using QScriptEngine
:
scriptEngine->currentContext()->activationObject().property("foo").call(scriptEngine->globalObject());
Why doesn't the function exist as a property of the evaluation result, and how do I call it?
That code will result in
foo()
being evaluated as a function declaration in the global scope. Since you don't call it, the resultingQJSValue
isundefined
. You can see the same behaviour by opening the JavaScript console in your browser and writing the same line:You can't call the function
foo()
ofundefined
, because it doesn't exist. What you can do, is call it through the global object:This is the same as what your C++ code sees. Therefore, to access and call the
foo()
function, you need to access it through the globalObject() function ofQJSEngine
:The output of this code is:
This is roughly the same as the line you posted that uses
QScriptEngine
.The benefit of this approach is that you don't need to touch your scripts to get it to work.
The downside is that writing JavaScript code this way can cause issues if you're planning on reusing the same
QJSEngine
to call multiple scripts, especially if the functions therein have identical names. Specifically, the objects that you evaluated will stick around in the global namespace forever.QScriptEngine
had a solution for this problem in the form ofQScriptContext
:push()
a fresh context before you evaluate your code, andpop()
afterwards. However, no such API exists inQJSEngine
.One way around this problem is to just create a new
QJSEngine
for every script. I haven't tried it, and I'm not sure how expensive it would be.The documentation looked like it might hint at another way around it, but I didn't quite understand how it would work with multiple functions per script.
After speaking with a colleague, I learned of an approach that solves the problem using an object as an interface:
The output of this code is:
You can read about this approach in detail in the article that I just linked to. Here's a summary of it:
exports
), allowing code outside of the function to create it and store it in a variable ((this.object = {})
).However, as the article states, this approach does still use the global scope:
If you want to take it further, follow the article through to its end. As long as you're using unique object names, though, it will be fine.
Here's an example of how a "real life" script would change to accommodate this solution:
Before
After
A
Car
script can have functions with the same names (activate
,destroy
, etc.) without affecting those ofPistol
.As of Qt 5.12,
QJSEngine
has support for proper JavaScript modules:All that needs to be done is to rename the file to have an
.mjs
extension, and then convert the code like so:The C++ to call one of these functions looks something like this: