QJSValue vs QJSManagedValue/QJSPrimitiveValue
May 21, 2021 by Fabian Kosmale | Comments
When Qt 6.1 got released you might have read about QJSManagedValue
and how it “give[s] more fine grained control over JavaScript execution”. But what does that actually mean? To understand this, let’s first recap what QJSValue
is all about, and then compare it with the new classes.
QJSValue as a container type
If we read QJSValue
’s class documentation, we are told that it “acts as a container for Qt/JavaScript data types”. It can store the types supported in ECMAScript (including function, array and arbitrary object types), as well as anything supported by QVariant. As a container, it can be used to pass values to and receive values from a QJSEngine
. If we for instance wanted to expose an external cache to the engine, we could write the lookup function in the following way:
class Cache : public QObject
{
Q_OBJECT
QML_ELEMENT
public:
Q_INVOKABLE QJSValue lookup(const QString &key) {
if (auto it = m_cache.constFind(key); it != m_cache.constEnd()) {
return *it; // impicit conversion
} else {
return QJSValue::UndefinedValue; // implicit conversion
}
}
QHash<QString, QString> m_cache;
}
We use undefined
as the return value in case of a cache miss, and return the cached value otherwise. Note that implicit conversions (from QString
and QJSValue::SpecialValue
respectively) occur when we return the values. We will come back later to this aspect.
QJSValue as a JS API
Besides its use case as a pure container of values, QJSValue also has an API to interact with the contained value: If it stores a function, we can call it with QJSValue::call
, if it contains an object, we can access and modify its property and we can compare two QJSValue
s with equals
or strictlyEquals
:
QJSEngine engine;
QJSValue object = engine.newObject();
object.setProperty("num", 42);
QJSValue function = engine.evaluate("(o) => o.num *= 2 ");
QJSValueList args = { object };
QJSValue result = function.call(args);
QJSValue expected = "84";
Q_ASSERT(result.equals(expected) && !result.strictlyEquals(expected));
The issue with QJSValue
Both use cases above certainly do need to be supported. Unfortunately, it turns out that they conflict. To understand why, we need to be aware of how the QML engine stores values. We don’t delve too deeply into this topic, but it is important to understand that values can be either be managed or primitive. In QML’s JS engine, a managed value can be thought of as a pointer to some data-structure on the heap, whose memory is managed by the engine’s garbage collector. On the other hand, the actual content of primitive values, is stored directly, using a technique called NaN-boxing1.
Primitive Values | Managed Values |
---|---|
int | Function |
double | Array |
undefined | QVariant |
null | string object |
QString |
For the purpose of this discussion, it is vital to recognize that we can obtain a pointer to the engine from a managed value, but not from a primitive one.
When using QJSValue for its JavaScript API, we sooner or later need access to the engine to actually evaluate JavaScript. An obvious example for this was the QJSValue result = function.call(args);
line: to run the function, we have to interpret it in the engine. This works, as the function is a managed value, and we can obtain the engine from it. A less obvious example where we need the engine is when we call a function or access a property on a primitive number or string. Whenever we call a method on a primitive, an instance of its corresponding non-primitive objects is created. This is commonly referred to as boxing. For instance, when we write (42).constructor
, that is basically equivalent to (new Number(42)).constructor
, and it returns the constructor method of the global Number
object. Consequently, if we write QJSValue(42).property("constructor")
, we would expect to obtain a QJSValue containing that function. However, what we get is instead a QJSValue containing undefined
! The attentive reader has probably already spotted the issue: the QJSValue
which we constructed contains only a primitive value, and thus we have no way to access the engine when we need it! We also can’t simply hardcode the property lookup for primitive values in QJSEngine
, as in one engine we might set Number.prototype.constructor.additionalProperty = "the Spanish Inquisition"
whereas in another one we might set Number.prototype.constructor.additionalProperty = 42
. The end result would then clearly be unexpected.
To ensure that property accesses always work, we would need to always store boxed values in QJSValue or store an additional pointer to the engine. That is not possible though: - The first option would require always passing an engine to QJSValue, which would be a bit silly when we use QJSValue simply to pass something to the engine as in the cache example. It would also be API incompatible with how QJSValue is currently used (for instance, we couldn’t support implicit conversion any longer). It would also lead to pointless JS heap allocations when passing around primitives. - The second option would again cause API issues as it still requires adding an engine parameter to all constructors, and it would increase the size needed to store a QJSValue.
The solution: QJSManagedValue
What we did instead was to introduce a new class, QJSManagedValue
, which is intended solely for the JS API use case, and relegate QJSValue
to being a storage class only. To that end, we will also deprecate the methods of QJSValue
which require an engine in an upcoming release of Qt 6. The API of QJSManagedValue
should be familiar to anyone coming from QJSValue
, with a few notable differences:
- The constructors (except for the default and move constructor2) require passing a QJSEngine pointer.
- We’ve added some methods that were missing in
QJSValue
, likedeleteProperty
andisSymbol
. - If
QJSValue
methods encounter an exception, they catch and discard it. In contrast,QJSManagedValue
leaves the exception intact.
Point 1 is what allows us to side-step the whole issue described in the previous paragraph. Note that obtaining the engine is normally not an issue in code: either you are in a scripting context where you’ve already got access to an engine (to create new objects with QJSEngine::newObject
and to evaluate expressions with QJSEngine::evaluate
), or you want to to evaluate some JavaScript in a QObject that has been registered with the engine. The latter can use qjsEngine(this)
to obtain the currently active QJSEngine
.
The effect of point 2 should hopefully be self-explanatory; any further methods that require interaction with the JS engine will also end up only in QJSManagedValue
.
Point 3 can be best demonstrated with a small example
QJSEngine engine;
// we create an object with a read-only property whose getter throws an exception
auto val = engine.evaluate("let o = { get f() {throw 42;} }; o");
val.property("f");
qDebug() << engine.hasError(); // prints false
// This time, we construct a QJSManagedValue before accessing the property
val = engine.evaluate("let o = { get f() {throw 42;} }; o");
QJSManagedValue managed(std::move(val), &engine);
managed.property("f");
qDebug() << engine.hasError(); // prints true
QJSValue error = engine.catchError();
Q_ASSERT(error.toInt(), 42);
In the above example, we used QJSEngine::catchError
, which was also newly introduced in Qt 6.1, to handle the exception. Inside a method of a registered object, we might want to instead let the exception bubble up the call stack.
Note that QJSManagedValue
should ideally be temporarily created on the stack, and discarded once you don’t need to work any longer on the contained value. The reason for this is that QJSValue
can store primitive values in a more efficient way. QJSManagedValue
should also not be used as an interface type (the return or parameter type of functions, and the type of properties), as the engine does not treat it in a special way, and will not convert values to it (in contrast to QJSValue).
What about QJSPrimitiveValue
QJSPrimitiveValue
is one more class we’ve introduced together with QJSManagedValue
. We actually do not expect many people to need it. It can store any of the primitive types, and supports arithmetic operations and comparisons according to the ECMA-262 standard. It allows for low-overhead operations on primitives (in contrast to QJSManagedValue, which always goes through the engine), while still yielding results that are indistinguishable from what the engine would return. If you however know the type of your values, and can live with the differences between JavaScript and C++, doing the operations on plain C++ types should still be faster. As QJSPrimitiveValue is comparatively large, it is also not recommended to store values.
Summary + Outlook
While the QML engine is mostly used in conjunction with QtQuick to implement user interfaces, it is still a full blown JavaScript engine that can be used to script your application. For that use-case, we expect that QJSManagedValue will help you to avoid gotchas pertaining to primitive values, and to ease your work thanks to the additional API. While we don’t have further improvements planned for the scripting use case in 6.2, we’re always interested in your suggestions. Also, thanks to a community contribution by Alex Shaw, you can look forward to registering JS modules in Qt 6.2.
-
Actually, nan-boxing is more like a set of various techniques which all make use of the fact that there are multiple ways to represent a NaN-value, even though only two are actually needed (one for signalling and one for quiet NaN). To learn more about NaN-boxing, you might want to read one of these blog posts.↩︎
-
The default constructor creates a
QJSManagedValue
representing undefined. As undefined has no methods or properties, and trying to access any normally still results in a TypeError exception, which we cannot throw due to the lack of an engine. As there is however no valid use case for accessing a property of undefined, we decided that the convenience of having a default constructor was worth this semantic divergence.↩︎
Blog Topics:
Comments
Subscribe to our newsletter
Subscribe Newsletter
Try Qt 6.8 Now!
Download the latest release here: www.qt.io/download.
Qt 6.8 release focuses on technology trends like spatial computing & XR, complex data visualization in 2D & 3D, and ARM-based development for desktop.
We're Hiring
Check out all our open positions here and follow us on Instagram to see what it's like to be #QtPeople.