[Development] Redesigning QML value types
Ulf Hermann
ulf.hermann at qt.io
Tue Sep 20 18:13:03 CEST 2022
Hi,
I'm currently trying to transform QML value types, such as font, rect,
size, etc, into something less random and more predictable. On that
occasion I'm wondering what semantics people actually expect from value
types.
One thing you have to know in advance is that QML has this internal
concept of "value type references". A value type reference is object of
a value type combined with a sort of pointer to the property it was
retrieved from. Whenever the value type object is modified, it is
subsequently written back to its original property. This mechanism is
mildly inefficient, as you may imagine. Yet, we use it all over the place.
Now, consider the following snippet of QML:
import QtQuick
QtObject {
property font f
function doEvil(ff: font) { ff.pointSize = 22 }
Component.onCompleted: doEvil(f)
}
Assume the default pointSize of a font object is 0. What pointSize
should the member 'f' of the created object receive here?
I see two ways of looking at this:
1. Value types are passed by value
----------------------------------
The doEvil() function then receives a copy of the font and the original
font is never modified. Its pointSize stays 0. If this is the way it
should behave, you get to tell me what the following should do, though:
Component.onCompleted: {
var ff = f;
ff.pointSize = 22;
}
In JavaScript, this is indistinguishable from simply "f.pointSize = 22".
So, strictly speaking, the latter would have to modify a copy of 'f',
not 'f' itself. That, however, would make working with value types
somewhat more complicated. You'd have to explicitly write them back
after modification. On the plus side, you could modify several
properties in a row and only write the value back once.
Since value types have been writing themselves back for about 10 years
now, we'd need some compatibility mechanism to phase such a behavior in,
over several versions of Qt. And I'm sure someone would hate it.
However, since we've never implemented value type references for lists,
lists of value types behave exactly this way. The following does in fact
not modify the list, but only a copy of the object at index 10:
property list<font> fonts: [ ... ]
Component.onCompleted: fonts[10].pointSize = 33
The same holds for nested value types. Assume a value type "outer" that
has a property "a" of another value type, which has an integer property
"b". The following does not write back:
property outer o
Component.onCompleted: o.a.b = 10
2. Everything is a reference
----------------------------
JavaScript doesn't really have value types. The doEvil() function thus
receives a reference to the original 'f', not a copy. The modification
of pointSize is written back to the original property.
With font you might still wrap your head around it because font "looks
like" an object with all its properties and internal logic. However, you
can also pass a point or a rect around through various functions, and
whenever you modify it, the result would be written back to the original
place where it was retrieved from, possibly in a different file half an
hour ago. I personally find that rather confusing.
Another drawback of this is that it would be rather hard to compile a
function that deals with value types to C++. In C++, value types don't
have a back reference to the property they were loaded from. A QML font
is just a QFont and nothing else. This assumption allows qmlcachegen and
qmlsc to generate efficient code. Yet, as you may notice by toggling
QV4_FORCE_INTERPRETER on the QML program above, they already mess this
up today (and no one has noticed).
So, the consequence of going with "Everything is a reference" would be
that we couldn't compile any function to C++ that passes a value type to
a JavaScript function or uses a value type returned from a JavaScript
function.
A middle ground would be that value types are passed by value when
calling or returning from a function, but are treated as references and
written back on modification inside a function. Inside the same function
qmlsc and qmlcachegen could keep track of where a value was loaded from
and write it back when it's modified. I've played with the concept a
bit, and it's possible to support nested value types and lists of value
types this way. All the writing back would still be inefficient, though.
In addition to whatever course is chosen, we might add a detach() method
on the JavaScript prototype used for all value types. With that you
could intentionally detach a value from the property it was loaded from.
regards,
Ulf
More information about the Development
mailing list