diff --git a/src/qml/jsapi/qjsengine.cpp b/src/qml/jsapi/qjsengine.cpp index b27c6add75..7e8effda45 100644 --- a/src/qml/jsapi/qjsengine.cpp +++ b/src/qml/jsapi/qjsengine.cpp @@ -150,6 +150,46 @@ Q_DECLARE_METATYPE(QList) } \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. diff --git a/src/qml/jsapi/qjsengine.h b/src/qml/jsapi/qjsengine.h index ac79b30ebc..84730afe2b 100644 --- a/src/qml/jsapi/qjsengine.h +++ b/src/qml/jsapi/qjsengine.h @@ -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); diff --git a/src/qml/jsruntime/qv4engine.cpp b/src/qml/jsruntime/qv4engine.cpp index 69db5efd05..0c3f16651a 100644 --- a/src/qml/jsruntime/qv4engine.cpp +++ b/src/qml/jsruntime/qv4engine.cpp @@ -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 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(); diff --git a/src/qml/jsruntime/qv4engine_p.h b/src/qml/jsruntime/qv4engine_p.h index e3e0a481cf..fe31b42c84 100644 --- a/src/qml/jsruntime/qv4engine_p.h +++ b/src/qml/jsruntime/qv4engine_p.h @@ -760,9 +760,17 @@ public: mutable QMutex moduleMutex; QHash> 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 nativeModules; + void injectModule(const QQmlRefPointer &moduleUnit); QQmlRefPointer moduleForUrl(const QUrl &_url, const ExecutableCompilationUnit *referrer = nullptr) const; QQmlRefPointer loadModule(const QUrl &_url, const ExecutableCompilationUnit *referrer = nullptr); + void registerModule(const QString &name, const QJSValue &module); bool diskCacheEnabled() const; diff --git a/src/qml/jsruntime/qv4executablecompilationunit.cpp b/src/qml/jsruntime/qv4executablecompilationunit.cpp index a4e7ce4466..f61bf36b0e 100644 --- a/src/qml/jsruntime/qv4executablecompilationunit.cpp +++ b/src/qml/jsruntime/qv4executablecompilationunit.cpp @@ -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; diff --git a/tests/auto/qml/qjsengine/CMakeLists.txt b/tests/auto/qml/qjsengine/CMakeLists.txt index 83e13826bf..3488a73a25 100644 --- a/tests/auto/qml/qjsengine/CMakeLists.txt +++ b/tests/auto/qml/qjsengine/CMakeLists.txt @@ -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" diff --git a/tests/auto/qml/qjsengine/testregister.mjs b/tests/auto/qml/qjsengine/testregister.mjs new file mode 100644 index 0000000000..bed822cc84 --- /dev/null +++ b/tests/auto/qml/qjsengine/testregister.mjs @@ -0,0 +1,10 @@ +import magic from 'magic'; +import { name } from 'qt_info'; + +export function getMagic() { + return magic; +} + +export function getName() { + return name; +} diff --git a/tests/auto/qml/qjsengine/testregister2.mjs b/tests/auto/qml/qjsengine/testregister2.mjs new file mode 100644 index 0000000000..9119c167e7 --- /dev/null +++ b/tests/auto/qml/qjsengine/testregister2.mjs @@ -0,0 +1,5 @@ +import math from 'math'; + +export function addAndDouble(a, b) { + return math.add(a, b) * 2; +} diff --git a/tests/auto/qml/qjsengine/testregister3.mjs b/tests/auto/qml/qjsengine/testregister3.mjs new file mode 100644 index 0000000000..ddd0515bbd --- /dev/null +++ b/tests/auto/qml/qjsengine/testregister3.mjs @@ -0,0 +1 @@ +import { subval } from 'notanobject'; diff --git a/tests/auto/qml/qjsengine/tst_qjsengine.cpp b/tests/auto/qml/qjsengine/tst_qjsengine.cpp index c8ceaf7680..81671176cc 100644 --- a/tests/auto/qml/qjsengine/tst_qjsengine.cpp +++ b/tests/auto/qml/qjsengine/tst_qjsengine.cpp @@ -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;