Integrating with JavaScript values from C++

The following classes can be used to load and access JavaSript from C++ code:

Use QJSValue to transfer values to and from the engine, and use QJSManagedValue to interact with JavaScript values. Only use QJSPrimitiveValues if you have to emulate the semantics of JS primitive values in C++.

QJSValueQJSManagedValueQJSPrimitiveValue
Persistently store valuesShort livedShort lived
Transport values to/from engineAccess propertiesOnly Primitives
Call methodsBasic arithmetic and comparison

QJSValue as a Container Type

QJSValue stores the Qt/JavaScript data 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.

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

In case of a cache miss, undefined is returned. Otherwise, the cached value is returned. Note that implicit conversions (from QString and QJSValue::SpecialValue respectively) occur when the value is returned.

QJSValue also has an API to interact with the contained value, but using QJSManagedValue is recommended.

Primitive and Managed Values

QJSValue and QJSManagedValue store values that can be either 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. The actual content of primitive values is stored directly, using a technique called NaN-boxing that enables you to represent a NaN-value in multiple ways, even though only two are actually needed; one for signalling and one for quiet NaN-value.

Primitive ValuesManaged Values
intFunction
doubleArray
undefinedQVariant
nullstring object
QString

A pointer to the engine can be obtained from a managed value, but not from a primitive one. When using QJSValue for its JavaScript API, you need access to the engine to evaluate JavaScript. For example, to run the call(args) function, you have to interpret it in the engine. This works, as the function is a managed value, and you can obtain the engine from it.

Similarly, where the engine is needed when you call a function or access a property on a primitive number or string. Whenever you call a method on a primitive, an instance of its corresponding non-primitive objects is created. This is referred as boxing. When you write (42).constructor, that is equivalent to (new Number(42)).constructor, and it returns the constructor method of the global number object. Accordingly, if you write QJSValue(42).property("constructor"), you would expect to obtain a QJSValue containing that function. However, what you get is instead a QJSValue containing undefined.

The QJSValue that you constructed contains only a primitive value, and thus you have no way to access the engine. You also can’t simply hardcode the property lookup for primitive values in QJSEngine, as in one engine you might set Number.prototype.constructor.additionalProperty = "the Spanish Inquisition" whereas in another Number.prototype.constructor.additionalProperty = 42. The end result would then clearly be unexpected.

To ensure that property accesses always work, you would need to always store boxed values in QJSValue or store an additional pointer to the engine.

However, this would be incompatible with how QJSValue is currently used, lead to pointless JS heap allocations when passing around primitives, and increase the size needed to store a QJSValue. Therefore, you should use QJSValue only for storage and QJSManagedValue to obtain the engine.

QJSManagedValue

QJSManagedValue is similar to QJSValue, with a few differences:

To obtain the engine 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 evaluate some JavaScript in a QObject that has been registered with the engine. In the latter case, you can use qjsEngine(this) to obtain the currently active QJSEngine.

QJSManagedValue also provides a few methods that have no equivalent in QJSEngine.

In the example below, QJSManagedValue methods encounter an exception, and QJSEngine::catchError is used to handle the exception.

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

However, inside a method of a registered object, you might want to instead let the exception bubble up the call stack.

QJSManagedValue should be temporarily created on the stack, and discarded once you don’t need to work any longer on the contained value. Since QJSValue can store primitive values in a more efficient way, QJSManagedValue should also not be used as an interface type which is 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).

QJSPrimitiveValue

QJSPrimitiveValue 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. As QJSPrimitiveValue is comparatively large, it is not recommended to store values.