Add wrapper caching. Try it on QScreen.

This commit is contained in:
Simon Edwards 2021-10-27 10:46:12 +02:00
parent 05c690dcd9
commit 710cfa3d31
13 changed files with 190 additions and 13 deletions

View File

@ -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"

View File

@ -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 <typename T>
void safeDelete(QPointer<T>& component) {

View File

@ -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), \

View File

@ -0,0 +1,34 @@
#pragma once
#include <napi.h>
#include <QtCore/QMap>
#include <QtCore/QObject>
#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<const QObject*, CachedObject> 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);
};

View File

@ -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<uint64_t>(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) {

View File

@ -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<QScreen>::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<QScreen>::New(env, screen)});
auto instance = WrapperCache::instance.get(info, screen);
jsArray[i] = instance;
}
return jsArray;

View File

@ -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<QScreen>::New(env, screen)});
return instance;
return WrapperCache::instance.get(info, screen);
} else {
return env.Null();
}

View File

@ -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<injectDestroyCallback>(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<Napi::Function>());
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<QScreen>::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))});
}
}

View File

@ -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);

View File

@ -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<QApplicationSignals> {
if (screenNative == null) {
return null;
}
return new QScreen(screenNative);
return wrapperCache.get<QScreen>(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>(QScreen, screenNative));
}
static setStyle(style: QStyle): void {
addon.QApplication.setStyle(style.native);

View File

@ -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<QWindowSignals> {
native: NativeElement;
@ -16,8 +17,7 @@ export class QWindow extends NodeObject<QWindowSignals> {
}
screen(): QScreen {
const screenNative = this.native.screen();
return new QScreen(screenNative);
return wrapperCache.get<QScreen>(QScreen, this.native.screen());
}
}

View File

@ -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<number, any>();
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<T>(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();

View File

@ -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