Details
-
Bug
-
Resolution: Fixed
-
P1: Critical
-
6.5.0
-
None
-
9eb6240eb (dev), b4cf8c1f1 (dev)
Description
When calling a function defined in JavaScript but with type annotations, qmlcachegen will adhere to the type annotation and coerce the arguments and return type accordingly. This is wrong because the interpreter does not do such a thing. So, you may have a function like this:
function lookup(a: int) : string { return strings[a]; }
If you pass 3.5 to this function, when called from JavaScript and interpreted it will return undefined (which isn't expressible as string). When called from C++ or run as native code, it will return whatever it finds at index 3 in strings. So there are two aspects to this problem:
- The QML engine will coerce when calling a function with a C++ (metatypes) interface, but it won't when calling a function with a JavaScript interface. A type-annotated function may still be called through its JavaScript interface.
- The compiler will generate coercion code for calling any function it can determine the types for, even if the function is then interpreted.
The problem is somewhat softened by the fact that all shadowable functions are called with QVariant arguments and return type from generated code, but that's not enough to solve it.
We have a pragma FunctionSignatureBehavior that can force the compiler to disregard type annotations or the engine to enforce them. Having the engine enforce the type annotations means it has to potentially convert all arguments twice - once to the C++ type to enforce the annotations, and then back to the JS type in order to call the JS interface. Also, setting a pragma to gain consistent behavior across execution modes is not ideal.
Can we do better here?
One idea is that a call should not perform a narrowing coercion, but is allowed to do a widening coercion. So, in the example above, the engine would reject a coercion from 3.5 to int and rather call the JavaScript interface. It should also output a warning in this case. Conversely, the compiler would reject a coercion from a floating point type to an integer type and refuse to compile the call. The reverse would not be a problem. Passing an int to a function that takes a double seems to be fine in all cases. This also means that the call frame setup should not do any construction of complex value types and it should check object types to see whether the coercion is widening or narrowing.
Is this analysis complete or am I missing something?
-> What I'm missing is the return type conversion. A JavaScript call may return absolutely anything. The example above may be rephrased as:
function lookup(a: int) : string { return strings[a] || 25; }
This would return a number, violating the signature on the return type side. If generated code assumes it returns a string, it can, for example do "lookup(3.5).length" and that would be 2, rather than undefined.
So, in generated C++ code we cannot assume anything about the return type of a JavaScript function even if it is fully annotated. This is certainly unsatisfactory. At the point when we would detect the discrepancy we cannot fall back to JavaScript execution as the caller is already running and expecting a specific return value.
We can also do something about this, though:
- When generating C++ code for functions (not bindings), refuse to do narrowing conversions for return values.
- Record the call graph for each function at compile time.
- At run time, if a function would call any function via its JavaScript interface (because there is no native code for it), refuse to call its metatypes interface.
If we can get this right, we can drop the metatypes-to-JS conversions in the engine, but analyzing the call graph at run time is annoying. There may be cycles in the call graph. We can do part of the call graph analysis at compile time. For functions from the same document we can at least determine whether they have native code themselves. If one of them has none, then we can drop the native code for all of its callers. Functions of which we know they are implemented in C++ can be omitted from the call graph. Leaf functions with native code are known to be safe. This way we can annotate functions with either "safe" or "run-time check these remaining functions", or we can drop the native code. It does not eliminate the cycles, but it should reduce the performance impact.
Attachments
Issue Links
- is required for
-
QTBUG-113490 Document QML_CONSTRUCTIBLE_VALUE and QML_STRUCTURED_VALUE
- Closed
- relates to
-
QTBUG-118076 pragma FunctionSignatureBehavior: Enforced is broken for object lists
- Closed
For Gerrit Dashboard: QTBUG-113527 | ||||||
---|---|---|---|---|---|---|
# | Subject | Branch | Project | Status | CR | V |
477308,29 | QML: Improve the JS-to-JS type check when enforcing signatures | dev | qt/qtdeclarative | Status: MERGED | +2 | 0 |
486912,11 | QML: Revert the default for enforcing function signatures | dev | qt/qtdeclarative | Status: MERGED | +2 | 0 |