wiki:QtScript

QtScript

QtScript provides a Qt-ish (C++) API on top of the JavaScriptCore C API. It is based on the existing QtScript module provided by Qt: http://doc.trolltech.com/qtscript.html

The work lives under JavaScriptCore/qt in webkit trunk.

https://bugs.webkit.org/show_bug.cgi?id=31863 is the umbrella task for this project.

QObject Binding

The purpose of the QObject binding, or bridge as it's called in QtWebKit, is to provide a JS wrapper object (AKA delegate) that enables access to a QObject from JavaScript.

The binding is completely dynamic, relying on QMetaObject introspection to figure out what properties and methods the C++ object has. Any QObject-derived class instance can be exported to JavaScript this way, without the C++ class author having to do anything special; just declare his properties (Q_PROPERTY) and slots in the class declaration.

The binding can roughly be divided into three "parts":

  • Handling property access.
  • Calling (meta-)methods.
  • Connections (C++ signal --> JS "slot").

The binding depends on QtScript's meta-type-based value conversion.

Binding Behavior

This section describes the current behavior of the QtScript binding in Qt. Not all of this is documented in the official docs (http://doc.trolltech.com/4.6/scripting.html#making-a-qobject-available-to-the-script-engine).

A QObject wrapper handles the following:

  • Reading:
    • Static (QMetaObject) properties. Returns an accessor (get/set functions) for the property, not the actual value.
    • Dynamic (QObject::dynamicPropertyNames()) properties. Returns the property's value, converted from QVariant.
    • Meta-methods (signals, slots) by name. Returns a method wrapper object. Private slots are not exposed (but protected are).
    • Meta-methods (signals, slots) by signature. Ditto.
    • Named child objects. Returns a wrapper object for the child.
  • Writing:
    • Static (QMetaObject) properties.
    • Dynamic properties.
    • Automatically creating a dynamic property when the property doesn't already exist.

The binding is completely dynamic. For example, if a dynamic property is added to the C++ object after the QtScript wrapper has been created, the wrapper can be used to access the new property. Conversely, if the dynamic property is removed after the QtScript wrapper has been created, the wrapper will no longer report the property. Similarly, if a child object is added, renamed or removed after the wrapper has been created, wrapper property access will reflect this.

Customizing Binding Behavior

The QScriptEngine::QObjectWrapOption flags can be used to configure the binding, per QObject wrapper. None of these options are on by default.

  • ExcludeChildObjects: Don't expose child objects.
  • ExcludeSuperClass{Properties,Methods,Contents}: Don't include members from the superclass (QMetaObject::superClass()).
  • QScriptEngine::ExcludeDeleteLater: This is a very special case of excluding superclass content: Excludes the QObject::deleteLater() slot, so that a script can't delete the object.
  • QScriptEngine::AutoCreateDynamicProperties: Create a new dynamic property on the C++ object, rather than on the JS wrapper object, when the property written doesn't exist.
  • QScriptEngine::PreferExistingWrapperObject: Return an existing wrapper object if one has previously been created with the same configuration.
  • QScriptEngine::SkipMethodsInEnumeration: Don't include methods (signals and slots) when enumerating the object's properties.
  • QScriptEngine::ExcludeSlots: Don't expose slots.

Sensible wrap options that have been requested, but not implemented (yet):

  • Reads of non-existent properties should throw a ReferenceError, rather than returning undefined (like normal JS objects do).
  • Writes to non-existent properties should throw a ReferenceError, rather than silently creating a new property (like normal JS objects do).
  • Disable implicit type conversion; e.g. attempts to write a string to a property of type int, or pass an int to a method that expects a string, should throw a TypeError.

Property Attributes

  • Static (QMetaObject) properties: DontDelete, ReadOnly if QMetaProperty::isWritable() returns false.
  • Dynamic (QObject::dynamicPropertyNames()) properties: No attributes. (Dynamic properties can be changed and deleted!)
  • Meta-methods (by name or signature): No attributes. Slots can be replaced by any value. Slots appear to be deletable (the delete operator will return true), but will resurface on subsequent reads.
  • Named child objects: DontDelete, ReadOnly, DontEnum. There's no way to enumerate child objects. The delete operator returns true due to a bug since 4.6; the child is not actually deleted.

C++ Object Ownership

The QScriptEngine::ValueOwnership enum is used to specify what happens with the underlying C++ object when the JS wrapper object is garbage collected.

  • QScriptEngine::QtOwnership: The object is owned by C++, i.e. will not be deleted. This is the default.
  • QScriptEngine::ScriptOwnership: The object will be deleted.
  • QScriptEngine::AutoOwnership: The object will only be deleted if it doesn't have a parent.

Calling Meta-Methods

The binding enables you to call QObject methods (typically slots, but also methods with the Q_INVOKABLE modifier) from JavaScript, like any other JS function. A C++ method wrapper object does the job of invoking the corresponding method when the function is called from JS.

JS value arguments are converted to the types expected by the C++ method. If too few arguments are provided, a TypeError is thrown. If too many arguments are provided, the extra arguments are ignored (but may still be accessed by the slot if the class inherits QScriptable -- more on that later). The C++ return value is converted to a JS value and passed back as the result from the method wrapper.

Like other C++ methods, meta-methods can be overloaded, and they can have default arguments. The QtScript binding tries to ensure that the intended overload is called. First, if there is an overload that expects precisely as many arguments as were passed, that overload is selected. Second, if there's more than one such overload, the decision is based on a heuristic of how well the source (JS) argument types match with the target (C++) argument types. If the heuristic doesn't help either, a TypeError is thrown.

Signal & Slot Connections

The binding provides a JavaScript "equivalent" of the C++ QObject::connect() function.

Every signal wrapper has functions connect() and disconnect(). Their usage is documented at http://doc.trolltech.com/scripting.html#using-signals-and-slots. Internally, connect() and disconnect() delegate to a "connection manager" that acts as the middle-man, ensuring that when the C++ signal is emitted, the associated JS function ("signal handler") is called.

A signal is bound to the wrapper object it was retrieved from; this is necessary because when connect() is invoked, the JS this-object will be the signal, not the wrapper object. So while "button.clicked.connect(...)" is a very convenient syntax, it comes at a price; it's not possible to share the clicked() signal wrapper between instances.

One limitation of the QtScript signal & slot mechanism is that it doesn't provide a way to force a queued connection.

C++ API related to QtScript connections: QScriptEngine::signalHandlerException(), qScriptConnect() and qScriptDisconnect().

Built-in QObject Prototype Object

QtScript provides a QObject prototype object which provides the functions findChild(), findChildren(), and toString() (since these functions are not slots).

Handling QObject Deletion

It's possible that a wrapped QObject is deleted "behind our back", i.e. outside the QtScript environment. To detect this, QtScript stores the QObject* in a QPointer. Trying to perform an operation (e.g. property access) on a wrapper object whose underlying C++ object has been deleted will cause an error to be thrown.

QScriptable

See http://doc.trolltech.com/qscriptable.html. Basically, if a QObject subclasses QScriptable, a field will be set in the QObject that makes it possible for the C++ property accessor or slot to inspect how it was called from JS (the this-object, the original arguments, etc.). This is typically used to implement a QObject-based prototype object.

Transferable Meta-Methods

Meta-method wrappers are transferable; for example, "myQObject.deleteLater.apply(myQWidget)", where myQObject is a QObject and myQWidget is a QWidget, will call myQWidget's deleteLater() slot.

Implementation Concerns & Ideas

The Qt implementation of the QObject binding has some performance/memory issues, which we should try to avoid from the beginning in a new implementation. See http://bugreports.qt.nokia.com/browse/QTBUG-12349.

"Too dynamic": The Qt implementation doesn't do any work at wrapper creation time to figure out which properties the QObject has. All the work is done in "catch-all" access functions for the object. This means the VM has no chance of optimizing property access, e.g. by delegating directly to an accessor function for a static (known at JIT compile time) property.

An alternative would be to create a "JS class" per QMetaObject, and populate it with accessors for named (static) properties and slots. There would still need to be fallbacks (interceptors) to handle access to dynamic properties and child objects. Alternatively, wrap options could be introduced to treat dynamic properties and children as fixed properties too. A wilder idea is to listen for ChildAdded/ChildRemoved/DynamicPropertyChange and change the JS class on the fly.

Creating a JS class with named property accessors has the potential downside that a lot of accessors could be created that would never be used. Maybe a hybrid approach is possible, where named accessors are installed lazily by the catch-all accessor?

Slow lookup: The Qt implementation converts the JS property name (JSC::Identifier) to a Latin-1 string, and uses that string to query the QMetaObject. Both these operations are slow. There should be a direct association between the JS property name (in whatever form the VM stores it) to the Qt property. The JS class approach outlined above seems like the preferred way to achieve that (let the VM do as much of the work as possible, avoid our own hashing/caching).

Slow method resolution: First, the possibility of a meta-method being overloaded means that all the methods need to be searched (including the superclass methods -- it's possible that a subclass overloads one of the superclass's methods!), and with QMetaObject you can only do a linear search.

Second, in order to obtain the meta-types for the return type and arguments, some costly operations are performed: 1) The parameter types are extracted using QMetaMethod::parameterTypes(). 2) The type names are resolved to meta-type ids using QMetaType::type(). At least for built-in types, this information could be computed once and cached ((it's not safe for custom types because they can be unregistered).

Supporting QScriptable: The string-based (slow) QObject::qt_metacast() is used to determine if the object implements the QScriptable interface. This is done per method call / property access. This information should also be cached in the meta-method/property wrapper if possible.

QMetaObject and friends

This section provides some background on the introspection facilities in Qt, which are the basis of the JS binding.

QMetaObject (http://doc.trolltech.com/latest/qmetaobject.html) is the class that enables a QObject to be introspected at runtime. An object's meta-object can be obtained by calling QObject::metaObject().

Most importantly, the meta-object contains information about the ("static") properties, and methods (signals/slots/Q_INVOKABLEs), of the class. Dynamic properties and child objects are handled per instance, i.e. they don't concern the meta-object.

Class members are queried by index: QMetaObject::property(int) and QMetaObject::method(int). Functions QMetaObject::indexOfProperty(const char*) and QMetaObject::indexOfMethod(const char*) can be used to map a property name or (normalized) function signature to an index. They're pretty slow since they just perform a linear search w/ string compare (no hashing).

Properties: QMetaObject::property() returns a QMetaProperty. The QMetaProperty can be used to query various flags of the property (writable, scriptable, ...), the type name and meta-type ID. To read (write) the property of an actual instance, call QMetaProperty::read(QObject*) (QMetaProperty::write(QObject*, QVariant)). read() returns a QVariant and write() expects a QVariant as value, so you will typically have to convert from/to QVariant when dealing with these functions.

Methods: QMetaObject::method() returns a QMetaMethod. The QMetaMethod can be used to query the kind (signal/slot/method), attributes, access (private/protected/public), the parameter names, and parameter types and return type (in _string_ form only!). To invoke a method of an actual instance, one can call QMetaMethod::invoke(). invoke() can't be invoked with a variable-length argument list; you need to explicitly pass all arguments (up to 10). This makes it badly suited for use in bindings. Instead, in the bindings we use an internal function, QMetaObject::metacall(). (Potentially this function could be used for reading and writing properties as well, since it would get rid of some overhead, e.g. creating QVariants.) When using the low-level metacall() function, all that's needed is a pointer to the QObject, index of property/method, and storage for the return value and arguments (passed as an array of void*).

Overloaded methods will have separate entries in the meta-object (with unique signatures). This also includes methods that have default arguments (they will appear as overloads in the meta-object).

In order to allocate storage for the return value and parameters, the corresponding meta-type IDs are needed; then one can create a QVariant containing such a type or call QMetaType::construct(). For properties, the type ID is stored directly in the meta-data for built-in types; but for methods, only the signature is stored, so a (slow) string-to-ID lookup needs to be performed. It would be a nice addition if type IDs for methods were stored in the meta-data as well, as it would allow

Super-class members: A QMetaObject's data only includes members declared by its class, _not_ inherited members. The QMetaObject's superClass() function returns the QMetaObject of the super-class. However, note that the property() and method() functions both take an index that's absolute, starting at the ultimate base class, QObject; for example, property(0) will always return the property descriptor for QObject::objectName, regardless of which class's meta-object you query. The members declared by the class itself start at QMetaObject::propertyOffset() and QMetaObject::methodOffset() (i.e., this offset equals the total number of inherited members). The number of non-inherited members is simply propertyCount() - propertyOffset() and methodCount() - methodOffset().

The Abstract Property Lookup Algorithm

Context: A JS wrapper object has been created from a QObject*. Then a property of the wrapper object is read from JS, and we want to return the proper value. QtValueToJS() is a helper function that converts a QVariant to a JS value (not shown). (Note: The algorithm currently doesn't take wrap options, e.g. that exclude child objects, into account; nor QScriptable.)

Inputs: JS object reference, JS property name.

Output: JS value.

  1. Convert JS object reference to QObject*.
  1. Convert the JS property name to a Qt (Latin-1) string.
  1. Get the meta-object of Result(1) (QObject::metaObject()).
  1. Query Result(3)'s property by name Result(2).
  1. If Result(4) is valid, return QtValueToJS(value of property Result(4) for Result(1)). (Alternatively, return a getter/setter accessor for the property.)
  1. Query Result(3)'s method by name/signature Result(2).
  1. If Result(6) is valid, return a JS method wrapper object for Result(6).
  1. Query Result(1)'s dynamic property by name Result(2).
  1. If Result(8) is valid, return QtValueToJS(value of property Result(8)).
  1. Query Result(1)'s child objects by name Result(2).
  1. If Result(10) is valid, return a JS wrapper object for Result(10).
  1. The property by the given name should not be handled by the QObject binding; return a status indicating so.

The Abstract Method Invocation Algorithm

Context: A JS method wrapper object has been returned to JS (as described in the previous section), and is now being called as a function; this should cause the underlying C++ method to be invoked, and the result, if any, passed back to JS.

Inputs: JS "this"-object reference, JS method wrapper object reference (callee), JS arguments list (, optional "callback data").

Output: JS value, or an error thrown if something went wrong.

  1. Convert the JS "this"-object reference to QObject*.
  1. Figure out which meta-method is being invoked. It would be possible to have a single method wrapper that looks up the method by name each time, but that's going to be slow. A per-method (per class) wrapper can store the index of the meta-method for fast access.
  1. If the number of JS arguments is less than the method's number of parameters, throw a SyntaxError.
  1. Get the parameter type IDs of the method. These can be calculated from QMetaMethod::parameterTypes(), or cached if possible.
  1. Convert the JS arguments to C++ values.
  1. If conversion failed, throw a TypeError.
  1. Get the return type ID and reserve space for the return value.
  1. Create an array of void* suitable for QMetaObject::metacall() (return value + arguments).
  1. QMetaObject::metacall(Result(1), QMetaObject::InvokeMetaMethod, Result(2)'s index, Result(8)).
  1. Return QtValueToJS(Result(9)).

For overloaded methods, things get a bit more complicated. First, we try to find an overload whose number of parameters is equal to the number of JS arguments passed. If that doesn't help, for each overload, we try to convert the JS arguments to the overload's expected C++ types, and use a heuristic to figure out which one's the best match. If a perfect match is found, that one is called immediately. Otherwise, the overload with the best matching argument conversion wins. If there's more than one "winner" (no single method that had the best conversion score), an ambiguity error is thrown.

The above algorithm doesn't consider the case where the QObject inherits QScriptable. In that case, an internal member must be set on the QScriptable before invoking the C++ method, so that the C++ method has access to the original JS environment of the call (see http://doc.trolltech.com/qscriptable.html).

Value Conversion

When interacting with Qt/C++ objects in JS, it will often be necessary to perform value conversion between the two worlds. For reading properties, QMetaProperty::read() returns a QVariant; this value must be converted to a JS value of a suitable type. For writing properties, QMetaProperty::write() takes a QVariant as argument; hence, the value passed from JS must first be converted to a QVariant of a suitable type. For calling slots, we may have to perform conversion both ways; converting arguments from JS to C++, and converting the slot's return value back to JS.

The two "top-level" Qt/C++ <--> JS conversion functions are:

  • qtVariantToJS(QVariant) : JSValue: Converts a QVariant to a JS value.
  • qtVariantFromJS(JSValue) : QVariant: Converts a JSValue to a QVariant.

A QVariant can contain a value of any type known to the Qt meta-type system; this includes both QMetaType's built-in types (bool, int, QString, etc.) and custom (user) types.

A QVariant's meta-type can be obtained from QVariant::userType(). A void pointer to the value can be obtained from QVariant::constData(). For example, if the type is QMetaType::Double, it's safe to cast the data pointer to double*; if the type is QMetaType::QString, it's safe to cast the data pointer to QString*; and so on.

Given a meta-type, storage for a value of that type can be allocated using the QMetaType(int type, const void *data) constructor.

We introduce two helper functions to convert between (userType, data*) pairs and JS values:

  • qtMetaTypeToJS(int, void*) : JSValue: Converts a meta-type to a JS value.
  • qtMetaTypeFromJS(JSValue, int, void*) : bool: Converts a JSValue to a meta-type.

These functions are essentially huge switches on the type argument, that cast the data pointer and get/set the value accordingly.

Some non-primitive types have special handling:

  • QDateTime: Converted from/to JS Date object.
  • QRegExp: Converted from/to JS RegExp object.
  • QStringList: Converted from/to JS Array object.
  • QVariantList: Converted from/to JS Array object.
  • QVariantMap: Converted from/to JS Object object.

In many cases there is no "good" conversion between a C++ type and JS. For example, there's no obvious way to convert a QPixmap to a JS object. For types where no "standard" (read: automagic) conversion makes sense, including user types, QtScript falls back to creating a JS wrapper object that contains the QVariant.

It's possible to override QtScript's conversion of any type by registering a pair of conversion functions for the type using qScriptRegisterMetaType().

Mapping Qt Classes to JavaScript

Qt's introspection capabilities can be used to set up a JS inheritance chain that corresponds to the C++ inheritance.

  • QMetaObject::superClass() provides the meta-object of a class's super class.
  • QMetaObject::propertyOffset() provides the offset of a class's own properties. QMetaObject::propertyCount() - QMetaObject::propertyOffset() gives the number of properties defined by the class itself (i.e. not including inherited properties).
  • QMetaObject::methodOffset() provides the offset of a class's own methods. QMetaObject::methodCount() - QMetaObject::methodOffset() gives the number of methods defined by the class itself (i.e. not including inherited methods).

The idea is to define a JS class (JSClassCreate() and friends in the JSC C API) that defines the class's own properties (corresponding to C++ properties), and that inherits the JS super-class. Each JS class also has a prototype object that contains the class's own methods (corresponding to C++ methods).

Example: QPushButton; see http://doc.trolltech.com/4.6/qpushbutton.html. (This would probably work better as a diagram:)

  • JSClass(QPushButton).properties = autoDefault, default, flat
  • JSPrototype(QPushButton).properties = showMenu
  • JSPrototype(QPushButton).proto = JSPrototype(QAbstractButton)
  • JSClass(QPushButton).superClass = JSClass(QAbstractButton)
  • JSClass(QAbstractButton).properties = autoExclusive, autoRepeat, ...
  • JSPrototype(QAbstractButton).properties = animateClick, click, setChecked, setIconSize, toggle
  • JSPrototype(QAbstractButton).proto = JSPrototype(QWidget)
  • JSClass(QAbstractButton).superClass = JSClass(QWidget)

... and so on, all the way down to QObject.

These class definitions and prototype objects can be created on demand, and cached so that they can be shared by all wrapped objects of the same type.

Last modified 14 years ago Last modified on Aug 18, 2010 6:03:06 AM