diff --git a/NativeScript/runtime/ArgConverter.h b/NativeScript/runtime/ArgConverter.h index 5e918109..57c5b1cf 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 d8674990..bae55621 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) { @@ -842,20 +842,25 @@ 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); - 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); } @@ -1084,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; @@ -1100,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/Caches.cpp b/NativeScript/runtime/Caches.cpp index cdb39610..13c86574 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 bc4641e4..c7f497a5 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 c094b27e..68cd1cc5 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 29f5951c..b9199a76 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 c53ceef1..39139d41 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 535f2959..85a556f1 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 304535be..d35a05b2 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,14 +807,17 @@ 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); } - 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); } @@ -821,17 +825,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 +852,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 +860,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 +946,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 +991,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 +1000,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 +1466,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 5846bd52..5f93e39b 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 c997d242..e8b3ae82 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 604e4933..fd472856 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; @@ -232,11 +295,11 @@ 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); - 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 013cf18f..5d84c1e2 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 316326c5..5828663b 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 bcd37978..216fb710 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 254edc7f..0b37dd61 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);