Add QJSEngine::registerModule
Some applications that use JavaScript as a scripting language may want to extend JS through C++ code. The current way to do that is with global objects. ES6 provides a better way of encapsulating code: modules. registerModule() allows an application to provide a QJSValue as a named module. Developers familiar with Node.js will find this very easy to use. Example: ```c++ QJSValue num(666); myEngine.registerModule("themarkofthebeast", num); ``` ```js import badnews from "themarkofthebeast"; ``` [ChangeLog][QtQml][QJSEngine] Adds the ability to register QJSValues in C++ as modules for importing in MJS files. Change-Id: I0c98dcb746aa2aa15aa2ab3082129d106413a23b Reviewed-by: Ulf Hermann <ulf.hermann@qt.io>
This commit is contained in:
parent
689522817d
commit
3464655f5e
|
@ -150,6 +150,46 @@ Q_DECLARE_METATYPE(QList<int>)
|
|||
}
|
||||
\endcode
|
||||
|
||||
Modules don't have to be files. They can be values registered with
|
||||
QJSEngine::registerModule():
|
||||
|
||||
\code
|
||||
import version from "version";
|
||||
|
||||
export function getVersion()
|
||||
{
|
||||
return version;
|
||||
}
|
||||
\endcode
|
||||
|
||||
\code
|
||||
QJSValue version(610);
|
||||
myEngine.registerModule("version", version);
|
||||
QJSValue module = myEngine.importModule("./myprint.mjs");
|
||||
QJSValue getVersion = module.property("getVersion");
|
||||
QJSValue result = getVersion.call();
|
||||
\endcode
|
||||
|
||||
Named exports are supported, but because they are treated as members of an
|
||||
object, the default export must be an ECMAScript object. Most of the newXYZ
|
||||
functions in QJSValue will return an object.
|
||||
|
||||
\code
|
||||
QJSValue name("Qt6");
|
||||
QJSValue obj = myEngine.newObject();
|
||||
obj.setProperty("name", name);
|
||||
myEngine.registerModule("info", obj);
|
||||
\endcode
|
||||
|
||||
\code
|
||||
import { name } from "info";
|
||||
|
||||
export function getName()
|
||||
{
|
||||
return name;
|
||||
}
|
||||
\endcode
|
||||
|
||||
\section1 Engine Configuration
|
||||
|
||||
The globalObject() function returns the \b {Global Object}
|
||||
|
@ -555,6 +595,8 @@ QJSValue QJSEngine::evaluate(const QString& program, const QString& fileName, in
|
|||
\note If an exception is thrown during the loading of the module, the return value
|
||||
will be the exception (typically an \c{Error} object; see QJSValue::isError()).
|
||||
|
||||
\sa registerModule()
|
||||
|
||||
\since 5.12
|
||||
*/
|
||||
QJSValue QJSEngine::importModule(const QString &fileName)
|
||||
|
@ -576,6 +618,37 @@ QJSValue QJSEngine::importModule(const QString &fileName)
|
|||
m_v4Engine->newErrorObject(QStringLiteral("Interrupted"))->asReturnedValue());
|
||||
}
|
||||
|
||||
/*!
|
||||
Register a QJSValue to serve as a module. After this function is called,
|
||||
all modules that import \a moduleName will import the value of \a value
|
||||
instead of loading \a moduleName from the filesystem.
|
||||
|
||||
Any valid QJSValue can be registered, but named exports (i.e.
|
||||
\c {import { name } from "info"} are treated as members of an object, so
|
||||
the default export must be created with one of the newXYZ methods of
|
||||
QJSEngine.
|
||||
|
||||
Because this allows modules that do not exist on the filesystem to be imported,
|
||||
scripting applications can use this to provide built-in modules, similar to
|
||||
Node.js
|
||||
|
||||
\note The QJSValue \a value is not called or read until it is used by another module.
|
||||
This means that there is no code to evaluate, so no errors will be seen until
|
||||
another module throws an exception while trying to load this module.
|
||||
|
||||
\warning Attempting to access a named export from a QJSValue that is not an
|
||||
object will trigger a \l{Script Exception}{exception}.
|
||||
|
||||
\sa importModule()
|
||||
*/
|
||||
bool QJSEngine::registerModule(const QString &moduleName, const QJSValue &value)
|
||||
{
|
||||
m_v4Engine->registerModule(moduleName, value);
|
||||
if (m_v4Engine->hasException)
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
/*!
|
||||
Creates a JavaScript object of class Object.
|
||||
|
||||
|
|
|
@ -71,6 +71,7 @@ public:
|
|||
QJSValue evaluate(const QString &program, const QString &fileName = QString(), int lineNumber = 1, QStringList *exceptionStackTrace = nullptr);
|
||||
|
||||
QJSValue importModule(const QString &fileName);
|
||||
bool registerModule(const QString &moduleName, const QJSValue &value);
|
||||
|
||||
QJSValue newObject();
|
||||
QJSValue newSymbol(const QString &name);
|
||||
|
|
|
@ -879,6 +879,10 @@ ExecutionEngine::ExecutionEngine(QJSEngine *jsEngine)
|
|||
ExecutionEngine::~ExecutionEngine()
|
||||
{
|
||||
modules.clear();
|
||||
for (auto val : nativeModules) {
|
||||
PersistentValueStorage::free(val);
|
||||
}
|
||||
nativeModules.clear();
|
||||
qDeleteAll(m_extensionData);
|
||||
delete m_multiplyWrappedQObjects;
|
||||
m_multiplyWrappedQObjects = nullptr;
|
||||
|
@ -2072,6 +2076,19 @@ QQmlRefPointer<ExecutableCompilationUnit> ExecutionEngine::loadModule(const QUrl
|
|||
return newModule;
|
||||
}
|
||||
|
||||
void ExecutionEngine::registerModule(const QString &_name, const QJSValue &module)
|
||||
{
|
||||
const QUrl url(_name);
|
||||
QMutexLocker moduleGuard(&moduleMutex);
|
||||
const auto existingModule = nativeModules.find(url);
|
||||
if (existingModule != nativeModules.end())
|
||||
return;
|
||||
|
||||
QV4::Value* val = this->memoryManager->m_persistentValues->allocate();
|
||||
*val = QJSValuePrivate::asReturnedValue(&module);
|
||||
nativeModules.insert(url, val);
|
||||
}
|
||||
|
||||
bool ExecutionEngine::diskCacheEnabled() const
|
||||
{
|
||||
return (!disableDiskCache() && !debugger()) || forceDiskCache();
|
||||
|
|
|
@ -760,9 +760,17 @@ public:
|
|||
|
||||
mutable QMutex moduleMutex;
|
||||
QHash<QUrl, QQmlRefPointer<ExecutableCompilationUnit>> modules;
|
||||
|
||||
// QV4::PersistentValue would be preferred, but using QHash will create copies,
|
||||
// and QV4::PersistentValue doesn't like creating copies.
|
||||
// Instead, we allocate a raw pointer using the same manual memory management
|
||||
// technique in QV4::PersistentValue.
|
||||
QHash<QUrl, QV4::Value*> nativeModules;
|
||||
|
||||
void injectModule(const QQmlRefPointer<ExecutableCompilationUnit> &moduleUnit);
|
||||
QQmlRefPointer<ExecutableCompilationUnit> moduleForUrl(const QUrl &_url, const ExecutableCompilationUnit *referrer = nullptr) const;
|
||||
QQmlRefPointer<ExecutableCompilationUnit> loadModule(const QUrl &_url, const ExecutableCompilationUnit *referrer = nullptr);
|
||||
void registerModule(const QString &name, const QJSValue &module);
|
||||
|
||||
bool diskCacheEnabled() const;
|
||||
|
||||
|
|
|
@ -581,7 +581,11 @@ Heap::Module *ExecutableCompilationUnit::instantiate(ExecutionEngine *engine)
|
|||
setModule(module->d());
|
||||
|
||||
for (const QString &request: moduleRequests()) {
|
||||
auto dependentModuleUnit = engine->loadModule(QUrl(request), this);
|
||||
const QUrl url(request);
|
||||
if (engine->nativeModules.contains(url))
|
||||
continue;
|
||||
|
||||
auto dependentModuleUnit = engine->loadModule(url, this);
|
||||
if (engine->hasException)
|
||||
return nullptr;
|
||||
dependentModuleUnit->instantiate(engine);
|
||||
|
@ -596,16 +600,62 @@ Heap::Module *ExecutableCompilationUnit::instantiate(ExecutionEngine *engine)
|
|||
}
|
||||
for (uint i = 0; i < importCount; ++i) {
|
||||
const CompiledData::ImportEntry &entry = data->importEntryTable()[i];
|
||||
auto dependentModuleUnit = engine->loadModule(urlAt(entry.moduleRequest), this);
|
||||
importName = runtimeStrings[entry.importName];
|
||||
const Value *valuePtr = dependentModuleUnit->resolveExport(importName);
|
||||
if (!valuePtr) {
|
||||
QString referenceErrorMessage = QStringLiteral("Unable to resolve import reference ");
|
||||
referenceErrorMessage += importName->toQString();
|
||||
engine->throwReferenceError(referenceErrorMessage, fileName(), entry.location.line, entry.location.column);
|
||||
return nullptr;
|
||||
QUrl url = urlAt(entry.moduleRequest);
|
||||
const auto nativeModule = engine->nativeModules.find(url);
|
||||
if (nativeModule != engine->nativeModules.end()) {
|
||||
importName = runtimeStrings[entry.importName];
|
||||
const QString name = importName->toQString();
|
||||
|
||||
QV4::Value *value = nativeModule.value();
|
||||
if (value->isNullOrUndefined()) {
|
||||
QString errorMessage = name;
|
||||
errorMessage += QStringLiteral(" from ");
|
||||
errorMessage += url.toString();
|
||||
errorMessage += QStringLiteral(" is null");
|
||||
engine->throwError(errorMessage);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
if (name == QStringLiteral("default")) {
|
||||
imports[i] = value;
|
||||
} else {
|
||||
url.setFragment(name);
|
||||
auto fragment = engine->nativeModules.find(url);
|
||||
if (fragment != engine->nativeModules.end()) {
|
||||
imports[i] = fragment.value();
|
||||
} else {
|
||||
Scope scope(this->engine);
|
||||
ScopedObject o(scope, value);
|
||||
if (!o) {
|
||||
QString referenceErrorMessage = QStringLiteral("Unable to resolve import reference ");
|
||||
referenceErrorMessage += name;
|
||||
referenceErrorMessage += QStringLiteral(" because ");
|
||||
referenceErrorMessage += url.toString(QUrl::RemoveFragment);
|
||||
referenceErrorMessage += QStringLiteral(" is not an object");
|
||||
engine->throwReferenceError(referenceErrorMessage, fileName(), entry.location.line, entry.location.column);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const ScopedPropertyKey key(scope, scope.engine->identifierTable->asPropertyKey(name));
|
||||
const ScopedValue result(scope, o->get(key));
|
||||
Value *valuePtr = engine->memoryManager->m_persistentValues->allocate();
|
||||
*valuePtr = result->asReturnedValue();
|
||||
imports[i] = valuePtr;
|
||||
engine->nativeModules.insert(url, valuePtr);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
auto dependentModuleUnit = engine->loadModule(url, this);
|
||||
importName = runtimeStrings[entry.importName];
|
||||
const Value *valuePtr = dependentModuleUnit->resolveExport(importName);
|
||||
if (!valuePtr) {
|
||||
QString referenceErrorMessage = QStringLiteral("Unable to resolve import reference ");
|
||||
referenceErrorMessage += importName->toQString();
|
||||
engine->throwReferenceError(referenceErrorMessage, fileName(), entry.location.line, entry.location.column);
|
||||
return nullptr;
|
||||
}
|
||||
imports[i] = valuePtr;
|
||||
}
|
||||
imports[i] = valuePtr;
|
||||
}
|
||||
|
||||
for (uint i = 0; i < data->indirectExportEntryTableSize; ++i) {
|
||||
|
@ -745,6 +795,8 @@ void ExecutableCompilationUnit::evaluate()
|
|||
void ExecutableCompilationUnit::evaluateModuleRequests()
|
||||
{
|
||||
for (const QString &request: moduleRequests()) {
|
||||
if (engine->nativeModules.contains(QUrl(request)))
|
||||
continue;
|
||||
auto dependentModuleUnit = engine->loadModule(QUrl(request), this);
|
||||
if (engine->hasException)
|
||||
return;
|
||||
|
|
|
@ -50,6 +50,9 @@ set(qmake_immediate_resource_files
|
|||
"importerror1.mjs"
|
||||
"modulewithlexicals.mjs"
|
||||
"testmodule.mjs"
|
||||
"testregister.mjs"
|
||||
"testregister2.mjs"
|
||||
"testregister3.mjs"
|
||||
)
|
||||
|
||||
qt_internal_add_resource(tst_qjsengine "qmake_immediate"
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
import magic from 'magic';
|
||||
import { name } from 'qt_info';
|
||||
|
||||
export function getMagic() {
|
||||
return magic;
|
||||
}
|
||||
|
||||
export function getName() {
|
||||
return name;
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
import math from 'math';
|
||||
|
||||
export function addAndDouble(a, b) {
|
||||
return math.add(a, b) * 2;
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
import { subval } from 'notanobject';
|
|
@ -251,6 +251,10 @@ private slots:
|
|||
void importModuleWithLexicallyScopedVars();
|
||||
void importExportErrors();
|
||||
|
||||
void registerModule();
|
||||
void registerModuleQObject();
|
||||
void registerModuleNamedError();
|
||||
|
||||
void equality();
|
||||
void aggressiveGc();
|
||||
void noAccumulatorInTemplateLiteral();
|
||||
|
@ -4923,6 +4927,76 @@ void tst_QJSEngine::importExportErrors()
|
|||
}
|
||||
}
|
||||
|
||||
void tst_QJSEngine::registerModule()
|
||||
{
|
||||
QJSEngine engine;
|
||||
QJSValue magic(63);
|
||||
QJSValue name("Qt6");
|
||||
QJSValue version("6.1.3");
|
||||
QJSValue obj = engine.newObject();
|
||||
bool ret = false;
|
||||
|
||||
obj.setProperty("name", name);
|
||||
obj.setProperty("version", version);
|
||||
|
||||
ret = engine.registerModule("magic", magic);
|
||||
QVERIFY2(ret, "Error registering magic");
|
||||
ret = engine.registerModule("qt_info", obj);
|
||||
QVERIFY2(ret, "Error registering qt_info");
|
||||
QJSValue result = engine.importModule(QStringLiteral(":/testregister.mjs"));
|
||||
QVERIFY(!result.isError());
|
||||
|
||||
QJSValue nameVal = result.property("getName").call();
|
||||
QJSValue magicVal = result.property("getMagic").call();
|
||||
QCOMPARE(nameVal.toString(), QLatin1String("Qt6"));
|
||||
QCOMPARE(magicVal.toInt(), 63);
|
||||
|
||||
// Verify that "name" doesn't change in JS even if the object is changed.
|
||||
QJSValue replacement("Bad");
|
||||
obj.setProperty("name", replacement);
|
||||
QJSValue newNameVal = result.property("getName").call();
|
||||
QCOMPARE(nameVal.toString(), "Qt6");
|
||||
}
|
||||
|
||||
class TestRegisterObject : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
TestRegisterObject() {}
|
||||
|
||||
Q_INVOKABLE int add(int a, int b) {
|
||||
return a + b;
|
||||
}
|
||||
};
|
||||
|
||||
void tst_QJSEngine::registerModuleQObject()
|
||||
{
|
||||
QJSEngine engine;
|
||||
TestRegisterObject obj;
|
||||
QJSValue wrapper = engine.newQObject(&obj);
|
||||
auto args = QJSValueList() << 1 << 2;
|
||||
|
||||
bool ret = engine.registerModule("math", wrapper);
|
||||
QVERIFY(ret);
|
||||
|
||||
QJSValue result = engine.importModule(QStringLiteral(":/testregister2.mjs"));
|
||||
QVERIFY(!result.isError());
|
||||
|
||||
QJSValue value = result.property("addAndDouble").call(args);
|
||||
QCOMPARE(value.toInt(), 6);
|
||||
}
|
||||
|
||||
void tst_QJSEngine::registerModuleNamedError() {
|
||||
QJSEngine engine;
|
||||
QJSValue notanobject(666);
|
||||
|
||||
bool ret = engine.registerModule("notanobject", notanobject);
|
||||
QVERIFY(ret);
|
||||
|
||||
QJSValue result = engine.importModule(QStringLiteral(":/testregister3.mjs"));
|
||||
QCOMPARE(result.toString(), QString("ReferenceError: Unable to resolve import reference subval because notanobject is not an object"));
|
||||
}
|
||||
|
||||
void tst_QJSEngine::equality()
|
||||
{
|
||||
QJSEngine engine;
|
||||
|
|
Loading…
Reference in New Issue