Subnavigation

QJSValue vs QJSManagedValue/QJSPrimitiveValue

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 QJSValues 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:

  1. The constructors (except for the default and move constructor2) require passing a QJSEngine pointer.
  2. We’ve added some methods that were missing in QJSValue, like deleteProperty and isSymbol.
  3. 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

List of main use cases for the three QJSValue classes

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.


  1. 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.↩︎

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