When working with Ultralight, you may want to expose your C++ classes to JavaScript. If your C++ application is based on Qt, you may find yourself in the challenging position of exposing a vast number of Qt classes to JavaScript, which could involve writing tedious manual bindings for each class. However, using Qt's meta-object system (reflection), you can dynamically expose these classes with far less effort.
In this article, we'll walk through how to set up a dynamic proxy using the JSC C API and Qt's meta-object system. This proxy will automatically forward method calls and property accesses in JavaScript to the corresponding Qt object in C++, and also support Qt's signal-slot mechanism.
Instead of manually defining JavaScript bindings for every Qt class and method, we create a generic proxy. This proxy will:
- Intercept method calls and property accesses made on JavaScript objects.
- Dynamically forward those calls and accesses to their corresponding
QObject
methods and properties in C++ using Qt's meta-object system (QMetaObject
). - Use Qt's
QVariant
and JavaScriptCore'sJSValueRef
to handle the type conversions between JavaScript and C++. - Support connecting JavaScript functions to Qt signals.
First, we define the JavaScript class (JSClassDefinition
) that represents our proxy object in JavaScript. This class will be named "Qt"
, and its callAsFunction
and getProperty
callbacks will dynamically forward method calls and property accesses to the underlying QObject
.
#include <JavaScriptCore/JavaScript.h>
#include <QMetaObject>
#include <QMetaMethod>
#include <QObject>
#include <QString>
#include <QVariant>
// Function declarations
JSValueRef QtProxyGetProperty(JSContextRef ctx, JSObjectRef object, JSStringRef propertyName, JSValueRef *exception);
bool QtProxySetProperty(JSContextRef ctx, JSObjectRef object, JSStringRef propertyName, JSValueRef value, JSValueRef *exception);
void QtProxyFinalize(JSObjectRef object);
static JSClassDefinition qtProxyClassDefinition = {
0, // version
kJSClassAttributeNone, // attributes
"Qt", // className
NULL, // parentClass
NULL, // staticValues
NULL, // staticFunctions
NULL, // initialize
QtProxyFinalize, // finalize
NULL, // hasProperty
QtProxyGetProperty, // getProperty
QtProxySetProperty, // setProperty
NULL, // deleteProperty
NULL, // getPropertyNames
NULL, // callAsFunction
NULL, // hasInstance
NULL // convertToType
};
// Create the JSClassRef for later use
JSClassRef QtProxyClass = JSClassCreate(&qtProxyClassDefinition);
To support Qt signals and manage the relationship between Qt objects and their JavaScript representations, we implement a QtProxy
class:
class QtProxy : public QObject {
Q_OBJECT
public:
QtProxy(QObject* wrappedObject, JSContextRef ctx)
: m_wrappedObject(wrappedObject), m_context(ctx) {
// Set up signal wrappers
const QMetaObject* metaObject = wrappedObject->metaObject();
for (int i = 0; i < metaObject->methodCount(); ++i) {
QMetaMethod method = metaObject->method(i);
if (method.methodType() == QMetaMethod::Signal) {
m_signalConnections[method.name()] = QList<JSObjectRef>();
}
}
}
QObject* wrappedObject() const { return m_wrappedObject; }
bool connectSignal(const QString& signalName, JSContextRef ctx, JSObjectRef callback) {
if (!m_signalConnections.contains(signalName)) {
return false;
}
m_signalConnections[signalName].append(callback);
JSValueProtect(ctx, callback);
// Connect the actual Qt signal to our slot
const QMetaObject* metaObject = m_wrappedObject->metaObject();
int signalIndex = metaObject->indexOfSignal(signalName.toUtf8().constData());
if (signalIndex != -1) {
QMetaObject::connect(m_wrappedObject, signalIndex, this, metaObject()->methodCount() + signalIndex);
}
return true;
}
JSValueRef callMethod(JSContextRef ctx, const QString& methodName, size_t argumentCount, const JSValueRef arguments[], JSValueRef* exception) {
QVariantList qtArgs;
for (size_t i = 0; i < argumentCount; ++i) {
QVariant qtArg = ConvertJSToQVariant(ctx, arguments[i]);
qtArgs.append(qtArg);
}
QVariant returnValue;
bool success = QMetaObject::invokeMethod(m_wrappedObject, methodName.toUtf8().constData(),
Qt::DirectConnection,
Q_RETURN_ARG(QVariant, returnValue),
Q_ARG(QVariantList, qtArgs));
if (!success) {
JSStringRef errorMsg = JSStringCreateWithUTF8CString("Failed to invoke Qt method");
*exception = JSValueMakeString(ctx, errorMsg);
JSStringRelease(errorMsg);
return JSValueMakeUndefined(ctx);
}
return ConvertQVariantToJS(ctx, returnValue);
}
protected:
int qt_metacall(QMetaObject::Call call, int methodId, void **args) override {
methodId = QObject::qt_metacall(call, methodId, args);
if (methodId < 0)
return methodId;
if (call == QMetaObject::InvokeMetaMethod) {
const QMetaObject* metaObject = m_wrappedObject->metaObject();
QMetaMethod signal = metaObject->method(methodId);
QString signalName = QString::fromLatin1(signal.name());
// Convert args to JSValueRef
QList<JSValueRef> jsArgs;
for (int i = 0; i < signal.parameterCount(); ++i) {
QVariant arg(QMetaType::type(signal.parameterType(i)), args[i+1]);
jsArgs.append(ConvertQVariantToJS(m_context, arg));
}
// Call all connected callbacks
for (JSObjectRef callback : m_signalConnections[signalName]) {
JSValueRef exception = nullptr;
JSObjectCallAsFunction(m_context, callback, nullptr, jsArgs.size(), jsArgs.constData(), &exception);
if (exception) {
// Handle exception (e.g., log it)
}
}
return -1;
}
return methodId;
}
private:
QObject* m_wrappedObject;
JSContextRef m_context;
QMap<QString, QList<JSObjectRef>> m_signalConnections;
};
void QtProxyFinalize(JSObjectRef object) {
QtProxy* proxy = static_cast<QtProxy*>(JSObjectGetPrivate(object));
delete proxy;
}
Next, we implement the property access functions. These will use the QMetaObject::property()
function to read or write the corresponding property on the QObject
.
JSValueRef QtProxyGetProperty(JSContextRef ctx, JSObjectRef object, JSStringRef propertyName, JSValueRef *exception) {
QtProxy* proxy = static_cast<QtProxy*>(JSObjectGetPrivate(object));
QObject* qobject = proxy->wrappedObject();
QString propName = QString::fromUtf8(JSStringGetCharactersPtr(propertyName), JSStringGetLength(propertyName));
const QMetaObject* metaObject = qobject->metaObject();
int propertyIndex = metaObject->indexOfProperty(propName.toUtf8().constData());
if (propertyIndex < 0) {
// Check if it's a method
int methodIndex = metaObject->indexOfMethod(propName.toUtf8().constData());
if (methodIndex >= 0) {
// Return a function that will call the method
return JSObjectMakeFunctionWithCallback(ctx, propertyName,
[](JSContextRef ctx, JSObjectRef function, JSObjectRef thisObject, size_t argumentCount, const JSValueRef arguments[], JSValueRef* exception) {
QtProxy* proxy = static_cast<QtProxy*>(JSObjectGetPrivate(thisObject));
JSStringRef propName = JSObjectGetPropertyName(ctx, function);
QString methodName = QString::fromUtf8(JSStringGetCharactersPtr(propName), JSStringGetLength(propName));
return proxy->callMethod(ctx, methodName, argumentCount, arguments, exception);
});
}
JSStringRef errorMsg = JSStringCreateWithUTF8CString("Property not found");
*exception = JSValueMakeString(ctx, errorMsg);
JSStringRelease(errorMsg);
return JSValueMakeUndefined(ctx);
}
QMetaProperty property = metaObject->property(propertyIndex);
QVariant value = property.read(qobject);
return ConvertQVariantToJS(ctx, value);
}
bool QtProxySetProperty(JSContextRef ctx, JSObjectRef object, JSStringRef propertyName, JSValueRef value, JSValueRef *exception) {
QtProxy* proxy = static_cast<QtProxy*>(JSObjectGetPrivate(object));
QObject* qobject = proxy->wrappedObject();
QString propName = QString::fromUtf8(JSStringGetCharactersPtr(propertyName), JSStringGetLength(propertyName));
const QMetaObject* metaObject = qobject->metaObject();
int propertyIndex = metaObject->indexOfProperty(propName.toUtf8().constData());
if (propertyIndex < 0) {
JSStringRef errorMsg = JSStringCreateWithUTF8CString("Property not found");
*exception = JSValueMakeString(ctx, errorMsg);
JSStringRelease(errorMsg);
return false;
}
QMetaProperty property = metaObject->property(propertyIndex);
QVariant qtValue = ConvertJSToQVariant(ctx, value);
return property.write(qobject, qtValue);
}
We need to handle the conversion between Qt's QVariant
and JavaScript's JSValueRef
:
JSValueRef ConvertQVariantToJS(JSContextRef ctx, const QVariant& value) {
switch (value.type()) {
case QVariant::String: {
QString str = value.toString();
JSStringRef jsStr = JSStringCreateWithUTF8CString(str.toUtf8().constData());
JSValueRef result = JSValueMakeString(ctx, jsStr);
JSStringRelease(jsStr);
return result;
}
case QVariant::Int:
return JSValueMakeNumber(ctx, value.toInt());
case QVariant::Double:
return JSValueMakeNumber(ctx, value.toDouble());
case QVariant::Bool:
return JSValueMakeBoolean(ctx, value.toBool());
case QVariant::List: {
QVariantList list = value.toList();
JSValueRef* arrayElements = new JSValueRef[list.size()];
for (int i = 0; i < list.size(); ++i) {
arrayElements[i] = ConvertQVariantToJS(ctx, list[i]);
}
JSObjectRef jsArray = JSObjectMakeArray(ctx, list.size(), arrayElements, nullptr);
delete[] arrayElements;
return jsArray;
}
// Add more cases as needed
default:
// For unsupported types, you might want to add custom conversion logic
// or return undefined
return JSValueMakeUndefined(ctx);
}
}
QVariant ConvertJSToQVariant(JSContextRef ctx, JSValueRef value) {
if (JSValueIsString(ctx, value)) {
JSStringRef jsString = JSValueToStringCopy(ctx, value, nullptr);
size_t maxBufferSize = JSStringGetMaximumUTF8CStringSize(jsString);
char* buffer = new char[maxBufferSize];
JSStringGetUTF8CString(jsString, buffer, maxBufferSize);
QString qString = QString::fromUtf8(buffer);
delete[] buffer;
JSStringRelease(jsString);
return QVariant(qString);
} else if (JSValueIsNumber(ctx, value)) {
return QVariant(JSValueToNumber(ctx, value, nullptr));
} else if (JSValueIsBoolean(ctx, value)) {
return QVariant(JSValueToBoolean(ctx, value));
} else if (JSValueIsArray(ctx, value)) {
JSObjectRef jsArray = JSValueToObject(ctx, value, nullptr);
JSPropertyNameArrayRef propertyNames = JSObjectCopyPropertyNames(ctx, jsArray);
size_t count = JSPropertyNameArrayGetCount(propertyNames);
QVariantList list;
for (size_t i = 0; i < count; ++i) {
JSValueRef element = JSObjectGetPropertyAtIndex(ctx, jsArray, i, nullptr);
list.append(ConvertJSToQVariant(ctx, element));
}
JSPropertyNameArrayRelease(propertyNames);
return QVariant(list);
}
// Add more type checks and conversions as needed
return QVariant();
}
To ensure proper memory management, especially when Qt objects are exposed to JavaScript, we implement a tracking system:
class QtJSCBridge {
public:
QtJSCBridge(JSContextRef ctx) : m_context(ctx) {}
void registerObject(QObject* obj, JSObjectRef jsObj) {
m_mutex.lock();
m_objectMap[obj] = jsObj;
JSValueProtect(m_context, jsObj);
m_mutex.unlock();
connect(obj, &QObject::destroyed, this, &QtJSCBridge::onObjectDestroyed);
}
void unregisterObject(QObject* obj) {
m_mutex.lock();
auto it = m_objectMap.find(obj);
if (it != m_objectMap.end()) {
JSValueUnprotect(m_context, it.value());
m_objectMap.erase(it);
}
m_mutex.unlock();
}
void onObjectDestroyed(QObject* obj) {
unregisterObject(obj);
}
void lockJSExecution() { m_mutex.lock(); }
void unlockJSExecution() { m_mutex.unlock(); }
private:
QMap<QObject*, JSObjectRef> m_objectMap;
QMutex m_mutex;
JSContextRef m_context;
};
For frequently accessed methods and properties, you might want to implement a caching mechanism:
class QtJSCCache {
public:
QMetaMethod getCachedMethod(const QMetaObject* metaObj, const QString& methodName) {
auto key = qMakePair(metaObj, methodName);
if (!m_methodCache.contains(key)) {
m_methodCache[key] = metaObj->method(metaObj->indexOfMethod(methodName.toUtf8().constData()));
}
return m_methodCache[key];
}
QMetaProperty getCachedProperty(const QMetaObject* metaObj, const QString& propertyName) {
auto key = qMakePair(metaObj, propertyName);
if (!m_propertyCache.contains(key)) {
m_propertyCache[key] = metaObj->property(metaObj->indexOfProperty(propertyName.toUtf8().constData()));
}
return m_propertyCache[key];
}
private:
QHash<QPair<const QMetaObject*, QString>, QMetaMethod> m_methodCache;
QHash<QPair<const QMetaObject*, QString>, QMetaProperty> m_propertyCache;
};
Here's an example of how to use the above code to expose Qt classes like QTimer, QPushButton, and QLineEdit to JavaScript:
#include <JavaScriptCore/JavaScript.h>
#include "QtJSCBridge.h" // Our QtJSCBridge class
#include "QtProxy.h" // Assume this contains our QtProxyClass definition
template<typename QtClass>
JSObjectRef createQtObject(JSContextRef ctx, JSObjectRef constructor, size_t argumentCount, const JSValueRef arguments[], JSValueRef* exception, QtJSCBridge* bridge) {
QtClass* object = new QtClass();
QtProxy* proxy = new QtProxy(object, ctx);
JSObjectRef jsObject = JSObjectMake(ctx, QtProxyClass, proxy);
bridge->registerObject(object, jsObject);
return jsObject;
}
template<typename QtClass>
void SetupQtClass(JSContextRef ctx, JSObjectRef globalObject, QtJSCBridge* bridge, const char* className) {
JSObjectRef classConstructor = JSObjectMakeConstructor(ctx, NULL,
[bridge](JSContextRef ctx, JSObjectRef constructor, size_t argumentCount, const JSValueRef arguments[], JSValueRef* exception) {
return createQtObject<QtClass>(ctx, constructor, argumentCount, arguments, exception, bridge);
});
JSStringRef jsClassName = JSStringCreateWithUTF8CString(className);
JSObjectSetProperty(ctx, globalObject, jsClassName, classConstructor, kJSPropertyAttributeNone, NULL);
JSStringRelease(jsClassName);
}
// In your main application setup
void setupJSEnvironment() {
JSContextRef ctx = /* initialize your JS context */;
JSObjectRef globalObject = JSContextGetGlobalObject(ctx);
QtJSCBridge* bridge = new QtJSCBridge(ctx);
// Set up various Qt classes
SetupQtClass<QTimer>(ctx, globalObject, bridge, "QTimer");
SetupQtClass<QPushButton>(ctx, globalObject, bridge, "QPushButton");
SetupQtClass<QLineEdit>(ctx, globalObject, bridge, "QLineEdit");
// ... add more Qt classes as needed
// ... set up other parts of your JS environment
}
Here's how you can use the exposed Qt classes from JavaScript:
// Create and use a QTimer
let timer = new QTimer();
timer.timeout.connect(() => console.log("Timer triggered!"));
timer.start(1000);
// Create and use a QPushButton
let button = new QPushButton();
button.setText("Click me!");
button.clicked.connect(() => console.log("Button clicked!"));
// Create and use a QLineEdit
let lineEdit = new QLineEdit();
lineEdit.setText("Enter text here");
lineEdit.textChanged.connect((text) => console.log("Text changed:", text));
// Later, we can interact with these objects
timer.stop();
console.log("Button text:", button.text());
lineEdit.clear();
This implementation provides a robust framework for dynamically exposing Qt classes to JavaScript using the JavaScriptCore API. By leveraging Qt's powerful meta-object system and JavaScriptCore's C API, this dynamic proxy approach allows you to create a flexible, maintainable bridge between JavaScript and your C++ code in Ultralight applications.
Key features of this approach include:
- Dynamic forwarding of method calls and property accesses.
- Automatic type conversion between Qt and JavaScript types.
- Support for Qt's signal-slot mechanism, allowing JavaScript functions to connect to Qt signals.
- Error handling for method calls and property accesses.
- Memory management to prevent leaks and ensure proper object lifecycle.
- Thread safety considerations for multi-threaded applications.
- Performance optimization through caching of frequently accessed methods and properties.
While this implementation covers many bases, remember that depending on your specific use case, you may need to:
- Expand the type conversion functions to cover all types used in your application.
- Implement a more comprehensive memory management strategy, especially for complex object graphs.
- Add more robust error handling and reporting.
- Optimize performance for your specific usage patterns.
- Enhance the signal-slot connection mechanism to support disconnecting signals or connecting to specific overloads of signals.
By using this dynamic proxy approach, you can significantly reduce the need for manual bindings while providing a powerful interface for JavaScript to interact with your Qt objects. This can greatly simplify the process of exposing C++ functionality to web-based UIs in your Ultralight applications.
Remember that while this approach is powerful and flexible, it may not be suitable for all use cases. For more complex scenarios or when deeper integration between Qt and JavaScript is required, you might want to consider using Qt's own QML engine or other dedicated Qt/JavaScript bridge solutions.