From 710cfa3d317c45b58f9055560b949d4b65891513 Mon Sep 17 00:00:00 2001 From: Simon Edwards Date: Wed, 27 Oct 2021 10:46:12 +0200 Subject: [PATCH] Add wrapper caching. Try it on `QScreen`. --- CMakeLists.txt | 5 ++ src/cpp/include/nodegui/Extras/Utils/nutils.h | 1 + .../nodegui/QtCore/QObject/qobject_macro.h | 9 ++- .../nodegui/core/WrapperCache/wrappercache.h | 34 +++++++++ src/cpp/lib/Extras/Utils/nutils.cpp | 14 ++++ .../QtGui/QApplication/qapplication_wrap.cpp | 7 +- src/cpp/lib/QtGui/QWindow/qwindow_wrap.cpp | 5 +- .../lib/core/WrapperCache/wrappercache.cpp | 70 +++++++++++++++++++ src/cpp/main.cpp | 3 + src/lib/QtGui/QApplication.ts | 5 +- src/lib/QtGui/QWindow.ts | 4 +- src/lib/core/WrapperCache.ts | 44 ++++++++++++ .../development/signal_and_event_handling.md | 2 +- 13 files changed, 190 insertions(+), 13 deletions(-) create mode 100644 src/cpp/include/nodegui/core/WrapperCache/wrappercache.h create mode 100644 src/cpp/lib/core/WrapperCache/wrappercache.cpp create mode 100644 src/lib/core/WrapperCache.ts diff --git a/CMakeLists.txt b/CMakeLists.txt index 7bdd5c41a..a9092645c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -11,6 +11,9 @@ set(CORE_WIDGETS_ADDON "nodegui_core") project(${CORE_WIDGETS_ADDON}) + +# Note: CMake+moc also use this list when finding files which `moc` applied. + add_library(${CORE_WIDGETS_ADDON} SHARED "${CMAKE_JS_SRC}" "${PROJECT_SOURCE_DIR}/src/cpp/main.cpp" @@ -24,6 +27,8 @@ add_library(${CORE_WIDGETS_ADDON} SHARED "${PROJECT_SOURCE_DIR}/src/cpp/lib/core/Events/eventsmap.cpp" "${PROJECT_SOURCE_DIR}/src/cpp/lib/core/Events/eventwidget.cpp" "${PROJECT_SOURCE_DIR}/src/cpp/lib/core/YogaWidget/yogawidget.cpp" + "${PROJECT_SOURCE_DIR}/src/cpp/include/nodegui/core/WrapperCache/wrappercache.h" + "${PROJECT_SOURCE_DIR}/src/cpp/lib/core/WrapperCache/wrappercache.cpp" # core deps "${PROJECT_SOURCE_DIR}/src/cpp/include/deps/yoga/log.cpp" "${PROJECT_SOURCE_DIR}/src/cpp/include/deps/yoga/Utils.cpp" diff --git a/src/cpp/include/nodegui/Extras/Utils/nutils.h b/src/cpp/include/nodegui/Extras/Utils/nutils.h index b6519b68f..5142443ba 100644 --- a/src/cpp/include/nodegui/Extras/Utils/nutils.h +++ b/src/cpp/include/nodegui/Extras/Utils/nutils.h @@ -17,6 +17,7 @@ DLL_EXPORT void* configureQWidget(QWidget* widget, YGNodeRef node, bool isLeafNode = false); DLL_EXPORT void* configureQObject(QObject* object); DLL_EXPORT void* configureComponent(void* component); +DLL_EXPORT uint64_t hashPointerTo53bit(const void* input); template void safeDelete(QPointer& component) { diff --git a/src/cpp/include/nodegui/QtCore/QObject/qobject_macro.h b/src/cpp/include/nodegui/QtCore/QObject/qobject_macro.h index f07e5984a..be1471ec0 100644 --- a/src/cpp/include/nodegui/QtCore/QObject/qobject_macro.h +++ b/src/cpp/include/nodegui/QtCore/QObject/qobject_macro.h @@ -17,6 +17,12 @@ \ EVENTWIDGET_WRAPPED_METHODS_DECLARATION_WITH_EVENT_SOURCE(source) \ \ + Napi::Value __id__(const Napi::CallbackInfo& info) { \ + Napi::Env env = info.Env(); \ + Napi::HandleScope scope(env); \ + return Napi::Value::From( \ + env, extrautils::hashPointerTo53bit(this->instance.data())); \ + } \ Napi::Value inherits(const Napi::CallbackInfo& info) { \ Napi::Env env = info.Env(); \ Napi::HandleScope scope(env); \ @@ -85,7 +91,8 @@ \ EVENTWIDGET_WRAPPED_METHODS_EXPORT_DEFINE(ComponentWrapName) \ \ - InstanceMethod("inherits", &ComponentWrapName::inherits), \ + InstanceMethod("__id__", &ComponentWrapName::__id__), \ + InstanceMethod("inherits", &ComponentWrapName::inherits), \ InstanceMethod("setProperty", &ComponentWrapName::setProperty), \ InstanceMethod("property", &ComponentWrapName::property), \ InstanceMethod("setObjectName", &ComponentWrapName::setObjectName), \ diff --git a/src/cpp/include/nodegui/core/WrapperCache/wrappercache.h b/src/cpp/include/nodegui/core/WrapperCache/wrappercache.h new file mode 100644 index 000000000..a8982d2bf --- /dev/null +++ b/src/cpp/include/nodegui/core/WrapperCache/wrappercache.h @@ -0,0 +1,34 @@ +#pragma once + +#include + +#include +#include + +#include "Extras/Export/export.h" +#include "QtGui/QScreen/qscreen_wrap.h" + +struct CachedObject { + napi_ref ref; + napi_env env; +}; + +class DLL_EXPORT WrapperCache : public QObject { + Q_OBJECT + + private: + QMap cache; + + public: + WrapperCache(); + Napi::Object get(const Napi::CallbackInfo& info, QScreen* screen); + + static WrapperCache instance; + static Napi::Object init(Napi::Env env, Napi::Object exports); + static Napi::Value injectDestroyCallback(const Napi::CallbackInfo& info); + + static Napi::FunctionReference destroyedCallback; + + public Q_SLOTS: + void handleDestroyed(const QObject* object); +}; diff --git a/src/cpp/lib/Extras/Utils/nutils.cpp b/src/cpp/lib/Extras/Utils/nutils.cpp index bd1f87a49..b623b7ed8 100644 --- a/src/cpp/lib/Extras/Utils/nutils.cpp +++ b/src/cpp/lib/Extras/Utils/nutils.cpp @@ -110,6 +110,20 @@ void* extrautils::configureQWidget(QWidget* widget, YGNodeRef node, return configureQObject(widget); } +uint64_t extrautils::hashPointerTo53bit(const void* input) { + // Hash the address of the object down to something which will + // fit into the JS 53bit safe integer space. + uint64_t address = reinterpret_cast(input); + uint64_t top8Bits = address & 0xff00000000000000u; + uint64_t foldedBits = (top8Bits >> 11) ^ address; + + // Clear the top 8bits which we folded, now shift out the last 3 bits + // Pointers are aligned on 64bit architectures to at least 8bytes + // boundaries. + uint64_t result = (foldedBits & ~0xff00000000000000u) >> 3; + return result; +} + Napi::FunctionReference NUtilsWrap::constructor; Napi::Object NUtilsWrap::init(Napi::Env env, Napi::Object exports) { diff --git a/src/cpp/lib/QtGui/QApplication/qapplication_wrap.cpp b/src/cpp/lib/QtGui/QApplication/qapplication_wrap.cpp index 37596b117..b2d94bd5f 100644 --- a/src/cpp/lib/QtGui/QApplication/qapplication_wrap.cpp +++ b/src/cpp/lib/QtGui/QApplication/qapplication_wrap.cpp @@ -5,6 +5,7 @@ #include "QtGui/QPalette/qpalette_wrap.h" #include "QtGui/QStyle/qstyle_wrap.h" #include "core/Integration/qode-api.h" +#include "core/WrapperCache/wrappercache.h" Napi::FunctionReference QApplicationWrap::constructor; @@ -175,8 +176,7 @@ Napi::Value StaticQApplicationWrapMethods::primaryScreen( Napi::HandleScope scope(env); auto screen = QApplication::primaryScreen(); if (screen) { - return QScreenWrap::constructor.New( - {Napi::External::New(env, screen)}); + return WrapperCache::instance.get(info, screen); } else { return env.Null(); } @@ -191,8 +191,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 = QScreenWrap::constructor.New( - {Napi::External::New(env, screen)}); + auto instance = WrapperCache::instance.get(info, screen); jsArray[i] = instance; } return jsArray; diff --git a/src/cpp/lib/QtGui/QWindow/qwindow_wrap.cpp b/src/cpp/lib/QtGui/QWindow/qwindow_wrap.cpp index 7d2633f10..7fabeaaac 100644 --- a/src/cpp/lib/QtGui/QWindow/qwindow_wrap.cpp +++ b/src/cpp/lib/QtGui/QWindow/qwindow_wrap.cpp @@ -2,6 +2,7 @@ #include "Extras/Utils/nutils.h" #include "QtGui/QScreen/qscreen_wrap.h" +#include "core/WrapperCache/wrappercache.h" Napi::FunctionReference QWindowWrap::constructor; @@ -49,9 +50,7 @@ Napi::Value QWindowWrap::screen(const Napi::CallbackInfo& info) { QScreen* screen = this->instance->screen(); if (screen) { - auto instance = QScreenWrap::constructor.New( - {Napi::External::New(env, screen)}); - return instance; + return WrapperCache::instance.get(info, screen); } else { return env.Null(); } diff --git a/src/cpp/lib/core/WrapperCache/wrappercache.cpp b/src/cpp/lib/core/WrapperCache/wrappercache.cpp new file mode 100644 index 000000000..d99637251 --- /dev/null +++ b/src/cpp/lib/core/WrapperCache/wrappercache.cpp @@ -0,0 +1,70 @@ +#include "core/WrapperCache/wrappercache.h" + +#include "Extras/Utils/nutils.h" + +DLL_EXPORT WrapperCache WrapperCache::instance; + +Napi::FunctionReference WrapperCache::destroyedCallback; + +WrapperCache::WrapperCache() {} + +Napi::Object WrapperCache::init(Napi::Env env, Napi::Object exports) { + Napi::HandleScope scope(env); + + exports.Set("WrapperCache_injectCallback", + Napi::Function::New(env)); + return exports; +} + +Napi::Value WrapperCache::injectDestroyCallback( + const Napi::CallbackInfo& info) { + Napi::Env env = info.Env(); + Napi::HandleScope scope(env); + + destroyedCallback = Napi::Persistent(info[0].As()); + return env.Null(); +} + +Napi::Object WrapperCache::get(const Napi::CallbackInfo& info, + QScreen* screen) { + Napi::Env env = info.Env(); + + if (this->cache.contains(screen)) { + napi_value result = nullptr; + napi_get_reference_value(this->cache[screen].env, this->cache[screen].ref, + &result); + return Napi::Object(env, result); + } + + Napi::HandleScope scope(env); + Napi::Object wrapper = + QScreenWrap::constructor.New({Napi::External::New(env, screen)}); + + napi_ref ref = nullptr; + napi_create_reference(env, wrapper, 1, &ref); + this->cache[screen].env = napi_env(env); + this->cache[screen].ref = ref; + + QObject::connect(screen, &QObject::destroyed, this, + &WrapperCache::handleDestroyed); + + return wrapper; +} + +void WrapperCache::handleDestroyed(const QObject* object) { + if (!this->cache.contains(object)) { + return; + } + + uint32_t result = 0; + Napi::Env env = this->cache[object].env; + napi_reference_unref(env, this->cache[object].ref, &result); + this->cache.remove(object); + + // Callback to JS with the address/ID of the destroyed object. So that it + // can clear it out of the cache. + if (destroyedCallback) { + destroyedCallback.Call( + {Napi::Value::From(env, extrautils::hashPointerTo53bit(object))}); + } +} diff --git a/src/cpp/main.cpp b/src/cpp/main.cpp index ea7526d3e..12cb0e0d6 100644 --- a/src/cpp/main.cpp +++ b/src/cpp/main.cpp @@ -114,6 +114,8 @@ #include "QtWidgets/QWidget/qwidget_wrap.h" #include "core/FlexLayout/flexlayout_wrap.h" #include "core/Integration/integration.h" +#include "core/WrapperCache/wrappercache.h" + // These cant be instantiated in JS Side void InitPrivateHelpers(Napi::Env env) { qodeIntegration::integrate(); @@ -123,6 +125,7 @@ void InitPrivateHelpers(Napi::Env env) { Napi::Object Main(Napi::Env env, Napi::Object exports) { InitPrivateHelpers(env); NUtilsWrap::init(env, exports); + WrapperCache::init(env, exports); QApplicationWrap::init(env, exports); QDateWrap::init(env, exports); QDateTimeWrap::init(env, exports); diff --git a/src/lib/QtGui/QApplication.ts b/src/lib/QtGui/QApplication.ts index 945e1447f..ed4714389 100644 --- a/src/lib/QtGui/QApplication.ts +++ b/src/lib/QtGui/QApplication.ts @@ -8,6 +8,7 @@ import { QPalette } from './QPalette'; import { StyleSheet } from '../core/Style/StyleSheet'; import memoizeOne from 'memoize-one'; import { QScreen } from './QScreen'; +import { wrapperCache } from '../core/WrapperCache'; /** @@ -82,11 +83,11 @@ export class QApplication extends NodeObject { if (screenNative == null) { return null; } - return new QScreen(screenNative); + return wrapperCache.get(QScreen, screenNative); } static screens(): QScreen[] { const screenNativeList = addon.QApplication.screens(); - return screenNativeList.map((screenNative: any) => new QScreen(screenNative)); + return screenNativeList.map((screenNative: any) => wrapperCache.get(QScreen, screenNative)); } static setStyle(style: QStyle): void { addon.QApplication.setStyle(style.native); diff --git a/src/lib/QtGui/QWindow.ts b/src/lib/QtGui/QWindow.ts index 0edec5903..2e13d7220 100644 --- a/src/lib/QtGui/QWindow.ts +++ b/src/lib/QtGui/QWindow.ts @@ -2,6 +2,7 @@ import { NativeElement } from '../core/Component'; import { checkIfNativeElement } from '../utils/helpers'; import { NodeObject, QObjectSignals } from '../QtCore/QObject'; import { QScreen } from './QScreen'; +import { wrapperCache } from '../core/WrapperCache'; export class QWindow extends NodeObject { native: NativeElement; @@ -16,8 +17,7 @@ export class QWindow extends NodeObject { } screen(): QScreen { - const screenNative = this.native.screen(); - return new QScreen(screenNative); + return wrapperCache.get(QScreen, this.native.screen()); } } diff --git a/src/lib/core/WrapperCache.ts b/src/lib/core/WrapperCache.ts new file mode 100644 index 000000000..23c2689e2 --- /dev/null +++ b/src/lib/core/WrapperCache.ts @@ -0,0 +1,44 @@ +import addon from '../utils/addon'; + +/** + * JS side cache for wrapper objects. + * + * This is mainly used for caching wrappers of Qt objects which are not + * directly created by our Nodejs application. The purpose of the cache + * is to keep "alive" wrapper objects and their underlying C++ wrappers + * which may be connected to Qt signals from the real Qt object. + * This makes it easier for application to grab one of these objects, + * set up event handlers, and then let the object go and *not* have the + * wrapper automatically and unexpectedly garbage collected. + */ +export class WrapperCache { + private _cache = new Map(); + + constructor() { + addon.WrapperCache_injectCallback(this._objectDestroyedCallback.bind(this)); + } + + private _objectDestroyedCallback(objectId: number): void { + console.log(`_objectDestroyedCallback() id: ${objectId}`); + if (!this._cache.has(objectId)) { + return; + } + const wrapper = this._cache.get(objectId); + wrapper.native = null; + this._cache.delete(objectId); + } + + get(wrapperConstructor: { new (native: any): T }, native: any): T { + const id = native.__id__(); + + console.log(`WrapperCache.get() id: ${id}`); + + if (this._cache.has(id)) { + return this._cache.get(id) as T; + } + const wrapper = new wrapperConstructor(native); + this._cache.set(id, wrapper); + return wrapper; + } +} +export const wrapperCache = new WrapperCache(); diff --git a/website/docs/development/signal_and_event_handling.md b/website/docs/development/signal_and_event_handling.md index 766a851e3..23ee272de 100644 --- a/website/docs/development/signal_and_event_handling.md +++ b/website/docs/development/signal_and_event_handling.md @@ -144,5 +144,5 @@ We need to run Qt's MOC (Meta Object Compiler) on the file whenever we use Q_OBJ # How does it work ? 1. On JS side for each widget instance we create an instance of NodeJS's Event Emitter. This is done by the class `EventWidget` from which `NodeWidget` inherits -2. We send this event emiiter's `emit` function to the C++ side by calling `initNodeEventEmitter` method and store a pointer to the event emitter's emit function using `emitOnNode`. initNodeEventEmitter function is added by a macro from EventWidget (c++). You can find the initNodeEventEmitter method with the event widget macros. +2. We send this event emitter's `emit` function to the C++ side by calling `initNodeEventEmitter` method and store a pointer to the event emitter's emit function using `emitOnNode`. initNodeEventEmitter function is added by a macro from EventWidget (c++). You can find the initNodeEventEmitter method with the event widget macros. 3. We setup Qt's connect method for all the signals that we want to listen to and call the emitOnNode (which is actually emit from Event emitter) whenever a signal arrives. This is done manually on every widget by overriding the method `connectSignalsToEventEmitter`. Check `npushbutton.h` for details. This takes care of all the signals of the widgets. Now to export all qt events of the widget, we had overriden the widgets `event(Event*)` method to listen to events received by the widget and send it to the event emitter. This is done inside the EVENTWIDGET_IMPLEMENTATIONS macro