From 360f2cfd10910b83cd4891bd5e47c6f394481f54 Mon Sep 17 00:00:00 2001 From: Eduardo Speroni Date: Thu, 8 Oct 2026 01:03:42 -0300 Subject: [PATCH 1/2] perf(runtime): stop copying StructInfo and resolving the Class on every native call StructInfo is now owned once by FFICall's process-wide cache (unique_ptr, non-copyable) and handed out by const reference; struct wrappers hold a pointer into it and Caches::StructInstances is keyed by that pointer instead of the struct name. The per-isolate struct constructor cache moves to a Caches::StateFor slot keyed by StructInfo* and also caches each constructor's prototype, whose property is now read-only so the cache cannot go stale. CacheItem caches the resolved Class and InvokeMethod takes it, so objc_getClass(name) no longer runs per call. Release TestRunner, iPhone 16 Pro simulator: UIScreen.mainScreen.bounds.size.width 2035 -> 1436 ns, cached bounds.size.width 697 -> 352 ns, screen.scale 173 -> 153 ns. Suite 1743/0 incl. the ASan lane. --- NativeScript/runtime/ArgConverter.mm | 17 +- NativeScript/runtime/Caches.cpp | 1 - NativeScript/runtime/Caches.h | 15 +- NativeScript/runtime/DataWrapper.h | 30 ++-- NativeScript/runtime/FFICall.cpp | 78 +++++---- NativeScript/runtime/FFICall.h | 18 +- NativeScript/runtime/Interop.h | 10 +- NativeScript/runtime/Interop.mm | 36 ++-- NativeScript/runtime/InteropTypes.mm | 2 +- NativeScript/runtime/MetadataBuilder.h | 23 ++- NativeScript/runtime/MetadataBuilder.mm | 156 ++++++++++++------ NativeScript/runtime/ObjectManager.mm | 4 +- NativeScript/runtime/Reference.cpp | 4 +- NativeScript/runtime/Runtime.mm | 2 +- .../app/tests/Marshalling/RecordTests.js | 16 ++ 15 files changed, 265 insertions(+), 147 deletions(-) diff --git a/NativeScript/runtime/ArgConverter.mm b/NativeScript/runtime/ArgConverter.mm index d86749901..1bdd325bb 100644 --- a/NativeScript/runtime/ArgConverter.mm +++ b/NativeScript/runtime/ArgConverter.mm @@ -550,7 +550,7 @@ const Meta* meta = ArgConverter::GetMeta(structName); tns::Assert(meta != nullptr && meta->type() == MetaType::Struct, isolate); const StructMeta* structMeta = static_cast(meta); - StructInfo structInfo = FFICall::GetStructInfo(structMeta); + const StructInfo& structInfo = FFICall::GetStructInfo(structMeta); Interop::InitializeStruct(context, retValue, structInfo.Fields(), value); return; } else if (baseWrapper->Type() == WrapperType::Struct) { @@ -847,15 +847,14 @@ } StructWrapper* structWrapper = static_cast(wrapper); - StructInfo structInfo = structWrapper->StructInfo(); + const StructInfo& structInfo = structWrapper->StructInfo(); auto cache = Caches::Get(isolate); - Local structCtorFunc = cache->StructCtorInitializer(context, structInfo); - Local proto; - bool success = - structCtorFunc->Get(context, tns::ToV8String(isolate, "prototype")).ToLocal(&proto); - - if (success && !proto.IsEmpty()) { - success = receiver->SetPrototype(context, proto).FromMaybe(false); + Local proto; + if (cache->StructPrototypeInitializer) { + proto = cache->StructPrototypeInitializer(context, structInfo); + } + if (!proto.IsEmpty()) { + bool success = receiver->SetPrototype(context, proto).FromMaybe(false); tns::Assert(success, isolate); } diff --git a/NativeScript/runtime/Caches.cpp b/NativeScript/runtime/Caches.cpp index cdb39610c..13c86574b 100644 --- a/NativeScript/runtime/Caches.cpp +++ b/NativeScript/runtime/Caches.cpp @@ -24,7 +24,6 @@ Caches::~Caches() { this->CtorFuncTemplates.clear(); this->CtorFuncs.clear(); this->ProtocolCtorFuncs.clear(); - this->StructConstructorFunctions.clear(); this->PrimitiveInteropTypes.clear(); this->CFunctions.clear(); diff --git a/NativeScript/runtime/Caches.h b/NativeScript/runtime/Caches.h index bc4641e45..c7f497a52 100644 --- a/NativeScript/runtime/Caches.h +++ b/NativeScript/runtime/Caches.h @@ -155,9 +155,6 @@ class Caches { robin_hood::unordered_map>> ProtocolCtorFuncs; - robin_hood::unordered_map>> - StructConstructorFunctions; robin_hood::unordered_map>> PrimitiveInteropTypes; @@ -167,7 +164,9 @@ class Caches { robin_hood::unordered_map>> Instances; - robin_hood::unordered_map, + // Root struct objects by (backing buffer, struct type). Child views created + // for nested-struct fields are never entered here. + robin_hood::unordered_map, std::shared_ptr>, pair_hash> StructInstances; @@ -184,8 +183,12 @@ class Caches { v8::Local, const BaseClassMeta*, KnownUnknownClassPair, const std::vector&)> ObjectCtorInitializer; - std::function(v8::Local, StructInfo)> - StructCtorInitializer; + // Resolves the prototype a struct instance of the given type must carry. + // Indirected through std::function so this header stays free of + // MetadataBuilder. + std::function(v8::Local, + const StructInfo&)> + StructPrototypeInitializer; robin_hood::unordered_map> Initializers; diff --git a/NativeScript/runtime/DataWrapper.h b/NativeScript/runtime/DataWrapper.h index c094b27ee..68cd1cc5d 100644 --- a/NativeScript/runtime/DataWrapper.h +++ b/NativeScript/runtime/DataWrapper.h @@ -109,13 +109,13 @@ struct StructField { const TypeEncoding* encoding) : offset_(offset), ffiType_(ffiType), name_(name), encoding_(encoding) {} - ptrdiff_t Offset() { return this->offset_; } + ptrdiff_t Offset() const { return this->offset_; } - ffi_type* FFIType() { return this->ffiType_; } + ffi_type* FFIType() const { return this->ffiType_; } - std::string Name() { return this->name_; } + const std::string& Name() const { return this->name_; } - const TypeEncoding* Encoding() { return this->encoding_; } + const TypeEncoding* Encoding() const { return this->encoding_; } private: ptrdiff_t offset_; @@ -124,17 +124,24 @@ struct StructField { const TypeEncoding* encoding_; }; +// Owned by FFICall's process-wide struct-info cache and immutable once +// built, so every consumer (wrappers, call sites) holds a pointer or +// reference into that cache instead of a copy. Non-copyable to keep it that +// way: a copy would make the wrappers' pointers dangle. struct StructInfo { public: StructInfo(std::string name, ffi_type* ffiType, std::vector fields) - : name_(name), ffiType_(ffiType), fields_(fields) {} + : name_(std::move(name)), ffiType_(ffiType), fields_(std::move(fields)) {} + + StructInfo(const StructInfo&) = delete; + StructInfo& operator=(const StructInfo&) = delete; - std::string Name() const { return this->name_; } + const std::string& Name() const { return this->name_; } ffi_type* FFIType() const { return this->ffiType_; } - std::vector Fields() { return this->fields_; } + const std::vector& Fields() const { return this->fields_; } private: std::string name_; @@ -296,19 +303,20 @@ class PrimitiveDataWrapper : public BaseDataWrapper { class StructTypeWrapper : public BaseDataWrapper { public: - StructTypeWrapper(StructInfo structInfo) : structInfo_(structInfo) {} + StructTypeWrapper(const struct StructInfo& structInfo) + : structInfo_(&structInfo) {} const WrapperType Type() { return WrapperType::StructType; } - const StructInfo StructInfo() { return this->structInfo_; } + const struct StructInfo& StructInfo() const { return *this->structInfo_; } private: - struct StructInfo structInfo_; + const struct StructInfo* structInfo_; }; class StructWrapper : public StructTypeWrapper { public: - StructWrapper(struct StructInfo structInfo, void* data, + StructWrapper(const struct StructInfo& structInfo, void* data, std::shared_ptr> parent) : StructTypeWrapper(structInfo), data_(data), diff --git a/NativeScript/runtime/FFICall.cpp b/NativeScript/runtime/FFICall.cpp index 29f5951c0..b9199a768 100644 --- a/NativeScript/runtime/FFICall.cpp +++ b/NativeScript/runtime/FFICall.cpp @@ -104,7 +104,8 @@ ffi_type* FFICall::GetArgumentType(const TypeEncoding* typeEncoding, bool isStru tns::Assert(meta->type() == MetaType::Struct); const StructMeta* structMeta = static_cast(meta); - StructInfo structInfo = FFICall::GetStructInfo(structMeta, structName); + const StructInfo& structInfo = + FFICall::GetStructInfo(structMeta, structName); return structInfo.FFIType(); } case BinaryTypeEncodingType::ConstantArrayEncoding: { @@ -127,7 +128,8 @@ ffi_type* FFICall::GetArgumentType(const TypeEncoding* typeEncoding, bool isStru size_t count = typeEncoding->details.anonymousRecord.fieldsCount; const TypeEncoding* fieldEncoding = typeEncoding->details.anonymousRecord.getFieldsEncodings(); const String* fieldNames = typeEncoding->details.anonymousRecord.getFieldNames(); - StructInfo structInfo = FFICall::GetStructInfo(count, fieldEncoding, fieldNames); + const StructInfo& structInfo = + FFICall::GetStructInfo(count, fieldEncoding, fieldNames); return structInfo.FFIType(); } default: { @@ -172,42 +174,47 @@ void FFICall::DisposeFFIType(ffi_type* type, const TypeEncoding* typeEncoding) { } -StructInfo FFICall::GetStructInfo(const StructMeta* structMeta, std::string structName) { - size_t fieldsCount = structMeta->fieldsCount(); - const TypeEncoding* fieldEncoding = structMeta->fieldsEncodings()->first(); - const String* fieldNames = structMeta->fieldNames().first(); - if (structName.empty()) { - structName = structMeta->name(); - } - StructInfo structInfo = FFICall::GetStructInfo(fieldsCount, fieldEncoding, fieldNames, structName); - return structInfo; +const StructInfo& FFICall::GetStructInfo(const StructMeta* structMeta, + std::string structName) { + size_t fieldsCount = structMeta->fieldsCount(); + const TypeEncoding* fieldEncoding = structMeta->fieldsEncodings()->first(); + const String* fieldNames = structMeta->fieldNames().first(); + if (structName.empty()) { + structName = structMeta->name(); + } + return FFICall::GetStructInfo(fieldsCount, fieldEncoding, fieldNames, + structName); } -StructInfo FFICall::GetStructInfo(size_t fieldsCount, const TypeEncoding* fieldEncoding, const String* fieldNames, std::string structName) { - if (structName.empty()) { - const TypeEncoding* temp = fieldEncoding; - std::stringstream ss; - for (int i = 0; i < fieldsCount; i++) { - std::string fieldName = fieldNames[i].valuePtr(); - ss << fieldName << "_" << temp->type; - temp = temp->next(); - } - structName = ss.str(); +const StructInfo& FFICall::GetStructInfo(size_t fieldsCount, + const TypeEncoding* fieldEncoding, + const String* fieldNames, + std::string structName) { + if (structName.empty()) { + const TypeEncoding* temp = fieldEncoding; + std::stringstream ss; + for (int i = 0; i < fieldsCount; i++) { + std::string fieldName = fieldNames[i].valuePtr(); + ss << fieldName << "_" << temp->type; + temp = temp->next(); } + structName = ss.str(); + } - { - SpinLock lock(structInfosCacheMutex_); - auto it = structInfosCache_.find(structName); - if (it != structInfosCache_.end()) { - return it->second; - } + { + SpinLock lock(structInfosCacheMutex_); + auto it = structInfosCache_.find(structName); + if (it != structInfosCache_.end()) { + return *it->second; } + } - std::vector fields; - fields.reserve(fieldsCount); - ffi_type* ffiType = new ffi_type({ .size = 0, .alignment = 0, .type = FFI_TYPE_STRUCT }); + std::vector fields; + fields.reserve(fieldsCount); + ffi_type* ffiType = + new ffi_type({.size = 0, .alignment = 0, .type = FFI_TYPE_STRUCT}); - ffiType->elements = new ffi_type*[fieldsCount + 1]; + ffiType->elements = new ffi_type*[fieldsCount + 1]; #if defined(__x86_64__) bool hasNestedStruct = false; @@ -272,14 +279,16 @@ StructInfo FFICall::GetStructInfo(size_t fieldsCount, const TypeEncoding* fieldE } #endif - StructInfo structInfo(structName, ffiType, fields); + auto structInfo = + std::make_unique(structName, ffiType, std::move(fields)); // The lock cannot be held while building: nested struct fields recurse // through GetArgumentType back into GetStructInfo. Concurrent builders are // therefore possible; the first emplace wins and everyone returns the // cached entry (the loser's ffi_type allocations are abandoned). SpinLock lock(structInfosCacheMutex_); - return structInfosCache_.emplace(structName, structInfo).first->second; + return *structInfosCache_.emplace(structName, std::move(structInfo)) + .first->second; } ParametrizedCall* ParametrizedCall::Get(const TypeEncoding* typeEncoding, const int initialParameterIndex, const int argsCount) { @@ -319,6 +328,7 @@ ParametrizedCall* ParametrizedCall::Get(const TypeEncoding* typeEncoding, const robin_hood::unordered_map ParametrizedCall::callsCache_; SpinMutex ParametrizedCall::callsCacheMutex_; -robin_hood::unordered_map FFICall::structInfosCache_; +robin_hood::unordered_map> + FFICall::structInfosCache_; SpinMutex FFICall::structInfosCacheMutex_; } diff --git a/NativeScript/runtime/FFICall.h b/NativeScript/runtime/FFICall.h index c53ceef11..39139d41c 100644 --- a/NativeScript/runtime/FFICall.h +++ b/NativeScript/runtime/FFICall.h @@ -4,6 +4,7 @@ #include #include +#include #include "DataWrapper.h" #include "Metadata.h" @@ -104,12 +105,14 @@ class FFICall : public BaseCall { static ffi_type* GetArgumentType(const TypeEncoding* typeEncoding, bool isStructMember = false); static void DisposeFFIType(ffi_type* type, const TypeEncoding* typeEncoding); - static StructInfo GetStructInfo(const StructMeta* structMeta, - std::string structName = ""); - static StructInfo GetStructInfo(size_t fieldsCount, - const TypeEncoding* fieldEncoding, - const String* fieldNames, - std::string structName = ""); + // The returned reference points into a process-wide cache that is never + // pruned, so it stays valid for the lifetime of the process. + static const StructInfo& GetStructInfo(const StructMeta* structMeta, + std::string structName = ""); + static const StructInfo& GetStructInfo(size_t fieldsCount, + const TypeEncoding* fieldEncoding, + const String* fieldNames, + std::string structName = ""); inline void* ArgumentBuffer(unsigned index) { return this->argsArray_[index]; @@ -118,7 +121,8 @@ class FFICall : public BaseCall { inline void** ArgsArray() { return this->argsArray_; } private: - static robin_hood::unordered_map structInfosCache_; + static robin_hood::unordered_map> + structInfosCache_; static SpinMutex structInfosCacheMutex_; void** argsArray_; bool useDynamicBuffer_; diff --git a/NativeScript/runtime/Interop.h b/NativeScript/runtime/Interop.h index 535f29593..85a556f15 100644 --- a/NativeScript/runtime/Interop.h +++ b/NativeScript/runtime/Interop.h @@ -97,10 +97,11 @@ class Interop { bool isStructMember = false, bool ownsReturnedObject = false, bool returnsUnmanaged = false, bool isInitializer = false); static void SetStructPropertyValue(v8::Local context, - StructWrapper* wrapper, StructField field, + StructWrapper* wrapper, + const StructField& field, v8::Local value); static void InitializeStruct(v8::Local context, void* destBuffer, - std::vector fields, + const std::vector& fields, v8::Local inititalizer); static void WriteTypeValue(v8::Local context, BaseDataWrapper* typeWrapper, void* dest, @@ -129,7 +130,7 @@ class Interop { static void SetStructValue(v8::Local value, void* destBuffer, ptrdiff_t position); static void InitializeStruct(v8::Local context, void* destBuffer, - std::vector fields, + const std::vector& fields, v8::Local inititalizer, ptrdiff_t& position); static void RegisterInteropType(v8::Local context, @@ -160,7 +161,8 @@ class Interop { const char* clazz); static v8::Local ToArray(v8::Local object); static v8::Local StructToValue( - v8::Local context, void* result, StructInfo structInfo, + v8::Local context, void* result, + const StructInfo& structInfo, std::shared_ptr> parentStruct); static const TypeEncoding* CreateEncoding(BinaryTypeEncodingType type); static v8::Local HandleOf(v8::Local context, diff --git a/NativeScript/runtime/Interop.mm b/NativeScript/runtime/Interop.mm index 304535be6..a258b136a 100644 --- a/NativeScript/runtime/Interop.mm +++ b/NativeScript/runtime/Interop.mm @@ -226,7 +226,7 @@ inline bool isBool() { if (typeWrapper->Type() == WrapperType::StructType) { if (isEmptyOrUndefined) { StructTypeWrapper* structTypeWrapper = static_cast(typeWrapper); - StructInfo structInfo = structTypeWrapper->StructInfo(); + const StructInfo& structInfo = structTypeWrapper->StructInfo(); memset(dest, 0, structInfo.FFIType()->size); success = true; @@ -243,7 +243,7 @@ inline bool isBool() { } else { // Create the structure using the struct initializer syntax StructTypeWrapper* structTypeWrapper = static_cast(typeWrapper); - StructInfo structInfo = structTypeWrapper->StructInfo(); + const StructInfo& structInfo = structTypeWrapper->StructInfo(); Interop::InitializeStruct(context, dest, structInfo.Fields(), arg.As()); success = true; } @@ -432,7 +432,7 @@ inline bool isBool() { ArgConverter::GetMeta(innerType->details.declarationReference.name.valuePtr()); tns::Assert(meta != nullptr && meta->type() == MetaType::Struct, isolate); const StructMeta* structMeta = static_cast(meta); - StructInfo structInfo = FFICall::GetStructInfo(structMeta); + const StructInfo& structInfo = FFICall::GetStructInfo(structMeta); // TODO: How to free this? // this is used when you have js obj and wants to pass the data as a struct ponter // (MyStruct*) we create a new MyStruct with a snapshot of the jsObject and pass that in but @@ -582,7 +582,7 @@ inline bool isBool() { const Meta* meta = ArgConverter::GetMeta(structName); tns::Assert(meta != nullptr && meta->type() == MetaType::Struct, isolate); const StructMeta* structMeta = static_cast(meta); - StructInfo structInfo = FFICall::GetStructInfo(structMeta); + const StructInfo& structInfo = FFICall::GetStructInfo(structMeta); Interop::InitializeStruct(context, dest, structInfo.Fields(), obj); } } else if (argHelper.isObject() && @@ -594,7 +594,7 @@ inline bool isBool() { const TypeEncoding* fieldEncoding = typeEncoding->details.anonymousRecord.getFieldsEncodings(); const String* fieldNames = typeEncoding->details.anonymousRecord.getFieldNames(); - StructInfo structInfo = FFICall::GetStructInfo(fieldsCount, fieldEncoding, fieldNames); + const StructInfo& structInfo = FFICall::GetStructInfo(fieldsCount, fieldEncoding, fieldNames); StructWrapper* structWrapper = static_cast(wrapper); void* data = structWrapper->Data(); @@ -607,7 +607,7 @@ inline bool isBool() { const TypeEncoding* fieldEncoding = typeEncoding->details.anonymousRecord.getFieldsEncodings(); const String* fieldNames = typeEncoding->details.anonymousRecord.getFieldNames(); - StructInfo structInfo = FFICall::GetStructInfo(fieldsCount, fieldEncoding, fieldNames); + const StructInfo& structInfo = FFICall::GetStructInfo(fieldsCount, fieldEncoding, fieldNames); Interop::InitializeStruct(context, dest, structInfo.Fields(), obj); } } else if (arg->IsFunction() && typeEncoding->type == BinaryTypeEncodingType::ProtocolEncoding) { @@ -785,7 +785,8 @@ inline bool isBool() { return nil; } -Local Interop::StructToValue(Local context, void* result, StructInfo structInfo, +Local Interop::StructToValue(Local context, void* result, + const StructInfo& structInfo, std::shared_ptr> parentStruct) { Isolate* isolate = v8::Isolate::GetCurrent(); StructWrapper* wrapper = nullptr; @@ -806,7 +807,7 @@ inline bool isBool() { } std::shared_ptr cache = Caches::Get(isolate); - std::pair key = std::make_pair(wrapper->Data(), structInfo.Name()); + std::pair key = std::make_pair(wrapper->Data(), &structInfo); auto it = cache->StructInstances.find(key); if (it != cache->StructInstances.end()) { return it->second->Get(isolate); @@ -821,17 +822,17 @@ inline bool isBool() { } void Interop::InitializeStruct(Local context, void* destBuffer, - std::vector fields, Local inititalizer) { + const std::vector& fields, Local inititalizer) { ptrdiff_t position = 0; Interop::InitializeStruct(context, destBuffer, fields, inititalizer, position); } void Interop::InitializeStruct(Local context, void* destBuffer, - std::vector fields, Local inititalizer, + const std::vector& fields, Local inititalizer, ptrdiff_t& position) { Isolate* isolate = v8::Isolate::GetCurrent(); for (auto it = fields.begin(); it != fields.end(); it++) { - StructField field = *it; + const StructField& field = *it; Local value; if (!inititalizer.IsEmpty() && !inititalizer->IsNullOrUndefined() && inititalizer->IsObject()) { @@ -848,7 +849,7 @@ inline bool isBool() { ArgConverter::GetMeta(field.Encoding()->details.declarationReference.name.valuePtr()); tns::Assert(meta != nullptr && meta->type() == MetaType::Struct, isolate); const StructMeta* structMeta = static_cast(meta); - StructInfo nestedStructInfo = FFICall::GetStructInfo(structMeta); + const StructInfo& nestedStructInfo = FFICall::GetStructInfo(structMeta); Interop::InitializeStruct(context, destBuffer, nestedStructInfo.Fields(), value, position); position += nestedStructInfo.FFIType()->size; } else if (type == BinaryTypeEncodingType::AnonymousStructEncoding) { @@ -856,7 +857,8 @@ inline bool isBool() { const TypeEncoding* fieldEncoding = field.Encoding()->details.anonymousRecord.getFieldsEncodings(); const String* fieldNames = field.Encoding()->details.anonymousRecord.getFieldNames(); - StructInfo nestedStructInfo = FFICall::GetStructInfo(fieldsCount, fieldEncoding, fieldNames); + const StructInfo& nestedStructInfo = + FFICall::GetStructInfo(fieldsCount, fieldEncoding, fieldNames); ptrdiff_t offset = position + field.Offset(); uint8_t* dst = (uint8_t*)destBuffer + offset; Interop::InitializeStruct(context, dst, nestedStructInfo.Fields(), value, position); @@ -941,7 +943,7 @@ inline bool isBool() { if (typeWrapper->Type() == WrapperType::StructType) { StructTypeWrapper* structTypeWrapper = static_cast(typeWrapper); - StructInfo structInfo = structTypeWrapper->StructInfo(); + const StructInfo& structInfo = structTypeWrapper->StructInfo(); void* result = call->ResultBuffer(); Local value = Interop::StructToValue(context, result, structInfo, parentStruct); @@ -986,7 +988,7 @@ inline bool isBool() { void* result = call->ResultBuffer(); const StructMeta* structMeta = static_cast(meta); - StructInfo structInfo = FFICall::GetStructInfo(structMeta, structName); + const StructInfo& structInfo = FFICall::GetStructInfo(structMeta, structName); Local value = Interop::StructToValue(context, result, structInfo, parentStruct); return value; } @@ -995,7 +997,7 @@ inline bool isBool() { size_t fieldsCount = typeEncoding->details.anonymousRecord.fieldsCount; const TypeEncoding* fieldEncoding = typeEncoding->details.anonymousRecord.getFieldsEncodings(); const String* fieldNames = typeEncoding->details.anonymousRecord.getFieldNames(); - StructInfo structInfo = FFICall::GetStructInfo(fieldsCount, fieldEncoding, fieldNames); + const StructInfo& structInfo = FFICall::GetStructInfo(fieldsCount, fieldEncoding, fieldNames); void* result = call->ResultBuffer(); Local value = Interop::StructToValue(context, result, structInfo, parentStruct); return value; @@ -1461,7 +1463,7 @@ inline bool isBool() { } void Interop::SetStructPropertyValue(Local context, StructWrapper* wrapper, - StructField field, Local value) { + const StructField& field, Local value) { if (value.IsEmpty()) { return; } diff --git a/NativeScript/runtime/InteropTypes.mm b/NativeScript/runtime/InteropTypes.mm index 5846bd526..5f93e39b5 100644 --- a/NativeScript/runtime/InteropTypes.mm +++ b/NativeScript/runtime/InteropTypes.mm @@ -578,7 +578,7 @@ new PrimitiveDataWrapper(sizeof(void*), } case WrapperType::StructType: { StructTypeWrapper* sw = static_cast(wrapper); - StructInfo structInfo = sw->StructInfo(); + const StructInfo& structInfo = sw->StructInfo(); size = structInfo.FFIType()->size; break; } diff --git a/NativeScript/runtime/MetadataBuilder.h b/NativeScript/runtime/MetadataBuilder.h index c997d2420..e8b3ae82c 100644 --- a/NativeScript/runtime/MetadataBuilder.h +++ b/NativeScript/runtime/MetadataBuilder.h @@ -1,6 +1,8 @@ #ifndef MetadataBuilder_h #define MetadataBuilder_h +#include + #include "ClassBuilder.h" #include "Common.h" #include "DataWrapper.h" @@ -21,7 +23,9 @@ class MetadataBuilder { const std::vector& additionalProtocols = std::vector()); static v8::Local GetOrCreateStructCtorFunction( - v8::Local context, StructInfo structInfo); + v8::Local context, const StructInfo& structInfo); + static v8::Local GetOrCreateStructPrototype( + v8::Local context, const StructInfo& structInfo); static v8::Intercepted StructPropertyGetterCallback( v8::Local property, const v8::PropertyCallbackInfo& info); @@ -63,13 +67,12 @@ class MetadataBuilder { const v8::FunctionCallbackInfo& info); static std::pair GetStructData( v8::Local context, v8::Local initializer, - StructInfo structInfo); + const StructInfo& structInfo); static v8::Local InvokeMethod(v8::Local context, const MethodMeta* meta, v8::Local receiver, - V8Args& args, - const std::string& containingClass, + V8Args& args, Class klass, bool isMethodCallback); static void RegisterAllocMethod( v8::Isolate* isolate, v8::Local ctorFuncTemplate, @@ -145,6 +148,18 @@ class MetadataBuilder { const T* meta_; const std::string className_; void* userData_; + + // Metadata can describe classes that are not loaded yet, so a nil + // lookup is retried on the next call instead of being cached. + Class ResolveClass() { + if (this->klass_ == nil && !this->className_.empty()) { + this->klass_ = objc_getClass(this->className_.c_str()); + } + return this->klass_; + } + + private: + Class klass_ = nil; }; }; diff --git a/NativeScript/runtime/MetadataBuilder.mm b/NativeScript/runtime/MetadataBuilder.mm index 604e4933e..966eebfd9 100644 --- a/NativeScript/runtime/MetadataBuilder.mm +++ b/NativeScript/runtime/MetadataBuilder.mm @@ -19,6 +19,21 @@ namespace tns { +namespace { +struct StructTypeEntry { + std::unique_ptr> ctor; + std::unique_ptr> prototype; +}; + +// Per-isolate struct constructor functions and their prototypes, keyed by the +// process-wide StructInfo (see FFICall.h), whose address is stable for the +// lifetime of the process. The prototype is cached because the constructor's +// `prototype` property is made read-only, so it cannot be swapped from JS. +struct StructTypeState { + robin_hood::unordered_map types; +}; +} // namespace + void MetadataBuilder::RegisterConstantsOnGlobalObject(Isolate* isolate, Local globalTemplate, bool isWorkerThread) { @@ -160,7 +175,7 @@ NamedPropertyHandlerConfiguration config(MetadataBuilder::GlobalPropertyGetter, info.GetReturnValue().Set(result); } else if (meta->type() == MetaType::Struct) { const StructMeta* structMeta = static_cast(meta); - StructInfo structInfo = FFICall::GetStructInfo(structMeta); + const StructInfo& structInfo = FFICall::GetStructInfo(structMeta); Local structCtorFunc = MetadataBuilder::GetOrCreateStructCtorFunction(context, structInfo); info.GetReturnValue().Set(structCtorFunc); @@ -169,12 +184,14 @@ NamedPropertyHandlerConfiguration config(MetadataBuilder::GlobalPropertyGetter, } Local MetadataBuilder::GetOrCreateStructCtorFunction(Local context, - StructInfo structInfo) { + const StructInfo& structInfo) { Isolate* isolate = v8::Isolate::GetCurrent(); - auto cache = Caches::Get(isolate); - auto it = cache->StructConstructorFunctions.find(structInfo.Name()); - if (it != cache->StructConstructorFunctions.end()) { - return it->second->Get(isolate); + StructTypeState* state = Caches::StateFor(isolate); + if (state != nullptr) { + auto it = state->types.find(&structInfo); + if (it != state->types.end()) { + return it->second.ctor->Get(isolate); + } } StructTypeWrapper* wrapper = new StructTypeWrapper(structInfo); @@ -194,12 +211,58 @@ NamedPropertyHandlerConfiguration config(MetadataBuilder::GlobalPropertyGetter, structCtorFunc->Set(context, tns::ToV8String(isolate, "equals"), equalsFunc).FromMaybe(false); tns::Assert(success, isolate); - cache->StructConstructorFunctions.emplace( - structInfo.Name(), std::make_unique>(isolate, structCtorFunc)); + // Like interface constructors (built from templates with a read-only + // prototype), a struct constructor's prototype can be extended but not + // replaced. + Local prototypeKey = tns::ToV8String(isolate, "prototype"); + Local proto; + success = structCtorFunc->Get(context, prototypeKey).ToLocal(&proto); + tns::Assert(success && proto->IsObject(), isolate); + const PropertyAttribute readOnlyFlags = static_cast( + PropertyAttribute::DontEnum | PropertyAttribute::DontDelete | PropertyAttribute::ReadOnly); + success = structCtorFunc->DefineOwnProperty(context, prototypeKey, proto, readOnlyFlags) + .FromMaybe(false); + tns::Assert(success, isolate); + + if (state != nullptr) { + StructTypeEntry entry; + entry.ctor = std::make_unique>(isolate, structCtorFunc); + entry.prototype = std::make_unique>(isolate, proto.As()); + state->types.emplace(&structInfo, std::move(entry)); + } return structCtorFunc; } +Local MetadataBuilder::GetOrCreateStructPrototype(Local context, + const StructInfo& structInfo) { + Isolate* isolate = v8::Isolate::GetCurrent(); + StructTypeState* state = Caches::StateFor(isolate); + if (state != nullptr) { + auto it = state->types.find(&structInfo); + if (it == state->types.end()) { + MetadataBuilder::GetOrCreateStructCtorFunction(context, structInfo); + it = state->types.find(&structInfo); + } + if (it != state->types.end()) { + return it->second.prototype->Get(isolate); + } + } + + // No per-isolate state (teardown has begun): resolve without caching. Each + // call here builds a fresh constructor, so instances created in this window + // do not share a prototype with earlier ones; only finalizer-driven + // conversions can reach it. + Local structCtorFunc = + MetadataBuilder::GetOrCreateStructCtorFunction(context, structInfo); + Local proto; + if (!structCtorFunc->Get(context, tns::ToV8String(isolate, "prototype")).ToLocal(&proto) || + !proto->IsObject()) { + return Local(); + } + return proto.As(); +} + void MetadataBuilder::StructConstructorCallback(const FunctionCallbackInfo& info) { Isolate* isolate = info.GetIsolate(); Local context = isolate->GetCurrentContext(); @@ -207,7 +270,7 @@ NamedPropertyHandlerConfiguration config(MetadataBuilder::GlobalPropertyGetter, StructTypeWrapper* typeWrapper = static_cast( info.Data().As()->Value(v8::kExternalPointerTypeTagDefault)); - StructInfo structInfo = typeWrapper->StructInfo(); + const StructInfo& structInfo = typeWrapper->StructInfo(); void* dest = nullptr; @@ -236,7 +299,7 @@ throw NativeScriptException( std::shared_ptr cache = Caches::Get(isolate); std::shared_ptr> poResult = ObjectManager::Register(context, result); - std::pair key = std::make_pair(wrapper->Data(), structInfo.Name()); + std::pair key = std::make_pair(wrapper->Data(), &structInfo); cache->StructInstances.emplace(key, poResult); tns::DeleteWrapperIfUnused(isolate, result, wrapper); @@ -267,7 +330,7 @@ throw NativeScriptException( } StructTypeWrapper* structTypeWrapper = static_cast(wrapper); - StructInfo structInfo = structTypeWrapper->StructInfo(); + const StructInfo& structInfo = structTypeWrapper->StructInfo(); std::pair pair1 = MetadataBuilder::GetStructData(context, arg1, structInfo); std::pair pair2 = MetadataBuilder::GetStructData(context, arg2, structInfo); @@ -298,7 +361,7 @@ throw NativeScriptException( std::pair MetadataBuilder::GetStructData(Local context, Local initializer, - StructInfo structInfo) { + const StructInfo& structInfo) { ffi_type* ffiType = nullptr; void* data = nullptr; @@ -767,26 +830,24 @@ NamedPropertyHandlerConfiguration config(nullptr, MetadataBuilder::SwizzledInsta bool instanceMethod = info.This()->InternalFieldCount() > 0; V8FunctionCallbackArgs args(info); - // Only the class-side call rewrites the name, so the common path reads - // item->className_ in place rather than copying it on every invocation. - const std::string* className = &item->className_; - std::string classWrapperName; + // A class-side call through a subclass constructor targets that subclass, + // not the class the metadata item was registered for. + Class klass = item->ResolveClass(); Local thiz = info.This(); if (thiz->IsFunction()) { - if (BaseDataWrapper* wrapper = tns::GetValue(isolate, thiz)) { + BaseDataWrapper* wrapper = tns::GetValue(isolate, thiz); + if (wrapper != nullptr && wrapper->Type() == WrapperType::ObjCClass) { ObjCClassWrapper* classWrapper = static_cast(wrapper); - classWrapperName = class_getName(classWrapper->Klass()); - className = &classWrapperName; + klass = classWrapper->Klass(); } } Local context = isolate->GetCurrentContext(); Local result = instanceMethod - ? MetadataBuilder::InvokeMethod(context, item->meta_, info.This(), args, *className, true) - : MetadataBuilder::InvokeMethod(context, item->meta_, Local(), args, *className, - true); + ? MetadataBuilder::InvokeMethod(context, item->meta_, info.This(), args, klass, true) + : MetadataBuilder::InvokeMethod(context, item->meta_, Local(), args, klass, true); if (!result.IsEmpty()) { info.GetReturnValue().Set(result); @@ -812,7 +873,7 @@ NamedPropertyHandlerConfiguration config(nullptr, MetadataBuilder::SwizzledInsta V8EmptyValueArgs args; Local context = isolate->GetCurrentContext(); Local result = MetadataBuilder::InvokeMethod(context, item->meta_->getter(), receiver, - args, item->className_, true); + args, item->ResolveClass(), true); if (!result.IsEmpty()) { info.GetReturnValue().Set(result); } @@ -833,8 +894,8 @@ NamedPropertyHandlerConfiguration config(nullptr, MetadataBuilder::SwizzledInsta Local value = info[0]; V8SimpleValueArgs args(value); Local context = isolate->GetCurrentContext(); - MetadataBuilder::InvokeMethod(context, item->meta_->setter(), receiver, args, item->className_, - true); + MetadataBuilder::InvokeMethod(context, item->meta_->setter(), receiver, args, + item->ResolveClass(), true); } void MetadataBuilder::PropertyNameGetterCallback(const FunctionCallbackInfo& info) { @@ -850,7 +911,7 @@ NamedPropertyHandlerConfiguration config(nullptr, MetadataBuilder::SwizzledInsta V8EmptyValueArgs args; Local context = isolate->GetCurrentContext(); Local result = MetadataBuilder::InvokeMethod( - context, item->meta_->getter(), Local(), args, item->className_, false); + context, item->meta_->getter(), Local(), args, item->ResolveClass(), false); if (!result.IsEmpty()) { info.GetReturnValue().Set(result); } @@ -871,7 +932,7 @@ NamedPropertyHandlerConfiguration config(nullptr, MetadataBuilder::SwizzledInsta V8SimpleValueArgs args(value); Local context = isolate->GetCurrentContext(); MetadataBuilder::InvokeMethod(context, item->meta_->setter(), Local(), args, - item->className_, false); + item->ResolveClass(), false); } Intercepted MetadataBuilder::StructPropertyGetterCallback(Local property, @@ -894,25 +955,26 @@ NamedPropertyHandlerConfiguration config(nullptr, MetadataBuilder::SwizzledInsta tns::Assert(baseWrapper->Type() == WrapperType::Struct, isolate); StructWrapper* wrapper = static_cast(baseWrapper); - StructInfo structInfo = wrapper->StructInfo(); + const StructInfo& structInfo = wrapper->StructInfo(); std::shared_ptr cache = Caches::Get(isolate); - std::pair key = std::make_pair(wrapper->Data(), structInfo.Name()); + std::pair key = std::make_pair(wrapper->Data(), &structInfo); std::shared_ptr> parentStruct = nullptr; auto x = cache->StructInstances.find(key); if (x != cache->StructInstances.end()) { parentStruct = x->second; } - std::vector fields = structInfo.Fields(); - auto it = std::find_if(fields.begin(), fields.end(), - [&propertyName](StructField& f) { return f.Name() == propertyName; }); + const std::vector& fields = structInfo.Fields(); + auto it = std::find_if(fields.begin(), fields.end(), [&propertyName](const StructField& f) { + return f.Name() == propertyName; + }); if (it == fields.end()) { info.GetReturnValue().SetUndefined(); return Intercepted::kYes; } - StructField field = *it; + const StructField& field = *it; const TypeEncoding* fieldEncoding = field.Encoding(); ptrdiff_t offset = field.Offset(); void* buffer = wrapper->Data(); @@ -945,17 +1007,17 @@ NamedPropertyHandlerConfiguration config(nullptr, MetadataBuilder::SwizzledInsta tns::Assert(baseWrapper->Type() == WrapperType::Struct, isolate); StructWrapper* wrapper = static_cast(baseWrapper); - StructInfo structInfo = wrapper->StructInfo(); - std::vector fields = structInfo.Fields(); + const StructInfo& structInfo = wrapper->StructInfo(); + const std::vector& fields = structInfo.Fields(); - auto it = std::find_if(fields.begin(), fields.end(), - [&propertyName](StructField& f) { return f.Name() == propertyName; }); + auto it = std::find_if(fields.begin(), fields.end(), [&propertyName](const StructField& f) { + return f.Name() == propertyName; + }); if (it == fields.end()) { return Intercepted::kNo; } - StructField field = *it; - Interop::SetStructPropertyValue(context, wrapper, field, value); + Interop::SetStructPropertyValue(context, wrapper, *it, value); return Intercepted::kYes; } @@ -973,10 +1035,8 @@ NamedPropertyHandlerConfiguration config(nullptr, MetadataBuilder::SwizzledInsta } Local MetadataBuilder::InvokeMethod(Local context, const MethodMeta* meta, - Local receiver, V8Args& args, - const std::string& containingClass, + Local receiver, V8Args& args, Class klass, bool isMethodCallback) { - Class klass = objc_getClass(containingClass.c_str()); // TODO: Find out if the isMethodCallback property can be determined based on a // UITableViewController.prototype.viewDidLoad.call(this) or super.viewDidLoad() call @@ -985,12 +1045,12 @@ NamedPropertyHandlerConfiguration config(nullptr, MetadataBuilder::SwizzledInsta bool logRuntimeDetail = value ? [value boolValue] : false; if (logRuntimeDetail) { // NOTE: stringWithFormat is slow, perhaps use different c string concatenation? - NSString* message = - [NSString stringWithFormat:@"MetadataBuilder::InvokeMethod: class {%s}, selector {%s}, " - @"isInitializer {%s}, type {%s}, lib {%s}", - containingClass.c_str(), meta->selectorAsString(), - meta->isInitializer() ? "true" : "false", meta->typeName(), - meta->topLevelModule()->getName()]; + NSString* message = [NSString + stringWithFormat:@"MetadataBuilder::InvokeMethod: class {%s}, selector {%s}, " + @"isInitializer {%s}, type {%s}, lib {%s}", + klass ? class_getName(klass) : "", meta->selectorAsString(), + meta->isInitializer() ? "true" : "false", meta->typeName(), + meta->topLevelModule()->getName()]; Log(@"%@", message); } #endif diff --git a/NativeScript/runtime/ObjectManager.mm b/NativeScript/runtime/ObjectManager.mm index 013cf18f8..5d84c1e22 100644 --- a/NativeScript/runtime/ObjectManager.mm +++ b/NativeScript/runtime/ObjectManager.mm @@ -216,8 +216,8 @@ void DisposeHandle(v8::Isolate* isolate, } } else { if (structWrapper->ChildCount() == 0) { - std::pair key = - std::make_pair(data, structWrapper->StructInfo().Name()); + std::pair key = + std::make_pair(data, &structWrapper->StructInfo()); cache->StructInstances.erase(key); std::free(data); } else { diff --git a/NativeScript/runtime/Reference.cpp b/NativeScript/runtime/Reference.cpp index 316326c59..5828663b0 100644 --- a/NativeScript/runtime/Reference.cpp +++ b/NativeScript/runtime/Reference.cpp @@ -305,7 +305,7 @@ void* Reference::GetWrappedPointer(Local context, StructTypeWrapper* structTypeWrapper = static_cast(refWrapper->TypeWrapper()); - StructInfo structInfo = structTypeWrapper->StructInfo(); + const StructInfo& structInfo = structTypeWrapper->StructInfo(); void* data = malloc(structInfo.FFIType()->size); Interop::InitializeStruct(context, data, structInfo.Fields(), value); refWrapper->SetData(data, true); @@ -429,7 +429,7 @@ Reference::DataPair Reference::GetDataPair(Local obj) { case WrapperType::StructType: { StructTypeWrapper* structTypeWrapper = static_cast(refWrapper->TypeWrapper()); - StructInfo structInfo = structTypeWrapper->StructInfo(); + const StructInfo& structInfo = structTypeWrapper->StructInfo(); size = structInfo.FFIType()->size; break; diff --git a/NativeScript/runtime/Runtime.mm b/NativeScript/runtime/Runtime.mm index bcd379788..216fb7104 100644 --- a/NativeScript/runtime/Runtime.mm +++ b/NativeScript/runtime/Runtime.mm @@ -386,7 +386,7 @@ void DisposeIsolateWhenPossible(Isolate* isolate) { Caches::Init(isolate, nextIsolateId.fetch_add(1, std::memory_order_relaxed)); cache->isWorker = isWorker; cache->ObjectCtorInitializer = MetadataBuilder::GetOrCreateConstructorFunctionTemplate; - cache->StructCtorInitializer = MetadataBuilder::GetOrCreateStructCtorFunction; + cache->StructPrototypeInitializer = MetadataBuilder::GetOrCreateStructPrototype; Isolate::Scope isolate_scope(isolate); HandleScope handle_scope(isolate); diff --git a/TestRunner/app/tests/Marshalling/RecordTests.js b/TestRunner/app/tests/Marshalling/RecordTests.js index 254edc7ff..0b37dd61e 100644 --- a/TestRunner/app/tests/Marshalling/RecordTests.js +++ b/TestRunner/app/tests/Marshalling/RecordTests.js @@ -4,6 +4,22 @@ describe(module.id, function () { gc(); }); + it("shares one prototype per struct type and keeps it read-only", function () { + var fromNative = CGRectMake(1, 2, 3, 4); + var fromJs = new CGRect({ origin: { x: 1, y: 2 }, size: { width: 3, height: 4 } }); + expect(Object.getPrototypeOf(fromNative)).toBe(CGRect.prototype); + expect(Object.getPrototypeOf(fromJs)).toBe(CGRect.prototype); + expect(fromNative instanceof CGRect).toBe(true); + expect(fromJs instanceof CGRect).toBe(true); + expect(Object.getPrototypeOf(fromNative.origin)).toBe(CGPoint.prototype); + expect(fromNative.origin instanceof CGPoint).toBe(true); + + var descriptor = Object.getOwnPropertyDescriptor(CGRect, "prototype"); + expect(descriptor.writable).toBe(false); + expect(function () { "use strict"; CGRect.prototype = {}; }).toThrow(); + expect(Object.getPrototypeOf(CGRectMake(0, 0, 0, 0))).toBe(CGRect.prototype); + }); + it("SimpleRecord", function () { var record = new TNSSimpleStruct(); expect(record instanceof TNSSimpleStruct).toBe(true); From f78f56b5b4eb207f041f5767b6bd5ae1ff09f41d Mon Sep 17 00:00:00 2001 From: Eduardo Speroni Date: Thu, 8 Oct 2026 01:18:31 -0300 Subject: [PATCH 2/2] perf(runtime): register root struct objects with ObjectManager once CreateJsWrapper's struct branch registered every fresh struct object, and StructToValue and StructConstructorCallback registered the same object again to obtain the handle they keep for child-view lookups. The second finalizer found the wrapper already deleted and allocated an External inside the GC finalizer, forcing a nested collection. The struct branch now honours skipGCRegistration and the two callers that keep the handle register exactly once; child views are unchanged. Release TestRunner: screen.bounds 618 -> 417 ns, UIScreen.mainScreen.bounds.size.width 1436 -> 1164 ns. Suite 1743/0, ASan lane clean. --- NativeScript/runtime/ArgConverter.h | 7 ++++-- NativeScript/runtime/ArgConverter.mm | 29 +++++++++++++++---------- NativeScript/runtime/Interop.mm | 7 ++++-- NativeScript/runtime/MetadataBuilder.mm | 2 +- 4 files changed, 28 insertions(+), 17 deletions(-) diff --git a/NativeScript/runtime/ArgConverter.h b/NativeScript/runtime/ArgConverter.h index 5e918109c..57c5b1cfc 100644 --- a/NativeScript/runtime/ArgConverter.h +++ b/NativeScript/runtime/ArgConverter.h @@ -42,6 +42,9 @@ class ArgConverter { v8::Local receiver, V8Args& args, const MethodMeta* meta, bool isMethodCallback); + // skipGCRegistration: for ObjC wrappers the object gets a strong handle and + // no finalizer; for Struct wrappers the caller must call + // ObjectManager::Register itself, exactly once. static v8::Local ConvertArgument( v8::Local context, BaseDataWrapper* wrapper, bool skipGCRegistration = false, @@ -54,8 +57,6 @@ class ArgConverter { std::vector()); static std::shared_ptr> CreateEmptyObject( v8::Local context, bool skipGCRegistration = false); - static std::shared_ptr> CreateEmptyStruct( - v8::Local context); static const Meta* FindMeta(Class klass, const TypeEncoding* typeEncoding = nullptr); // Looks up the JS wrapper cached for `target` in Caches::Instances, dropping @@ -101,6 +102,8 @@ class ArgConverter { v8::Local context, v8::NamedPropertyGetterCallback propertyGetter = nullptr, v8::NamedPropertySetterCallbackV2 propertySetter = nullptr); + static v8::Local NewEmptyInstance( + v8::Local context, v8::Persistent* ctorFunc); static std::shared_ptr> CreateEmptyInstance( v8::Local context, v8::Persistent* ctorFunc, bool skipGCRegistration = false); diff --git a/NativeScript/runtime/ArgConverter.mm b/NativeScript/runtime/ArgConverter.mm index 1bdd325bb..bae556217 100644 --- a/NativeScript/runtime/ArgConverter.mm +++ b/NativeScript/runtime/ArgConverter.mm @@ -842,8 +842,14 @@ if (wrapper->Type() == WrapperType::Struct) { if (receiver.IsEmpty()) { - std::shared_ptr> poStruct = CreateEmptyStruct(context); - receiver = poStruct->Get(isolate).As(); + Persistent* ctorFunc = Caches::Get(isolate)->EmptyStructCtorFunc.get(); + tns::Assert(ctorFunc != nullptr, isolate); + receiver = NewEmptyInstance(context, ctorFunc); + // A struct object must carry exactly one finalizer registration; callers + // passing skipGCRegistration keep the handle and register it themselves. + if (!skipGCRegistration) { + ObjectManager::Register(context, receiver); + } } StructWrapper* structWrapper = static_cast(wrapper); @@ -1083,15 +1089,8 @@ return ArgConverter::CreateEmptyInstance(context, ctorFunc, skipGCRegistration); } -std::shared_ptr> ArgConverter::CreateEmptyStruct(Local context) { - Isolate* isolate = v8::Isolate::GetCurrent(); - Persistent* ctorFunc = Caches::Get(isolate)->EmptyStructCtorFunc.get(); - tns::Assert(ctorFunc != nullptr, isolate); - return ArgConverter::CreateEmptyInstance(context, ctorFunc); -} - -std::shared_ptr> ArgConverter::CreateEmptyInstance( - Local context, Persistent* ctorFunc, bool skipGCRegistration) { +Local ArgConverter::NewEmptyInstance(Local context, + Persistent* ctorFunc) { Isolate* isolate = v8::Isolate::GetCurrent(); Local emptyCtorFunc = ctorFunc->Get(isolate); Local value; @@ -1099,7 +1098,13 @@ !value->IsObject()) { tns::Assert(false, isolate); } - Local result = value.As(); + return value.As(); +} + +std::shared_ptr> ArgConverter::CreateEmptyInstance( + Local context, Persistent* ctorFunc, bool skipGCRegistration) { + Isolate* isolate = v8::Isolate::GetCurrent(); + Local result = NewEmptyInstance(context, ctorFunc); std::shared_ptr> poValue; if (!skipGCRegistration) { diff --git a/NativeScript/runtime/Interop.mm b/NativeScript/runtime/Interop.mm index a258b136a..d35a05b27 100644 --- a/NativeScript/runtime/Interop.mm +++ b/NativeScript/runtime/Interop.mm @@ -813,8 +813,11 @@ inline bool isBool() { return it->second->Get(isolate); } - Local res = ArgConverter::ConvertArgument(context, wrapper); - if (parentStruct == nullptr) { + // The root's registration handle is the one StructInstances and child views' + // Parent() hold; child views are registered by ConvertArgument. + bool isRoot = parentStruct == nullptr; + Local res = ArgConverter::ConvertArgument(context, wrapper, isRoot); + if (isRoot) { std::shared_ptr> poResult = ObjectManager::Register(context, res); cache->StructInstances.emplace(key, poResult); } diff --git a/NativeScript/runtime/MetadataBuilder.mm b/NativeScript/runtime/MetadataBuilder.mm index 966eebfd9..fd4728560 100644 --- a/NativeScript/runtime/MetadataBuilder.mm +++ b/NativeScript/runtime/MetadataBuilder.mm @@ -295,7 +295,7 @@ throw NativeScriptException( StructWrapper* wrapper = new StructWrapper(structInfo, dest, nullptr); Local context = isolate->GetCurrentContext(); - Local result = ArgConverter::ConvertArgument(context, wrapper); + Local result = ArgConverter::ConvertArgument(context, wrapper, true); std::shared_ptr cache = Caches::Get(isolate); std::shared_ptr> poResult = ObjectManager::Register(context, result);