Результат оценки QJSEngine не содержит функции
Я мигрирую QScriptEngine
код для QJSEngine
и столкнулся с проблемой, когда я не могу вызывать функции после оценки скриптов:
#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;
}
Результат этого сценария:
Script has no "activate" function
Эта же функция может быть вызвана, когда я использовал QScriptEngine
:
scriptEngine->currentContext()->activationObject().property("foo").call(scriptEngine->globalObject());
Почему функция не существует как свойство результата оценки, и как я ее называю?
1 ответ
Этот код приведет к foo()
оценивается как объявление функции в глобальной области видимости. Поскольку вы не называете это, в результате QJSValue
является undefined
, Вы можете увидеть то же поведение, открыв консоль JavaScript в своем браузере и написав ту же строку:
Вы не можете вызвать функцию foo()
из undefined
потому что его не существует. Что вы можете сделать, это вызвать его через глобальный объект:
Это то же самое, что видит ваш код C++. Поэтому для доступа и вызова foo()
функция, вам нужно получить к ней доступ через функцию globalObject() QJSEngine
:
#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 (!engine.globalObject().hasProperty("foo")) {
qWarning() << "Script has no \"foo\" function";
return 1;
}
if (!engine.globalObject().property("foo").isCallable()) {
qWarning() << "\"foo\" property of script is not callable";
return 1;
}
QJSValue callResult = engine.globalObject().property("foo").call();
if (callResult.isError()) {
qWarning() << "Error calling \"foo\" function:" << callResult.toString();
return 1;
}
qDebug() << "Result of call:" << callResult.toString();
return 0;
}
Выход этого кода:
Result of call: "foo"
Это примерно так же, как строка, которую вы опубликовали, которая использует QScriptEngine
,
Преимущество этого подхода в том, что вам не нужно трогать свои скрипты, чтобы заставить его работать.
Недостатком является то, что написание кода JavaScript таким способом может вызвать проблемы, если вы планируете использовать то же самое QJSEngine
вызывать несколько сценариев, особенно если в них есть одинаковые имена. В частности, объекты, которые вы оценили, навсегда останутся в глобальном пространстве имен.
QScriptEngine
было решение этой проблемы в виде QScriptContext
: push()
свежий контекст, прежде чем оценивать свой код, и pop()
после этого. Однако такого API не существует вQJSEngine
,
Одним из способов решения этой проблемы является создание нового QJSEngine
для каждого сценария. Я не пробовал, и я не уверен, насколько дорого это будет.
Документация выглядела так, как будто она может намекнуть по-другому, но я не совсем понял, как она будет работать с несколькими функциями на скрипт.
Поговорив с коллегой, я узнал о подходе, который решает проблему с использованием объекта в качестве интерфейса:
#include <QCoreApplication>
#include <QtQml>
int main(int argc, char *argv[])
{
QCoreApplication a(argc, argv);
QJSEngine engine;
QString code = QLatin1String("( function(exports) {"
"exports.foo = function() { return \"foo\"; };"
"exports.bar = function() { return \"bar\"; };"
"})(this.object = {})");
QJSValue evaluationResult = engine.evaluate(code);
if (evaluationResult.isError()) {
qWarning() << evaluationResult.toString();
return 1;
}
QJSValue object = engine.globalObject().property("object");
if (!object.hasProperty("foo")) {
qWarning() << "Script has no \"foo\" function";
return 1;
}
if (!object.property("foo").isCallable()) {
qWarning() << "\"foo\" property of script is not callable";
return 1;
}
QJSValue callResult = object.property("foo").call();
if (callResult.isError()) {
qWarning() << "Error calling \"foo\" function:" << callResult.toString();
return 1;
}
qDebug() << "Result of call:" << callResult.toString();
return 0;
}
Выход этого кода:
Result of call: "foo"
Вы можете прочитать об этом подходе подробно в статье, на которую я только что ссылался. Вот краткое изложение этого:
- Объявляет объект, к которому можно добавлять свойства всякий раз, когда вы определяете что-то, что нужно "экспортировать" в C++.
- "Функция модуля" принимает объект интерфейса в качестве аргумента (
exports
), позволяя коду вне функции создавать его и сохранять в переменной ((this.object = {})
).
Однако, как говорится в статье, этот подход все еще использует глобальную область:
Предыдущий шаблон обычно используется модулями JavaScript, предназначенными для браузера. Модуль будет запрашивать одну глобальную переменную и упаковывать свой код в функцию, чтобы иметь собственное личное пространство имен. Но этот шаблон все еще вызывает проблемы, если несколько модулей имеют одно и то же имя или если вы хотите загрузить две версии модуля рядом друг с другом.
Если вы хотите продвинуться дальше, прочитайте статью до конца. Пока вы используете уникальные имена объектов, все будет хорошо.
Вот пример того, как сценарий "реальной жизни" может измениться, чтобы приспособиться к этому решению:
До
function activate(thisEntity, withEntities, activatorEntity, gameController, activationTrigger, activationContext) {
gameController.systemAt("WeaponComponentType").addMuzzleFlashTo(thisEntity, "muzzle-flash");
}
function equipped(thisEntity, ownerEntity) {
var sceneItemComponent = thisEntity.componentOfType("SceneItemComponentType");
sceneItemComponent.spriteFileName = ":/sprites/pistol-equipped.png";
var physicsComponent = thisEntity.componentOfType("PhysicsComponentType");
physicsComponent.width = sceneItemComponent.sceneItem.width;
physicsComponent.height = sceneItemComponent.sceneItem.height;
}
function unequipped(thisEntity, ownerEntity) {
var sceneItemComponent = thisEntity.componentOfType("SceneItemComponentType");
sceneItemComponent.spriteFileName = ":/sprites/pistol.png";
var physicsComponent = thisEntity.componentOfType("PhysicsComponentType");
physicsComponent.width = sceneItemComponent.sceneItem.width;
physicsComponent.height = sceneItemComponent.sceneItem.height;
}
function destroy(thisEntity, gameController) {
}
После
( function(exports) {
exports.activate = function(thisEntity, withEntities, activatorEntity, gameController, activationTrigger, activationContext) {
gameController.systemAt("WeaponComponentType").addMuzzleFlashTo(thisEntity, "muzzle-flash");
}
exports.equipped = function(thisEntity, ownerEntity) {
var sceneItemComponent = thisEntity.componentOfType("SceneItemComponentType");
sceneItemComponent.spriteFileName = ":/sprites/pistol-equipped.png";
var physicsComponent = thisEntity.componentOfType("PhysicsComponentType");
physicsComponent.width = sceneItemComponent.sceneItem.width;
physicsComponent.height = sceneItemComponent.sceneItem.height;
}
exports.unequipped = function(thisEntity, ownerEntity) {
var sceneItemComponent = thisEntity.componentOfType("SceneItemComponentType");
sceneItemComponent.spriteFileName = ":/sprites/pistol.png";
var physicsComponent = thisEntity.componentOfType("PhysicsComponentType");
physicsComponent.width = sceneItemComponent.sceneItem.width;
physicsComponent.height = sceneItemComponent.sceneItem.height;
}
exports.destroy = function(thisEntity, gameController) {
}
})(this.Pistol = {});
Car
скрипт может иметь функции с одинаковыми именами (activate
, destroy
и т. д.), не затрагивая Pistol
,
По состоянию на Qt 5.12 QJSEngine
имеет поддержку для правильных модулей JavaScript:
Для большей функциональности вы можете инкапсулировать свой код и данные в модули. Модуль - это файл, который содержит код сценария, переменные и т. Д. И использует операторы экспорта для описания своего интерфейса с остальной частью приложения. С помощью операторов импорта модуль может ссылаться на функциональность других модулей. Это позволяет безопасно создавать сценариев приложения из небольших связанных блоков. В отличие от этого, подход с использованием метода rate() несет в себе риск того, что внутренние переменные или функции из одного вызова метода define () могут случайно загрязнить глобальный объект и повлиять на последующие оценки.
Все, что нужно сделать, это переименовать файл, чтобы иметь .mjs
расширение, а затем преобразовать код следующим образом:
export function activate(thisEntity, withEntities, activatorEntity, gameController, activationTrigger, activationContext) {
gameController.systemAt("WeaponComponentType").addMuzzleFlashTo(thisEntity, "muzzle-flash");
}
export function equipped(thisEntity, ownerEntity) {
var sceneItemComponent = thisEntity.componentOfType("SceneItemComponentType");
sceneItemComponent.spriteFileName = ":/sprites/pistol-equipped.png";
var physicsComponent = thisEntity.componentOfType("PhysicsComponentType");
physicsComponent.width = sceneItemComponent.sceneItem.width;
physicsComponent.height = sceneItemComponent.sceneItem.height;
}
export function unequipped(thisEntity, ownerEntity) {
var sceneItemComponent = thisEntity.componentOfType("SceneItemComponentType");
sceneItemComponent.spriteFileName = ":/sprites/pistol.png";
var physicsComponent = thisEntity.componentOfType("PhysicsComponentType");
physicsComponent.width = sceneItemComponent.sceneItem.width;
physicsComponent.height = sceneItemComponent.sceneItem.height;
}
export function destroy(thisEntity, gameController) {
}
C++ для вызова одной из этих функций выглядит примерно так:
QJSvalue module = engine.importModule("pistol.mjs");
QJSValue function = module.property("activate");
QJSValue result = function.call(args);