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:
Alex Shaw 2021-04-25 17:57:37 -04:00
parent 689522817d
commit 3464655f5e
10 changed files with 254 additions and 10 deletions

View File

@ -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.

View File

@ -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);

View File

@ -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();

View File

@ -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;

View File

@ -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;

View File

@ -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"

View File

@ -0,0 +1,10 @@
import magic from 'magic';
import { name } from 'qt_info';
export function getMagic() {
return magic;
}
export function getName() {
return name;
}

View File

@ -0,0 +1,5 @@
import math from 'math';
export function addAndDouble(a, b) {
return math.add(a, b) * 2;
}

View File

@ -0,0 +1 @@
import { subval } from 'notanobject';

View File

@ -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;