wiki:QtScript

Version 3 (modified by kent.hansen@nokia.com, 9 years ago) (diff)

--

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.