From 431e8167ed5d050c10d2a49d05a2adcfabcd9270 Mon Sep 17 00:00:00 2001 From: Igor Randjelovic Date: Sat, 3 Jul 2021 19:55:08 +0200 Subject: [PATCH] fix: memory leaks & WeakRefs (#111) * fix: don't allocate persistent empty objects * fix: Blocks memory leak (#100) * feat: drop custom WeakRef implementation & use built-in * fix: free allocated memory in ReferenceWrapper * fix: DictionaryAdapter Hanging References (#114) * Clean up hanging references before deallocating Reduces total dictionary related memory leaks by 36% * fix * chore: more leak fixes * fix: only release enumerator_ when set (#117) * fix: handle methods with pointer type params fixes #109 * fix: referenceWrapper memory leak & CString leak * refactor: ReferenceWrapper dispose to be managed internally * fix: pre-allocate memory even for empty values * fix: undo invalid fix (needs a different way) Co-authored-by: Darin Dimitrov Co-authored-by: Bryse Meijer Co-authored-by: logikgate --- NativeScript/runtime/ArgConverter.mm | 1 + NativeScript/runtime/DataWrapper.h | 17 ++- NativeScript/runtime/DictionaryAdapter.mm | 26 +++- NativeScript/runtime/Helpers.mm | 1 + NativeScript/runtime/Interop.h | 4 +- NativeScript/runtime/Interop.mm | 34 +++-- NativeScript/runtime/ObjectManager.mm | 3 - NativeScript/runtime/Reference.cpp | 6 +- NativeScript/runtime/Runtime.mm | 2 +- NativeScript/runtime/WeakRef.cpp | 146 ++-------------------- NativeScript/runtime/WeakRef.h | 15 --- TestRunner/app/tests/shared/WeakRef.js | 43 ++++--- 12 files changed, 104 insertions(+), 194 deletions(-) diff --git a/NativeScript/runtime/ArgConverter.mm b/NativeScript/runtime/ArgConverter.mm index 45dcd2ec..8e86e6f4 100644 --- a/NativeScript/runtime/ArgConverter.mm +++ b/NativeScript/runtime/ArgConverter.mm @@ -267,6 +267,7 @@ id adapter = [[DictionaryAdapter alloc] initWithJSObject:value.As() isolate:isolate]; memset(retValue, 0, sizeof(id)); *static_cast(retValue) = adapter; + CFAutorelease(adapter); return; } } else if (type == BinaryTypeEncodingType::StructDeclarationReference) { diff --git a/NativeScript/runtime/DataWrapper.h b/NativeScript/runtime/DataWrapper.h index aec8933d..7d183e31 100644 --- a/NativeScript/runtime/DataWrapper.h +++ b/NativeScript/runtime/DataWrapper.h @@ -259,6 +259,17 @@ class ReferenceWrapper: public BaseDataWrapper { disposeData_(false) { } + ~ReferenceWrapper() { + if(this->value_ != nullptr) { + value_->Reset(); + delete value_; + } + + if (this->data_ != nullptr) { + std::free(this->data_); + } + } + const WrapperType Type() { return WrapperType::Reference; } @@ -291,16 +302,12 @@ class ReferenceWrapper: public BaseDataWrapper { } void SetData(void* data, bool disposeData = false) { - if (this->data_ != nullptr && data != nullptr && this->disposeData_) { + if (this->data_ != nullptr && this->disposeData_) { std::free(this->data_); } this->data_ = data; this->disposeData_ = disposeData; } - - bool ShouldDisposeData() { - return this->disposeData_; - } private: BaseDataWrapper* typeWrapper_; v8::Persistent* value_; diff --git a/NativeScript/runtime/DictionaryAdapter.mm b/NativeScript/runtime/DictionaryAdapter.mm index 12508695..8e1060be 100644 --- a/NativeScript/runtime/DictionaryAdapter.mm +++ b/NativeScript/runtime/DictionaryAdapter.mm @@ -55,6 +55,10 @@ - (id)nextObject { } - (void)dealloc { + self->isolate_ = nullptr; + self->map_ = nil; + self->cache_ = nil; + [super dealloc]; } @@ -138,6 +142,10 @@ - (NSArray*)allObjects { } - (void)dealloc { + self->isolate_ = nullptr; + self->dictionary_ = nil; + self->cache_ = nil; + [super dealloc]; } @@ -147,6 +155,7 @@ @implementation DictionaryAdapter { Isolate* isolate_; std::shared_ptr> object_; std::shared_ptr cache_; + NSEnumerator* enumerator_; } - (instancetype)initWithJSObject:(Local)jsObject isolate:(Isolate*)isolate { @@ -225,10 +234,14 @@ - (NSEnumerator*)keyEnumerator { Local obj = self->object_->Get(self->isolate_); if (obj->IsMap()) { - return [[DictionaryAdapterMapKeysEnumerator alloc] initWithMap:self->object_ isolate:self->isolate_ cache:self->cache_]; + self->enumerator_ = [[DictionaryAdapterMapKeysEnumerator alloc] initWithMap:self->object_ isolate:self->isolate_ cache:self->cache_]; + + return self->enumerator_; } - return [[DictionaryAdapterObjectKeysEnumerator alloc] initWithProperties:self->object_ isolate:self->isolate_ cache:self->cache_]; + self->enumerator_ = [[DictionaryAdapterObjectKeysEnumerator alloc] initWithProperties:self->object_ isolate:self->isolate_ cache:self->cache_]; + + return self->enumerator_; } - (void)dealloc { @@ -240,6 +253,15 @@ - (void)dealloc { delete wrapper; } self->object_->Reset(); + self->isolate_ = nullptr; + self->cache_ = nil; + self->object_ = nil; + + if (self->enumerator_ != nullptr) { + CFAutorelease(self->enumerator_); + self->enumerator_ = nullptr; + } + [super dealloc]; } diff --git a/NativeScript/runtime/Helpers.mm b/NativeScript/runtime/Helpers.mm index f6556264..38382f12 100644 --- a/NativeScript/runtime/Helpers.mm +++ b/NativeScript/runtime/Helpers.mm @@ -252,6 +252,7 @@ return Local(); } + v8::Locker locker(isolate); Local result; if (!obj->GetPrivate(context, privateKey).ToLocal(&result)) { tns::Assert(false, isolate); diff --git a/NativeScript/runtime/Interop.h b/NativeScript/runtime/Interop.h index dd6f6621..b5057163 100644 --- a/NativeScript/runtime/Interop.h +++ b/NativeScript/runtime/Interop.h @@ -126,6 +126,7 @@ class Interop { static id ToObject(v8::Local context, v8::Local arg); static v8::Local GetPrimitiveReturnType(v8::Local context, BinaryTypeEncodingType type, BaseCall* call); private: + static std::pair CreateMethodInternal(const uint8_t initialParamIndex, const uint8_t argsCount, const TypeEncoding* typeEncoding, FFIMethodCallback callback, void* userData); static CFTypeRef CreateBlock(const uint8_t initialParamIndex, const uint8_t argsCount, const TypeEncoding* typeEncoding, FFIMethodCallback callback, void* userData); template static void SetStructValue(v8::Local value, void* destBuffer, ptrdiff_t position); @@ -187,7 +188,8 @@ class Interop { const void* invoke; JSBlockDescriptor* descriptor; void* userData; - + ffi_closure* ffiClosure; + static JSBlockDescriptor kJSBlockDescriptor; } JSBlock; }; diff --git a/NativeScript/runtime/Interop.mm b/NativeScript/runtime/Interop.mm index 4e13348c..3fdc0014 100644 --- a/NativeScript/runtime/Interop.mm +++ b/NativeScript/runtime/Interop.mm @@ -46,43 +46,53 @@ v8::Locker locker(isolate); Isolate::Scope isolate_scope(isolate); HandleScope handle_scope(isolate); + BlockWrapper* blockWrapper = static_cast(tns::GetValue(isolate, callback)); tns::DeleteValue(isolate, callback); wrapper->callback_->Reset(); + delete blockWrapper; } } delete wrapper; + ffi_closure_free(block->ffiClosure); block->~JSBlock(); } } }; -IMP Interop::CreateMethod(const uint8_t initialParamIndex, const uint8_t argsCount, const TypeEncoding* typeEncoding, FFIMethodCallback callback, void* userData) { +std::pair Interop::CreateMethodInternal(const uint8_t initialParamIndex, const uint8_t argsCount, const TypeEncoding* typeEncoding, FFIMethodCallback callback, void* userData) { void* functionPointer; ffi_closure* closure = static_cast(ffi_closure_alloc(sizeof(ffi_closure), &functionPointer)); ParametrizedCall* call = ParametrizedCall::Get(typeEncoding, initialParamIndex, initialParamIndex + argsCount); ffi_status status = ffi_prep_closure_loc(closure, call->Cif, callback, userData, functionPointer); tns::Assert(status == FFI_OK); - return (IMP)functionPointer; + return std::make_pair((IMP)functionPointer, closure); + +} + +IMP Interop::CreateMethod(const uint8_t initialParamIndex, const uint8_t argsCount, const TypeEncoding* typeEncoding, FFIMethodCallback callback, void* userData) { + std::pair result = Interop::CreateMethodInternal(initialParamIndex, argsCount, typeEncoding, callback, userData); + return result.first; } CFTypeRef Interop::CreateBlock(const uint8_t initialParamIndex, const uint8_t argsCount, const TypeEncoding* typeEncoding, FFIMethodCallback callback, void* userData) { JSBlock* blockPointer = reinterpret_cast(malloc(sizeof(JSBlock))); - void* functionPointer = (void*)CreateMethod(initialParamIndex, argsCount, typeEncoding, callback, userData); + + std::pair result = Interop::CreateMethodInternal(initialParamIndex, argsCount, typeEncoding, callback, userData); *blockPointer = { .isa = nullptr, .flags = JSBlock::BLOCK_HAS_COPY_DISPOSE | JSBlock::BLOCK_NEEDS_FREE | (1 /* ref count */ << 1), .reserved = 0, - .invoke = functionPointer, + .invoke = (void*)result.first, .descriptor = &JSBlock::kJSBlockDescriptor, + .userData = userData, + .ffiClosure = result.second, }; - blockPointer->userData = userData; - object_setClass((__bridge id)blockPointer, objc_getClass("__NSMallocBlock__")); - return blockPointer; + return CFAutorelease(blockPointer); } Local Interop::CallFunction(CMethodCall& methodCall) { @@ -295,8 +305,10 @@ Local value = referenceWrapper->Value() != nullptr ? referenceWrapper->Value()->Get(isolate) : Local(); ffi_type* ffiType = FFICall::GetArgumentType(innerType); data = calloc(1, ffiType->size); - referenceWrapper->SetData(data); + + referenceWrapper->SetData(data, true); referenceWrapper->SetEncoding(innerType); + // Initialize the ref/out parameter value before passing it to the function call if (!value.IsEmpty()) { Interop::WriteValue(context, innerType, data, value); @@ -350,7 +362,7 @@ BaseDataWrapper* baseWrapper = tns::GetValue(isolate, arg); if (baseWrapper != nullptr && baseWrapper->Type() == WrapperType::Block) { BlockWrapper* wrapper = static_cast(baseWrapper); - blockPtr = wrapper->Block(); + blockPtr = Block_copy(wrapper->Block()); } else { std::shared_ptr> poCallback = std::make_shared>(isolate, arg); MethodCallbackWrapper* userData = new MethodCallbackWrapper(isolate, poCallback, 1, argsCount, blockTypeEncoding); @@ -512,13 +524,16 @@ Local buffer = arg.As(); NSDataAdapter* adapter = [[NSDataAdapter alloc] initWithJSObject:buffer isolate:isolate]; Interop::SetValue(dest, adapter); + CFAutorelease(adapter); } else if (tns::IsArrayOrArrayLike(isolate, obj)) { Local array = Interop::ToArray(obj); ArrayAdapter* adapter = [[ArrayAdapter alloc] initWithJSObject:array isolate:isolate]; Interop::SetValue(dest, adapter); + CFAutorelease(adapter); } else { DictionaryAdapter* adapter = [[DictionaryAdapter alloc] initWithJSObject:obj isolate:isolate]; Interop::SetValue(dest, adapter); + CFAutorelease(adapter); } } else { tns::Assert(false, isolate); @@ -574,6 +589,7 @@ } else { Local obj = arg.As(); DictionaryAdapter* adapter = [[DictionaryAdapter alloc] initWithJSObject:obj isolate:isolate]; + CFAutorelease(adapter); return adapter; } } diff --git a/NativeScript/runtime/ObjectManager.mm b/NativeScript/runtime/ObjectManager.mm index 654a2b2d..9f7e435b 100644 --- a/NativeScript/runtime/ObjectManager.mm +++ b/NativeScript/runtime/ObjectManager.mm @@ -112,9 +112,6 @@ case WrapperType::Reference: { ReferenceWrapper* referenceWrapper = static_cast(wrapper); if (referenceWrapper->Data() != nullptr) { - if (referenceWrapper->ShouldDisposeData()) { - std::free(referenceWrapper->Data()); - } referenceWrapper->SetData(nullptr); referenceWrapper->SetEncoding(nullptr); } diff --git a/NativeScript/runtime/Reference.cpp b/NativeScript/runtime/Reference.cpp index 5b3ed77a..d6d6c53f 100644 --- a/NativeScript/runtime/Reference.cpp +++ b/NativeScript/runtime/Reference.cpp @@ -58,7 +58,7 @@ Local Reference::GetInteropReferenceCtorFunc(Local contex Local prototype = prototypeValue.As(); Reference::RegisterToStringMethod(context, prototype); - cache->InteropReferenceCtorFunc = std::make_unique>(isolate, ctorFunc); + cache->InteropReferenceCtorFunc = std::make_unique >(isolate, ctorFunc); return ctorFunc; } @@ -80,6 +80,10 @@ void Reference::ReferenceConstructorCallback(const FunctionCallbackInfo& val = new Persistent(isolate, info[1]); } } + + if(val != nullptr) { + val->SetWeak(); + } ReferenceWrapper* wrapper = new ReferenceWrapper(typeWrapper, val); Local thiz = info.This(); diff --git a/NativeScript/runtime/Runtime.mm b/NativeScript/runtime/Runtime.mm index 3a5c354f..44400b70 100644 --- a/NativeScript/runtime/Runtime.mm +++ b/NativeScript/runtime/Runtime.mm @@ -105,7 +105,7 @@ DefineGlobalObject(context); DefineCollectFunction(context); - PromiseProxy::Init(context); +// PromiseProxy::Init(context); Console::Init(context); WeakRef::Init(context); diff --git a/NativeScript/runtime/WeakRef.cpp b/NativeScript/runtime/WeakRef.cpp index 3ca5f4e3..d9601338 100644 --- a/NativeScript/runtime/WeakRef.cpp +++ b/NativeScript/runtime/WeakRef.cpp @@ -1,7 +1,4 @@ #include "WeakRef.h" -#include "NativeScriptException.h" -#include "ArgConverter.h" -#include "Caches.h" #include "Helpers.h" using namespace v8; @@ -11,141 +8,20 @@ namespace tns { void WeakRef::Init(Local context) { Isolate* isolate = context->GetIsolate(); - Local weakRefCtorFunc; - bool success = Function::New(context, ConstructorCallback, Local()).ToLocal(&weakRefCtorFunc); - tns::Assert(success, isolate); - - Local name = tns::ToV8String(isolate, "WeakRef"); - Local global = context->Global(); - success = global->Set(context, name, weakRefCtorFunc).FromMaybe(false); - tns::Assert(success, isolate); -} - -void WeakRef::ConstructorCallback(const FunctionCallbackInfo& info) { - Isolate* isolate = info.GetIsolate(); - tns::Assert(info.IsConstructCall(), isolate); - try { - if (info.Length() < 1 || !info[0]->IsObject()) { - throw NativeScriptException("Argument must be an object."); + std::string source = R"( + global.WeakRef.prototype.get = global.WeakRef.prototype.deref; + global.WeakRef.prototype.clear = () => { + console.warn('WeakRef.clear() is non-standard and has been deprecated. It does nothing and the call can be safely removed.'); } + )"; - Local target = info[0].As(); - Local context = isolate->GetCurrentContext(); - - std::shared_ptr> poValue = ArgConverter::CreateEmptyObject(context); - Local weakRef = poValue->Get(isolate).As(); - poValue->Reset(); - - Persistent* poTarget = new Persistent(isolate, target); - Persistent* poHolder = new Persistent(isolate, weakRef); - CallbackState* callbackState = new CallbackState(poTarget, poHolder); - - poTarget->SetWeak(callbackState, WeakTargetCallback, WeakCallbackType::kFinalizer); - poHolder->SetWeak(callbackState, WeakHolderCallback, WeakCallbackType::kFinalizer); - - bool success = weakRef->Set(context, tns::ToV8String(isolate, "get"), GetGetterFunction(isolate)).FromMaybe(false); - tns::Assert(success, isolate); + Local