*CTRL-O*

Create dynamic function objects from C++ for QML

When working with QML, I always favor static typing to dynamic typing as much as possible. I very rarely use var when declaring properties. Sometimes though, this is inevitable. Especially if you are working on an old code base.

Imagine you have some sort of internal C++ data structure that persists data between different runs of your application (e.g window geometry from the last time it was open.). It is relatively easy to expose these data types to QML if they are primitive, or even complex… A map comes in, it is then hooked up to properties in the QML side and a connection is made so that both sides are notified of any changes. Basically how Qt.labs.settings.Settings work.

Window {
    id: root

    MySettings {
        id: settings

        property int x: root.x
        property int y: root.y
        property int width: root.width
        property int height: root.height
    }
}

In MySettings, each MySettings object is tied to a certain internal data structure. The absence of a property in the QML side is a programming error. And the same happens If the names or types of the properties don’t match. This is pretty useful and can help us reduce a lot of boilerplate code.

There’s one case that’s not covered here though. We may also have callable objects that are persisted or stored in the same data structure as the ones I mention above. In these cases, what we can do is to have a QObject and expose it to QML side so we can dispatch function calls with string (or even better, with enums) identifiers which are then used in C++ side to get callbacks and execute.

Button {
    onClicked: {
        // If the function is not found, the program asserts.
        // But we won't know this until we actually click the button.
        bridge.send("OnClicked")
    }

    CppBridge { id: bridge }
}

The downside of this is that we can only check for the validity of these functions when it is executed. On top of that, we don’t allow any function arguments. It could be done by passing a QVariantList, but it’s just an ugly way of doing it. I would prefer a more natural solution where we can call the function like I’m calling any other function with arguments.

Button {
    onClicked: {
        // Just an ugly syntax...
        bridge.send("OnClicked", [1, 2])
    }

    CppBridge { id: bridge }
}

The ideal solution for this is the same mechanism that we use for exposing the primitive types. But how can we expose the functions like the properties?

Enter QJSEngine

QJSEngine provides an environment for evaluating JavaScript code. For example, It would allow you to implement a JavaScript console in your Qt application. It can also help you create JavaScript objects in the C++ side that you can pass along to the QML side.

What we want to do here, is to create an object that can be called using the regular function call syntax in the QML side.

Window {
    id: root

    Button {
        onClicked: {
            settings.createCopy()
        }
    }

    MySettings {
        id: settings

        property int x: root.x
        property int y: root.y
        property int width: root.width
        property int height: root.height

        property var createCopy
    }
}

We can use QJSEngine::evaluate to create a function object.

QJSValue function = engine.evaluate("(function() { console.log('Hello world.') })")
assert(function.isCallable());

Using the same syntax, we can create a function and pass it to QML side.

MySettings::init(InternalData *data)
{
    // Skipping a lot of the details here. You wouldn't actually refer to a
    // specific function object with name here but iterate over the available
    // ones.
    QJSEngine *engine = qjsEngine(this);
    InternalFunctionObject *obj = data.get("createCopy");
    QJSValue function = engine->evaluate("(function() { this._call('createCopy') })")
    assert(function.isCallable());

    const int propertyIndex = metaObject()->indexOfProperty("createCopy");
    assert(propertyIndex > 0);
    auto property = metaObject()->property(propertyIndex);
    property.write(this, QVariant::fromValue<QJSValue>(function));
}

But with this code, if we call settings.createCopy we’ll get an error like this:

<Unknown File>:1: TypeError: Property '_call' of object [object Object] is not a function

We want this._call call to happen in the current instance. But there’s no instance information here. QJSEngine will blindly create a QJSValue object without knowing anything about MySettings. How do we tell the engine that we want this to be bound to our MySettings instance?

This is where Function.bind() and QJSValue::call come in. We can create a function that returns another function that has this bound to our MySettings instance.

MySettings::init(InternalData *data)
{
    // Skipping a lot of the details here. You wouldn't actually refer to a
    // specific function object with name here but iterate over the available
    // ones.
    QJSEngine *engine = qjsEngine(this);
    InternalFunctionObject *obj = data.get("createCopy");

    QJSValue function =
        engine->evaluate("(function(obj) { return (function() { "
                         "this.call(args) }).bind(obj) })");
    assert(function.isCallable());

    QJSValue boundFunction = function.call(QJSValueList{engine->toScriptValue(this)});
    assert(boundFunction.isCallable());

    const int propertyIndex = metaObject()->indexOfProperty("createCopy");
    assert(propertyIndex > 0);
    auto property = metaObject()->property(propertyIndex);
    property.write(this, QVariant::fromValue<QJSValue>(boundFunction));
}

Now we have something we can use. The first function creates another function and the call to bind() creates yet another function for us to use. And the final product will have this bound to MySettings instance.

Window {
    id: root

    Button {
        onClicked: {
            // This now works! But we are not able to pass any arguments yet.
            settings.createCopy()
        }
    }

    MySettings {
        id: settings

        // ...

        property var createCopy
    }
}

The next problem to solve is passing arguments to this function. Unfortunately, there’s no way for us to statically tell the QML compiler that we want a function with two arguments and also mandate the type of these arguments. This part of the solution will still require the function to be actually called to see If the arguments were passed correctly.

We can use arguments object in JavaScript to create a function that takes as many arguments as we provide them. We will still need to pass a list in the C++ side, but at least our call in QML side will look like an actual function call.

MySettings::init(InternalData *data)
{
    // Skipping a lot of the details here. You wouldn't actually refer to a
    // specific function object with name here but iterate over the available
    // ones.
    QJSEngine *engine = qjsEngine(this);
    InternalFunctionObject *obj = data.get("createCopy");

    QJSValue function =
        engine->evaluate("(function(obj) { return (function() { "
                         "let args = [];"
                         "for (let i = 0; i < arguments.length; i++) {"
                         "    args.push(arguments[i])"
                         "};"
                         "this.call(args) }).bind(obj) })");
    assert(function.isCallable());

    QJSValue boundFunction = function.call(QJSValueList{engine->toScriptValue(this)});
    assert(boundFunction.isCallable());

    const int propertyIndex = metaObject()->indexOfProperty("createCopy");
    assert(propertyIndex > 0);
    auto property = metaObject()->property(propertyIndex);
    property.write(this, QVariant::fromValue<QJSValue>(boundFunction));
}

Now we have a function that takes an unspecified number of arguments. These arguments are converted to QVariantList because there’s no way for us to expose a variadic method to QML from C++ side.

Window {
    id: root

    Button {
        onClicked: {
            // This now works!
            settings.createCopy()

            // Sadly, so does this.
            settings.createCopy(1, 2)
        }
    }

    MySettings {
        id: settings

        // ...

        property var createCopy
    }
}

Conclusion

This solution has three benefits:

1- We get to know If we are missing a function or not when the QML document is first instantiated. And not when we dispatch the function with a string, or do some other magic.

2- We are able to pass arguments to our functions in the C++ side.

3- We enable a uniform function call syntax.

This may not be the greatest solution, this is something that I came up with as I was thinking about it and have not really applied it to the real life product yet. But it was interesting to see how I can use these public APIs.

It would be great to be able to also check for the function arguments at initialization time. Ideally, the declaration would look like this:

MySettings {
    id: settings

    // ...

    property Function<string> createCopy
}

However, this is something that needs support from the QML engine. This is something I’ll be looking into for my future experiments.