Add QObject.parent() and infra for correct subclass wrappers

This commit is contained in:
Simon Edwards 2022-05-05 17:10:40 +02:00
parent bc9bf12e11
commit 880ea7c998
15 changed files with 109 additions and 86 deletions

View File

@ -35,9 +35,7 @@ class DLL_EXPORT NAbstractItemModel : public QAbstractItemModel,
return *newIndex;
}
QObject *parent() const {
return nullptr;
}
QObject* parent() const { return nullptr; }
QModelIndex parent(const QModelIndex& child) const override {
Napi::Env env = this->dispatchOnNode.Env();

View File

@ -8,10 +8,9 @@
#include "core/WrapperCache/wrappercache.h"
/*
This macro adds common QObject exported methods
The exported methods are taken into this macro to avoid writing them in each
and every widget we export.
This macro adds common QObject exported methods
The exported methods are taken into this macro to avoid writing them in each
and every widget we export.
*/
#ifndef QOBJECT_WRAPPED_METHODS_DECLARATION_WITH_EVENT_SOURCE
@ -22,7 +21,8 @@
Napi::Value __id__(const Napi::CallbackInfo& info) { \
Napi::Env env = info.Env(); \
return Napi::Value::From( \
env, extrautils::hashPointerTo53bit(this->instance.data())); \
env, extrautils::hashPointerTo53bit( \
static_cast<QObject*>(this->instance.data()))); \
} \
Napi::Value inherits(const Napi::CallbackInfo& info) { \
Napi::Env env = info.Env(); \
@ -93,7 +93,7 @@
} \
Napi::Value parent(const Napi::CallbackInfo& info) { \
Napi::Env env = info.Env(); \
QObject *parent = this->instance->parent(); \
QObject* parent = this->instance->parent(); \
if (parent) { \
return WrapperCache::instance.getWrapper(env, parent); \
} else { \
@ -109,8 +109,7 @@
Napi::Env env = info.Env(); \
delete static_cast<QObject*>(this->instance); \
return env.Null(); \
} \
}
// Ideally this macro below should go in
// QOBJECT_WRAPPED_METHODS_DECLARATION_WITH_EVENT_SOURCE but some wrappers
@ -176,4 +175,22 @@
#define QOBJECT_SIGNALS QOBJECT_SIGNALS_ON_TARGET(this)
#endif // QOBJECT_SIGNALS
/*
Macro to register a function to wrap QObject pointers of a
given subclass to wrapper instances. First parameter is the
plain name of the QObject subclass (no quotes), seconds is the
name of the wrapper class.
*/
#ifndef QOBJECT_REGISTER_WRAPPER
#define QOBJECT_REGISTER_WRAPPER(qobjectType, ComponentWrapName) \
WrapperCache::instance.registerWrapper( \
QString(#qobjectType), \
[](Napi::Env env, QObject* qobject) -> Napi::Object { \
qobjectType* exactQObject = dynamic_cast<qobjectType*>(qobject); \
Napi::Object wrapper = ComponentWrapName::constructor.New( \
{Napi::External<QObject>::New(env, exactQObject)}); \
return wrapper; \
});
#endif // QOBJECT_REGISTER_WRAPPER
#include "QtCore/QObject/qobject_wrap.h"

View File

@ -29,8 +29,7 @@ class DLL_EXPORT NApplication : public QApplication, public EventWidget {
this, &QGuiApplication::primaryScreenChanged, [=](QScreen* screen) {
Napi::Env env = this->emitOnNode.Env();
Napi::HandleScope scope(env);
auto instance =
WrapperCache::instance.get<QScreen>(env, screen, &QScreenWrap::constructor, false);
auto instance = WrapperCache::instance.getWrapper(env, screen, true);
this->emitOnNode.Call(
{Napi::String::New(env, "primaryScreenChanged"), instance});
});
@ -38,8 +37,7 @@ class DLL_EXPORT NApplication : public QApplication, public EventWidget {
QObject::connect(this, &QGuiApplication::screenAdded, [=](QScreen* screen) {
Napi::Env env = this->emitOnNode.Env();
Napi::HandleScope scope(env);
auto instance =
WrapperCache::instance.get<QScreen>(env, screen, &QScreenWrap::constructor, false);
auto instance = WrapperCache::instance.getWrapper(env, screen, true);
this->emitOnNode.Call({Napi::String::New(env, "screenAdded"), instance});
});
@ -47,8 +45,7 @@ class DLL_EXPORT NApplication : public QApplication, public EventWidget {
this, &QGuiApplication::screenRemoved, [=](QScreen* screen) {
Napi::Env env = this->emitOnNode.Env();
Napi::HandleScope scope(env);
auto instance =
WrapperCache::instance.get<QScreen>(env, screen, &QScreenWrap::constructor, false);
auto instance = WrapperCache::instance.getWrapper(env, screen, true);
this->emitOnNode.Call(
{Napi::String::New(env, "screenRemoved"), instance});
});

View File

@ -427,7 +427,7 @@
Napi::Env env = info.Env(); \
QWindow* window = this->instance->windowHandle(); \
if (window) { \
return WrapperCache::instance.get<QWindow>(env, window, &QWindowWrap::constructor, false); \
return WrapperCache::instance.getWrapper(env, window, true); \
} else { \
return env.Null(); \
} \

View File

@ -2,19 +2,19 @@
#include <napi.h>
#include <QDebug>
#include <QtCore/QMap>
#include <QtCore/QObject>
#include "Extras/Export/export.h"
#include "Extras/Utils/nutils.h"
struct CachedObject {
napi_ref ref;
napi_env env;
};
typedef Napi::Object (*WrapFunc)(Napi::Env, QObject *);
typedef Napi::Object (*WrapFunc)(Napi::Env, QObject*);
/**
* C++ side cache for wrapper objects.
@ -37,41 +37,28 @@ class DLL_EXPORT WrapperCache : public QObject {
static WrapperCache instance;
/**
* Get a wrapper for a given Qt object.
* Register a function to wrap certain instances of a `QObject` subclass.
*
* @param T - (template argument) The Qt class of the object being cached,
* e.g. `QScreen`.
* @param W - (template argument) The wrapper type which matches the object
* `QScreenWrap`.
* @param env = Napi environment
* @param object - Pointer to the QObject for which a wrapper is required.
* @return The JS wrapper object.
* @param typeName - The name of the `QObject` subclass this wrapper function
* applies to.
* @param wrapFunc - Function to wrap `QObject` instances.
*/
template <class T>
Napi::Object get(Napi::Env env, T* object, Napi::FunctionReference* constructorFunc, bool isCreatedByNodeGui) {
uint64_t ptrHash = extrautils::hashPointerTo53bit(object);
if (this->cache.contains(ptrHash)) {
napi_value result = nullptr;
napi_get_reference_value(env, this->cache[ptrHash].ref, &result);
napi_valuetype valuetype;
napi_typeof(env, result, &valuetype);
if (valuetype != napi_null) {
return Napi::Object(env, result);
}
}
Napi::Object wrapper = constructorFunc->New({Napi::External<T>::New(env, object)});
store(env, extrautils::hashPointerTo53bit(object), object, wrapper, isCreatedByNodeGui);
return wrapper;
}
void registerWrapper(QString typeName, WrapFunc wrapFunc) {
this->wrapperRegistry[typeName] = wrapFunc;
}
Napi::Value getWrapper(Napi::Env env, QObject* qobject) {
/**
* Get a wrapper for a QObject
*
* @param env - Napi environment
* @param qobject - The QObject or subclass instance to wrap
* @param keepAlive - Set this to true if the wrapper object should be kept
* alive until the underlying QObject is destroyed regardless of whether
* the JS side holding a reference to it or not. (Defaults to false).
* @return Napi object wrapping the object
*/
Napi::Value getWrapper(Napi::Env env, QObject* qobject,
bool keepAlive = false) {
if (qobject == nullptr) {
return env.Null();
}
@ -88,10 +75,15 @@ class DLL_EXPORT WrapperCache : public QObject {
}
}
// QString className(object->metaObject()->className());
// if (this->wrapperRegistry.contains(className)) {
// this->wrapperRegistry[className]
// }
QString className(qobject->metaObject()->className());
if (this->wrapperRegistry.contains(className)) {
Napi::Object wrapper = this->wrapperRegistry[className](env, qobject);
store(env, ptrHash, qobject, wrapper, !keepAlive);
return wrapper;
} else {
qDebug() << "NodeGui: Unable to find wrapper for instance of class "
<< className << ".\n\n";
}
return env.Null();
}
@ -107,7 +99,8 @@ class DLL_EXPORT WrapperCache : public QObject {
* @param object - Pointer to the QObject for which a wrapper is required.
* @param wrapper - The wrapper object matching `object`.
*/
void store(Napi::Env env, uint64_t ptrHash, QObject *qobject, Napi::Object wrapper, bool isWeak) {
void store(Napi::Env env, uint64_t ptrHash, QObject* qobject,
Napi::Object wrapper, bool isWeak) {
napi_ref ref = nullptr;
napi_create_reference(env, wrapper, isWeak ? 0 : 1, &ref);
@ -121,8 +114,7 @@ class DLL_EXPORT WrapperCache : public QObject {
static Napi::Object init(Napi::Env env, Napi::Object exports) {
exports.Set("WrapperCache_injectCallback",
Napi::Function::New<injectDestroyCallback>(env));
exports.Set("WrapperCache_store",
Napi::Function::New<storeJS>(env));
exports.Set("WrapperCache_store", Napi::Function::New<storeJS>(env));
return exports;
}
@ -158,14 +150,10 @@ class DLL_EXPORT WrapperCache : public QObject {
if (destroyedCallback) {
Napi::Env env = destroyedCallback.Env();
Napi::HandleScope scope(env);
destroyedCallback.Call(
env.Global(),
{Napi::Value::From(env, ptrHash)});
destroyedCallback.Call(env.Global(), {Napi::Value::From(env, ptrHash)});
}
uint32_t result = 0;
// TODO: Grab the wrapper C++ object and null out its ref to the Qt object.
napi_reference_unref(this->cache[ptrHash].env, this->cache[ptrHash].ref,
&result);
this->cache.remove(ptrHash);

View File

@ -12,13 +12,7 @@ Napi::Object QObjectWrap::init(Napi::Env env, Napi::Object exports) {
env, CLASSNAME, {QOBJECT_WRAPPED_METHODS_EXPORT_DEFINE(QObjectWrap)});
constructor = Napi::Persistent(func);
exports.Set(CLASSNAME, func);
WrapperCache::instance.registerWrapper(QString("NObject"),
[](Napi::Env env, QObject *qobject) -> Napi::Object {
QObject *exactQObject = dynamic_cast<QObject*>(qobject);
Napi::Object wrapper = QObjectWrap::constructor.New({Napi::External<QObject>::New(env, exactQObject)});
return wrapper;
});
QOBJECT_REGISTER_WRAPPER(NObject, QObjectWrap);
return exports;
}

View File

@ -118,8 +118,7 @@ Napi::Value StaticQApplicationWrapMethods::clipboard(
Napi::Env env = info.Env();
QClipboard* clipboard = QApplication::clipboard();
if (clipboard) {
return WrapperCache::instance.get<QClipboard>(env,
clipboard, &QClipboardWrap::constructor, false);
return WrapperCache::instance.getWrapper(env, clipboard, true);
} else {
return env.Null();
}
@ -163,7 +162,7 @@ Napi::Value StaticQApplicationWrapMethods::primaryScreen(
Napi::Env env = info.Env();
auto screen = QApplication::primaryScreen();
if (screen) {
return WrapperCache::instance.get<QScreen>(env, screen, &QScreenWrap::constructor, false);
return WrapperCache::instance.getWrapper(env, screen, true);
} else {
return env.Null();
}
@ -176,8 +175,7 @@ Napi::Value StaticQApplicationWrapMethods::screens(
Napi::Array jsArray = Napi::Array::New(env, screens.size());
for (int i = 0; i < screens.size(); i++) {
QScreen* screen = screens[i];
auto instance =
WrapperCache::instance.get<QScreen>(env, screen, &QScreenWrap::constructor, false);
auto instance = WrapperCache::instance.getWrapper(env, screen, true);
jsArray[i] = instance;
}
return jsArray;

View File

@ -19,6 +19,7 @@ Napi::Object QClipboardWrap::init(Napi::Env env, Napi::Object exports) {
QOBJECT_WRAPPED_METHODS_EXPORT_DEFINE(QClipboardWrap)});
constructor = Napi::Persistent(func);
exports.Set(CLASSNAME, func);
QOBJECT_REGISTER_WRAPPER(QClipboard, QClipboardWrap);
return exports;
}

View File

@ -16,6 +16,7 @@ Napi::Object QScreenWrap::init(Napi::Env env, Napi::Object exports) {
QOBJECT_WRAPPED_METHODS_EXPORT_DEFINE(QScreenWrap)});
constructor = Napi::Persistent(func);
exports.Set(CLASSNAME, func);
QOBJECT_REGISTER_WRAPPER(QScreen, QScreenWrap);
return exports;
}

View File

@ -55,8 +55,7 @@ void QWindowWrap::connectSignalsToEventEmitter() {
this->instance.data(), &QWindow::screenChanged, [=](QScreen* screen) {
Napi::Env env = this->emitOnNode.Env();
Napi::HandleScope scope(env);
auto instance =
WrapperCache::instance.get<QScreen>(env, screen, &QScreenWrap::constructor, false);
auto instance = WrapperCache::instance.getWrapper(env, screen, true);
this->emitOnNode.Call(
{Napi::String::New(env, "screenChanged"), instance});
});
@ -81,7 +80,7 @@ Napi::Value QWindowWrap::screen(const Napi::CallbackInfo& info) {
Napi::Env env = info.Env();
QScreen* screen = this->instance->screen();
if (screen) {
return WrapperCache::instance.get<QScreen>(env, screen, &QScreenWrap::constructor, false);
return WrapperCache::instance.getWrapper(env, screen, true);
} else {
return env.Null();
}

View File

@ -12,6 +12,7 @@ Napi::Object QWidgetWrap::init(Napi::Env env, Napi::Object exports) {
env, CLASSNAME, {QWIDGET_WRAPPED_METHODS_EXPORT_DEFINE(QWidgetWrap)});
constructor = Napi::Persistent(func);
exports.Set(CLASSNAME, func);
QOBJECT_REGISTER_WRAPPER(NWidget, QWidgetWrap);
return exports;
}

View File

@ -16,6 +16,7 @@ Napi::Object CacheTestQObjectWrap::init(Napi::Env env, Napi::Object exports) {
QOBJECT_WRAPPED_METHODS_EXPORT_DEFINE(CacheTestQObjectWrap)});
constructor = Napi::Persistent(func);
exports.Set(CLASSNAME, func);
QOBJECT_REGISTER_WRAPPER(CacheTestQObject, CacheTestQObjectWrap);
return exports;
}
@ -47,8 +48,7 @@ void CacheTestQObjectWrap::connectSignalsToEventEmitter() {
Napi::Value CacheTestQObjectWrap::foo(const Napi::CallbackInfo& info) {
Napi::Env env = info.Env();
CacheTestQObject* foo = this->instance->foo();
return WrapperCache::instance.get<CacheTestQObject>(
env, foo, &CacheTestQObjectWrap::constructor, false);
return WrapperCache::instance.getWrapper(env, foo);
}
Napi::Value CacheTestQObjectWrap::clearFoo(const Napi::CallbackInfo& info) {
@ -60,6 +60,5 @@ Napi::Value CacheTestQObjectWrap::clearFoo(const Napi::CallbackInfo& info) {
Napi::Value CacheTestQObjectWrap::bar(const Napi::CallbackInfo& info) {
Napi::Env env = info.Env();
CacheTestQObject* bar = this->instance->bar();
return WrapperCache::instance.get<CacheTestQObject>(
env, bar, &CacheTestQObjectWrap::constructor, false);
return WrapperCache::instance.getWrapper(env, bar);
}

View File

@ -1,3 +1,4 @@
import { QObject } from '../QtCore/QObject';
import addon from '../utils/addon';
import { NativeElement } from './Component';
@ -15,11 +16,17 @@ import { NativeElement } from './Component';
export class WrapperCache {
private _strongCache = new Map<number, QObject>();
private _weakCache = new Map<number, WeakRef<QObject>>();
private _wrapperRegistry = new Map<string, { new (native: any): QObject }>();
constructor() {
addon.WrapperCache_injectCallback(this._objectDestroyedCallback.bind(this));
}
_flush(): void {
this._strongCache = new Map<number, QObject>();
this._weakCache = new Map<number, WeakRef<QObject>>();
}
private _objectDestroyedCallback(objectId: number): void {
if (this._strongCache.has(objectId)) {
const wrapper = this._strongCache.get(objectId);
@ -47,12 +54,13 @@ export class WrapperCache {
return wrapper;
}
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
getWrapper(native: any): QObject | null {
if (native == null) {
return null;
}
const id = native.__id__();
const id = native.__id__();
if (this._strongCache.has(id)) {
return this._strongCache.get(id);
}
@ -65,7 +73,19 @@ export class WrapperCache {
}
}
return null; // FIXME: Create new wrapper on demand.
if (this._wrapperRegistry.has(native.wrapperType)) {
const wrapper = new (this._wrapperRegistry.get(native.wrapperType))(native);
this.store(wrapper);
return wrapper;
} else {
console.log(`NodeGui: Unable to find JS wrapper for type '${native.wrapperType}'.`);
}
return null;
}
registerWrapper(qobjectClassName: string, wrapperConstructor: { new (native: any): QObject }): void {
this._wrapperRegistry.set(qobjectClassName, wrapperConstructor);
}
store(wrapper: QObject): void {

View File

@ -6,7 +6,7 @@ import { wrapperCache } from '../../core/WrapperCache';
export class CacheTestQObject extends QObject<QObjectSignals> {
constructor(arg?: NativeElement) {
let native;
if (native == null) {
if (arg == null) {
native = new addon.CacheTestQObject();
} else {
native = arg;
@ -16,7 +16,7 @@ export class CacheTestQObject extends QObject<QObjectSignals> {
}
foo(): CacheTestQObject {
return wrapperCache.get<CacheTestQObject>(CacheTestQObject, this.native.foo());
return wrapperCache.getWrapper(this.native.foo()) as CacheTestQObject;
}
clearFoo(): void {
@ -24,6 +24,7 @@ export class CacheTestQObject extends QObject<QObjectSignals> {
}
bar(): CacheTestQObject {
return wrapperCache.get<CacheTestQObject>(CacheTestQObject, this.native.bar());
return wrapperCache.getWrapper(this.native.bar()) as CacheTestQObject;
}
}
wrapperCache.registerWrapper('CacheTestQObjectWrap', CacheTestQObject);

View File

@ -1,12 +1,14 @@
import { QObject } from '../../QtCore/QObject';
import { QApplication } from '../../QtGui/QApplication';
import { CacheTestQObject } from './CacheTestQObject';
import { wrapperCache } from '../WrapperCache';
describe('WrapperCache using CacheTestQObject', () => {
const qApp = QApplication.instance();
qApp.setQuitOnLastWindowClosed(true);
it('Cached foo', () => {
wrapperCache._flush();
const a = new CacheTestQObject();
expect(a).not.toBeNull();
@ -14,11 +16,12 @@ describe('WrapperCache using CacheTestQObject', () => {
expect(foo).not.toBeNull();
const foo2 = a.foo();
expect(foo).toBe(foo2);
expect(foo.native.__id__()).toBe(foo2.native.__id__());
expect(foo).toBe(foo2);
});
it('clearFoo() and wrapper expiration', () => {
wrapperCache._flush();
const a = new CacheTestQObject();
const foo = a.foo();
a.clearFoo();
@ -26,6 +29,7 @@ describe('WrapperCache using CacheTestQObject', () => {
});
it('clearFoo() and new wrapper', () => {
wrapperCache._flush();
const a = new CacheTestQObject();
const foo = a.foo();
const fooId = foo.native.__id__();
@ -38,6 +42,7 @@ describe('WrapperCache using CacheTestQObject', () => {
});
it('Cached foo and bar', () => {
wrapperCache._flush();
const a = new CacheTestQObject();
const foo = a.foo();
const bar = a.bar();
@ -46,11 +51,13 @@ describe('WrapperCache using CacheTestQObject', () => {
});
it('QObject.parent() can be null', () => {
wrapperCache._flush();
const a = new QObject();
expect(a.parent()).toBeNull();
});
it('QObject.parent() === QObject.parent()', () => {
wrapperCache._flush();
const a = new QObject();
const b = new QObject(a);
expect(a.native.__id__()).toEqual(b.parent().native.__id__());
@ -60,12 +67,14 @@ describe('WrapperCache using CacheTestQObject', () => {
});
it('QObject.delete() clears the native field', () => {
wrapperCache._flush();
const a = new QObject();
a.delete();
expect(a.native).toBeNull();
});
it('QObject.delete() clears chains of QObjects and their native field', () => {
wrapperCache._flush();
const a = new QObject();
const b = new QObject(a);
a.delete();