From ab0b10871f84bbdbfbc761824f57804f55e9ff05 Mon Sep 17 00:00:00 2001 From: Eduardo Speroni Date: Mon, 12 Oct 2026 16:44:02 -0300 Subject: [PATCH] perf(runtime): build struct objects from per-type templates with GC-owned storage Each struct type gets a FunctionTemplate whose instance template carries a native data property per field, replacing the shared empty constructor with a named interceptor plus a SetPrototype per instance. Field reads become inline-cacheable accessor calls instead of string-compared interceptor hits. A struct object's wrapper and bytes live in one malloc block handed to V8 as the external backing store of an ArrayBuffer held in an internal field, with `free` as its deleter. V8's ArrayBuffer sweeper reclaims it once the struct object is unreachable, so struct objects are no longer registered with ObjectManager and have no finalizer, and a struct can never be reached after its memory is gone, even through an object a refusing finalizer revives. Nested struct fields stay live views into the root's bytes. Each view is cached in an internal field of its parent, so `rect.size === rect.size`, and keeps its parent alive through another internal field; its own storage block holds only the wrapper. This removes the per-read child wrapper + weak handle, the ChildCount re-arm logic and Caches::StructInstances. Views more than one level deep are now views as well (they used to be detached copies). Fields are own, enumerable, non-deletable properties, so Object.keys and JSON.stringify see them, and methods added to a struct's prototype are reachable (the interceptor used to answer undefined for every non-field name). Because Object.assign now reaches every setter, none of them asserts: - a struct-typed field takes a struct of the same layout and field names or a plain object, and throws a TypeError otherwise (it used to copy the field's size from any struct, over-reading a smaller one); - a pointer-typed field takes a Pointer, null, an ArrayBuffer, a struct (its address) or an interop.Reference (the address it wraps, without re-pointing the Reference), and throws a TypeError for anything else; - other encodings go through WriteValue. Writes through a Proxy receiver land in the native buffer. A struct built over a pointer (`CGRect(pointer)`) aliases the pointer's memory instead of freeing it when collected, and keeps the Pointer object alive. `new` without an initializer zero-fills. --- NativeScript/runtime/ArgConverter.h | 11 +- NativeScript/runtime/ArgConverter.mm | 46 +-- NativeScript/runtime/Caches.cpp | 1 - NativeScript/runtime/Caches.h | 30 +- NativeScript/runtime/DataWrapper.h | 40 ++- NativeScript/runtime/Interop.h | 14 +- NativeScript/runtime/Interop.mm | 173 +++++++---- NativeScript/runtime/MetadataBuilder.h | 28 +- NativeScript/runtime/MetadataBuilder.mm | 294 +++++++++++------- NativeScript/runtime/ObjectManager.mm | 27 +- NativeScript/runtime/Runtime.mm | 5 +- TestRunner/app/tests/GCFinalizerTests.js | 46 ++- .../app/tests/Marshalling/RecordTests.js | 239 ++++++++++++++ docs/knowledge/v8-resurrecting-finalizers.md | 6 +- 14 files changed, 654 insertions(+), 306 deletions(-) diff --git a/NativeScript/runtime/ArgConverter.h b/NativeScript/runtime/ArgConverter.h index defd275d..9fa21ee4 100644 --- a/NativeScript/runtime/ArgConverter.h +++ b/NativeScript/runtime/ArgConverter.h @@ -34,17 +34,14 @@ struct MethodCallbackWrapper { class ArgConverter { public: - static void Init(v8::Local context, - v8::NamedPropertyGetterCallback structPropertyGetter, - v8::NamedPropertySetterCallbackV2 structPropertySetter); + static void Init(v8::Local context); static v8::Local Invoke(v8::Local context, Class klass, 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. + // no finalizer. static v8::Local ConvertArgument( v8::Local context, BaseDataWrapper* wrapper, bool skipGCRegistration = false, @@ -101,9 +98,7 @@ class ArgConverter { private: static v8::Local CreateEmptyInstanceFunction( - v8::Local context, - v8::NamedPropertyGetterCallback propertyGetter = nullptr, - v8::NamedPropertySetterCallbackV2 propertySetter = nullptr); + v8::Local context); static v8::Local NewEmptyInstance( v8::Local context, v8::Persistent* ctorFunc); static std::shared_ptr> CreateEmptyInstance( diff --git a/NativeScript/runtime/ArgConverter.mm b/NativeScript/runtime/ArgConverter.mm index 7ba287b2..b2f7d8eb 100644 --- a/NativeScript/runtime/ArgConverter.mm +++ b/NativeScript/runtime/ArgConverter.mm @@ -16,15 +16,11 @@ namespace tns { -void ArgConverter::Init(Local context, NamedPropertyGetterCallback structPropertyGetter, - NamedPropertySetterCallbackV2 structPropertySetter) { +void ArgConverter::Init(Local context) { Isolate* isolate = v8::Isolate::GetCurrent(); auto cache = Caches::Get(isolate); cache->EmptyObjCtorFunc = std::make_unique>( isolate, ArgConverter::CreateEmptyInstanceFunction(context)); - cache->EmptyStructCtorFunc = std::make_unique>( - isolate, ArgConverter::CreateEmptyInstanceFunction(context, structPropertyGetter, - structPropertySetter)); } Local ArgConverter::Invoke(Local context, Class klass, Local receiver, @@ -855,34 +851,9 @@ return Null(isolate); } - if (wrapper->Type() == WrapperType::Struct) { - if (receiver.IsEmpty()) { - 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); - const StructInfo& structInfo = structWrapper->StructInfo(); - auto cache = Caches::Get(isolate); - 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); - } - - tns::SetValue(isolate, receiver, structWrapper); - - return receiver; - } + // Struct objects come from Interop::StructToValue or a struct constructor, + // never from here: their shape and lifetime are per struct type. + tns::Assert(wrapper->Type() != WrapperType::Struct, isolate); if (wrapper->Type() == WrapperType::ObjCAllocObject) { ObjCAllocDataWrapper* allocDataWrapper = static_cast(wrapper); @@ -1131,19 +1102,12 @@ return poValue; } -Local ArgConverter::CreateEmptyInstanceFunction( - Local context, NamedPropertyGetterCallback propertyGetter, - NamedPropertySetterCallbackV2 propertySetter) { +Local ArgConverter::CreateEmptyInstanceFunction(Local context) { Isolate* isolate = v8::Isolate::GetCurrent(); Local emptyInstanceCtorFuncTemplate = FunctionTemplate::New(isolate, nullptr); Local instanceTemplate = emptyInstanceCtorFuncTemplate->InstanceTemplate(); instanceTemplate->SetInternalFieldCount(2); - if (propertyGetter != nullptr || propertySetter != nullptr) { - NamedPropertyHandlerConfiguration config(propertyGetter, propertySetter); - instanceTemplate->SetHandler(config); - } - instanceTemplate->SetHandler(IndexedPropertyHandlerConfiguration(IndexedPropertyGetterCallback, IndexedPropertySetterCallback)); diff --git a/NativeScript/runtime/Caches.cpp b/NativeScript/runtime/Caches.cpp index 868c1b3b..dc1a3344 100644 --- a/NativeScript/runtime/Caches.cpp +++ b/NativeScript/runtime/Caches.cpp @@ -29,7 +29,6 @@ Caches::~Caches() { this->CFunctions.clear(); this->Instances.clear(); - this->StructInstances.clear(); this->PointerInstances.clear(); this->cacheBoundObjects_.clear(); } diff --git a/NativeScript/runtime/Caches.h b/NativeScript/runtime/Caches.h index 01d0b642..6d67ef74 100644 --- a/NativeScript/runtime/Caches.h +++ b/NativeScript/runtime/Caches.h @@ -14,6 +14,8 @@ namespace tns { struct StructInfo; +class StructWrapper; +enum class StructStorage; struct ObjectWeakCallbackState; class PromiseRejectionTracker; class IsolateTracked; @@ -35,13 +37,6 @@ struct TransparentStringEqual { } }; -struct pair_hash { - template - std::size_t operator()(const std::pair& pair) const { - return std::hash()(pair.first) ^ std::hash()(pair.second); - } -}; - class Caches { public: class WorkerState { @@ -169,12 +164,6 @@ class Caches { robin_hood::unordered_map>> Instances; - // 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; robin_hood::unordered_map>> PointerInstances; @@ -188,20 +177,19 @@ class Caches { v8::Local, const BaseClassMeta*, KnownUnknownClassPair, const std::vector&)> ObjectCtorInitializer; - // 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; + // Builds a struct object over `data`: a copy of it (kCopy; zero-filled when + // `data` is null) or an alias kept alive by `parent` (kAlias). Indirected + // through std::function so this header stays free of MetadataBuilder. + std::function(v8::Local, const StructInfo&, + void* data, StructStorage storage, + v8::Local parent)> + StructInstanceFactory; robin_hood::unordered_map> Initializers; std::unique_ptr> EmptyObjCtorFunc = std::unique_ptr>(nullptr); - std::unique_ptr> EmptyStructCtorFunc = - std::unique_ptr>(nullptr); std::unique_ptr> SliceFunc = std::unique_ptr>(nullptr); std::unique_ptr> OriginalExtendsFunc = diff --git a/NativeScript/runtime/DataWrapper.h b/NativeScript/runtime/DataWrapper.h index 4059e2e6..f51b6c85 100644 --- a/NativeScript/runtime/DataWrapper.h +++ b/NativeScript/runtime/DataWrapper.h @@ -314,31 +314,39 @@ class StructTypeWrapper : public BaseDataWrapper { const struct StructInfo* structInfo_; }; +enum class StructStorage { + // The struct object owns a copy of the bytes. + kCopy, + // The struct object aliases bytes owned by whatever its parent field holds. + kAlias, +}; + +// Constructed in place at the start of the ArrayBuffer backing store held in +// kStorageField (followed by the bytes, for kCopy) and destroyed by the store's +// deleter, so struct objects need no finalizer. V8 may run that deleter on a +// background thread, and never runs it at process exit: the destructor must +// not touch V8 or the isolate, and nothing may depend on it running. That +// ArrayBuffer must never reach JS (copy the bytes instead): a detach or +// transfer would pull the wrapper out from under the struct object. class StructWrapper : public StructTypeWrapper { public: - StructWrapper(const struct StructInfo& structInfo, void* data, - std::shared_ptr> parent) - : StructTypeWrapper(structInfo), - data_(data), - childCount_(0), - parent_(parent) {} + // Internal fields of a struct object after the wrapper (field 0). The parent + // field holds whatever keeps aliased bytes alive: a view's parent struct + // object, or the Pointer object a pointer-backed struct was built over. From + // kFirstViewField on, one slot per struct-typed field caches its view. + static constexpr int kParentField = 1; + static constexpr int kStorageField = 2; + static constexpr int kFirstViewField = 3; + + StructWrapper(const struct StructInfo& structInfo, void* data) + : StructTypeWrapper(structInfo), data_(data) {} const WrapperType Type() { return WrapperType::Struct; } void* Data() const { return this->data_; } - std::shared_ptr> Parent() { return this->parent_; } - - void IncrementChildren() { this->childCount_++; } - - void DecrementChildren() { this->childCount_--; } - - int ChildCount() { return this->childCount_; } - private: void* data_; - int childCount_; - std::shared_ptr> parent_; }; class ObjCAllocDataWrapper : public BaseDataWrapper { diff --git a/NativeScript/runtime/Interop.h b/NativeScript/runtime/Interop.h index b35cb5a0..03b1e107 100644 --- a/NativeScript/runtime/Interop.h +++ b/NativeScript/runtime/Interop.h @@ -89,11 +89,13 @@ class Interop { static v8::Local GetResultByType( v8::Local context, BaseDataWrapper* typeWrapper, BaseCall* call, - std::shared_ptr> parentStruct = nullptr); + v8::Local parentStruct = v8::Local()); + // A non-empty `parentStruct` makes a struct-typed result a view into the + // parent's buffer (`call` points at the field) instead of a copy. static v8::Local GetResult( v8::Local context, const TypeEncoding* typeEncoding, BaseCall* call, bool marshalToPrimitive, - std::shared_ptr> parentStruct = nullptr, + v8::Local parentStruct = v8::Local(), bool isStructMember = false, bool ownsReturnedObject = false, bool returnsUnmanaged = false, bool isInitializer = false); static void SetStructPropertyValue(v8::Local context, @@ -160,10 +162,10 @@ class Interop { static bool isRefTypeEqual(const TypeEncoding* typeEncoding, const char* clazz); static v8::Local ToArray(v8::Local object); - static v8::Local StructToValue( - v8::Local context, void* result, - const StructInfo& structInfo, - std::shared_ptr> parentStruct); + static v8::Local StructToValue(v8::Local context, + void* result, + const StructInfo& structInfo, + v8::Local parentStruct); static const TypeEncoding* CreateEncoding(BinaryTypeEncodingType type); static v8::Local HandleOf(v8::Local context, v8::Local value); diff --git a/NativeScript/runtime/Interop.mm b/NativeScript/runtime/Interop.mm index 6bfec57f..35e31489 100644 --- a/NativeScript/runtime/Interop.mm +++ b/NativeScript/runtime/Interop.mm @@ -887,42 +887,17 @@ static bool SnapshotArrayLike(Isolate* isolate, Local object, Local Interop::StructToValue(Local context, void* result, - const StructInfo& structInfo, - std::shared_ptr> parentStruct) { + const StructInfo& structInfo, Local parentStruct) { Isolate* isolate = v8::Isolate::GetCurrent(); - StructWrapper* wrapper = nullptr; - if (parentStruct == nullptr) { - ffi_type* ffiType = structInfo.FFIType(); - void* dest = malloc(ffiType->size); - memcpy(dest, result, ffiType->size); - - wrapper = new StructWrapper(structInfo, dest, nullptr); - } else { - Local parent = parentStruct->Get(isolate); - BaseDataWrapper* parentWrapper = tns::GetValue(isolate, parent); - if (parentWrapper != nullptr && parentWrapper->Type() == WrapperType::Struct) { - StructWrapper* parentStructWrapper = static_cast(parentWrapper); - parentStructWrapper->IncrementChildren(); - } - wrapper = new StructWrapper(structInfo, result, parentStruct); - } - std::shared_ptr cache = Caches::Get(isolate); - 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); - } + tns::Assert(cache != nullptr && cache->StructInstanceFactory != nullptr, isolate); - // 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); + if (parentStruct.IsEmpty()) { + return cache->StructInstanceFactory(context, structInfo, result, StructStorage::kCopy, + Local()); } - return res; + return cache->StructInstanceFactory(context, structInfo, result, StructStorage::kAlias, + parentStruct); } void Interop::InitializeStruct(Local context, void* destBuffer, @@ -1044,8 +1019,7 @@ static bool SnapshotArrayLike(Isolate* isolate, Local object, Local Interop::GetResultByType(Local context, BaseDataWrapper* typeWrapper, - BaseCall* call, - std::shared_ptr> parentStruct) { + BaseCall* call, Local parentStruct) { Isolate* isolate = v8::Isolate::GetCurrent(); if (typeWrapper->Type() == WrapperType::StructType) { @@ -1061,8 +1035,7 @@ static bool SnapshotArrayLike(Isolate* isolate, Local object, Local Interop::GetResult(Local context, const TypeEncoding* typeEncoding, - BaseCall* call, bool marshalToPrimitive, - std::shared_ptr> parentStruct, + BaseCall* call, bool marshalToPrimitive, Local parentStruct, bool isStructMember, bool ownsReturnedObject, bool returnsUnmanaged, bool isInitializer) { Isolate* isolate = v8::Isolate::GetCurrent(); @@ -1247,7 +1220,7 @@ static bool SnapshotArrayLike(Isolate* isolate, Local object, LocalCif, FFI_FN(block->invoke), call.ResultBuffer(), call.ArgsArray()); - Local result = Interop::GetResult(context, enc, &call, true, nullptr); + Local result = Interop::GetResult(context, enc, &call, true); info.GetReturnValue().Set(result); }, @@ -1569,6 +1542,39 @@ static bool SnapshotArrayLike(Isolate* isolate, Local object, Local value) { + return value->IsArrayBuffer() || value->IsArrayBufferView() || value->IsSharedArrayBuffer(); +} + +bool HasSameStructLayout(const StructInfo& a, const StructInfo& b) { + if (&a == &b) { + return true; + } + const std::vector& aFields = a.Fields(); + const std::vector& bFields = b.Fields(); + if (a.FFIType()->size != b.FFIType()->size || aFields.size() != bFields.size()) { + return false; + } + for (size_t i = 0; i < aFields.size(); i++) { + if (aFields[i].Offset() != bFields[i].Offset() || + aFields[i].FFIType()->size != bFields[i].FFIType()->size || + aFields[i].Encoding()->type != bFields[i].Encoding()->type || + aFields[i].Name() != bFields[i].Name()) { + return false; + } + } + return true; +} + +} // namespace + void Interop::SetStructPropertyValue(Local context, StructWrapper* wrapper, const StructField& field, Local value) { if (value.IsEmpty()) { @@ -1581,18 +1587,46 @@ static bool SnapshotArrayLike(Isolate* isolate, Local object, Localtype) { - case BinaryTypeEncodingType::StructDeclarationReference: { - BaseDataWrapper* sourceWrapper = - tns::GetValueOrReport(isolate, value, "struct field assignment"); - if (sourceWrapper == nullptr) { + case BinaryTypeEncodingType::StructDeclarationReference: + case BinaryTypeEncodingType::AnonymousStructEncoding: { + const StructInfo* nestedInfo = nullptr; + if (fieldEncoding->type == BinaryTypeEncodingType::StructDeclarationReference) { + const Meta* meta = + ArgConverter::GetMeta(fieldEncoding->details.declarationReference.name.valuePtr()); + tns::Assert(meta != nullptr && meta->type() == MetaType::Struct, isolate); + nestedInfo = &FFICall::GetStructInfo(static_cast(meta)); + } else { + nestedInfo = + &FFICall::GetStructInfo(fieldEncoding->details.anonymousRecord.fieldsCount, + fieldEncoding->details.anonymousRecord.getFieldsEncodings(), + fieldEncoding->details.anonymousRecord.getFieldNames()); + } + + bool isArrayBufferLike = IsArrayBufferLike(value); + bool isNativeObject = + !isArrayBufferLike && value->IsObject() && value.As()->InternalFieldCount() > 0; + if (isNativeObject) { + BaseDataWrapper* sourceWrapper = + tns::GetValueOrReport(isolate, value, "struct field assignment"); + if (sourceWrapper == nullptr) { + return; + } + if (sourceWrapper->Type() == WrapperType::Struct) { + StructWrapper* source = static_cast(sourceWrapper); + if (HasSameStructLayout(source->StructInfo(), *nestedInfo)) { + // The source can be a view of this very field. + memmove(destBuffer, source->Data(), nestedInfo->FFIType()->size); + break; + } + } + } + if (isNativeObject || isArrayBufferLike || !value->IsObject()) { + isolate->ThrowException(Exception::TypeError(tns::ToV8String( + isolate, "A struct field accepts a struct of its type or a plain object"))); return; } - tns::Assert(sourceWrapper->Type() == WrapperType::Struct, isolate); - StructWrapper* targetStruct = static_cast(sourceWrapper); - void* sourceBuffer = targetStruct->Data(); - size_t fieldSize = field.FFIType()->size; - memcpy(destBuffer, sourceBuffer, fieldSize); + Interop::InitializeStruct(context, destBuffer, nestedInfo->Fields(), value); break; } case BinaryTypeEncodingType::ConstantArrayEncoding: { @@ -1600,14 +1634,39 @@ static bool SnapshotArrayLike(Isolate* isolate, Local object, Local target = tns::UnwrapProxy(value); + // A revoked Proxy (empty target) goes to WriteValue, which throws for it. + if (target.IsEmpty() || target->IsNullOrUndefined() || IsArrayBufferLike(target)) { + Interop::WriteValue(context, fieldEncoding, destBuffer, value); + break; + } + + BaseDataWrapper* wrapper = tns::GetValue(isolate, target); + if (wrapper == nullptr && target->IsObject() && + target.As()->InternalFieldCount() > 0) { + // A native object whose wrapper was released: report it, write nothing. + tns::GetValueOrReport(isolate, target, "struct field assignment"); + return; + } + + if (wrapper != nullptr && wrapper->Type() == WrapperType::Struct) { + Interop::SetValue(destBuffer, static_cast(wrapper)->Data()); + } else if (wrapper != nullptr && wrapper->Type() == WrapperType::Reference) { + // WriteValue would treat a Reference as an out-parameter and re-point it + // at fresh memory; a field stores the address it already wraps. + Interop::SetValue(destBuffer, Reference::GetWrappedPointer(context, target, fieldEncoding)); + } else if ((wrapper != nullptr && wrapper->Type() == WrapperType::Pointer) || + (wrapper == nullptr && target->IsObject() && + fieldEncoding->details.pointer.getInnerType()->type == + BinaryTypeEncodingType::StructDeclarationReference)) { + // WriteValue snapshots a plain object into a new struct for a struct pointer. + Interop::WriteValue(context, fieldEncoding, destBuffer, value); + } else { + isolate->ThrowException(Exception::TypeError(tns::ToV8String( + isolate, + "A pointer field accepts a Pointer, a Reference, a struct, an ArrayBuffer or null"))); return; } - tns::Assert(wrapper->Type() == WrapperType::Struct, isolate); - StructWrapper* structWrapper = static_cast(wrapper); - void* data = structWrapper->Data(); - Interop::SetValue(destBuffer, data); break; } case BinaryTypeEncodingType::UShortEncoding: { @@ -1661,8 +1720,7 @@ static bool SnapshotArrayLike(Isolate* isolate, Local object, Local result = Interop::GetResult( - methodCall.context_, methodCall.typeEncoding_, &call, marshalToPrimitive, nullptr, false, - methodCall.ownsReturnedObject_, methodCall.returnsUnmanaged_, methodCall.isInitializer_); + Local result = + Interop::GetResult(methodCall.context_, methodCall.typeEncoding_, &call, marshalToPrimitive, + Local(), false, methodCall.ownsReturnedObject_, + methodCall.returnsUnmanaged_, methodCall.isInitializer_); return result; } diff --git a/NativeScript/runtime/MetadataBuilder.h b/NativeScript/runtime/MetadataBuilder.h index e8b3ae82..d0660990 100644 --- a/NativeScript/runtime/MetadataBuilder.h +++ b/NativeScript/runtime/MetadataBuilder.h @@ -24,14 +24,12 @@ class MetadataBuilder { std::vector()); static v8::Local GetOrCreateStructCtorFunction( 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); - static v8::Intercepted StructPropertySetterCallback( - v8::Local property, v8::Local value, - const v8::PropertyCallbackInfo& info); + // See Caches::StructInstanceFactory. + static v8::Local NewStructInstance(v8::Local context, + const StructInfo& structInfo, + void* data, + StructStorage storage, + v8::Local parent); private: static v8::Local @@ -63,6 +61,20 @@ class MetadataBuilder { const v8::FunctionCallbackInfo& info); static void StructEqualsCallback( const v8::FunctionCallbackInfo& info); + static void StructFieldGetterCallback( + v8::Local property, + const v8::PropertyCallbackInfo& info); + static void StructFieldSetterCallback( + v8::Local property, v8::Local value, + const v8::PropertyCallbackInfo& info); + static StructWrapper* AttachStructStorage(v8::Isolate* isolate, + v8::Local instance, + const StructInfo& structInfo, + void* data, StructStorage storage); + static v8::Local CreateStructCtorTemplate( + v8::Isolate* isolate, StructTypeWrapper* typeWrapper); + static v8::Local GetOrCreateStructCtorTemplate( + v8::Local context, const StructInfo& structInfo); static void ToStringFunctionCallback( const v8::FunctionCallbackInfo& info); static std::pair GetStructData( diff --git a/NativeScript/runtime/MetadataBuilder.mm b/NativeScript/runtime/MetadataBuilder.mm index 374dff2a..d45d980c 100644 --- a/NativeScript/runtime/MetadataBuilder.mm +++ b/NativeScript/runtime/MetadataBuilder.mm @@ -21,14 +21,14 @@ namespace { struct StructTypeEntry { + StructTypeWrapper* typeWrapper = nullptr; + std::unique_ptr> ctorTemplate; std::unique_ptr> ctor; - std::unique_ptr> prototype; }; -// Per-isolate struct constructor functions and their prototypes, keyed by the +// Per-isolate struct constructor templates and functions, 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. +// lifetime of the process. struct StructTypeState { robin_hood::unordered_map types; }; @@ -68,6 +68,15 @@ bool ResolveProxyReceiver(Isolate* isolate, Local& receiver, const char* return true; } +// Internal field layout: see StructWrapper. The parent<->view cycle is plain +// JS, so the collector can reclaim it. +constexpr int kStructParentField = StructWrapper::kParentField; +constexpr int kStructFirstViewField = StructWrapper::kFirstViewField; + +// Accessor data: the field index in the low 16 bits, the view slot (0 for a +// field that is not a struct) above them. +constexpr int kStructViewSlotShift = 16; +constexpr int32_t kStructFieldIndexMask = (1 << kStructViewSlotShift) - 1; } // namespace void MetadataBuilder::RegisterConstantsOnGlobalObject(Isolate* isolate, @@ -228,19 +237,20 @@ NamedPropertyHandlerConfiguration config(MetadataBuilder::GlobalPropertyGetter, StructTypeState* state = Caches::StateFor(isolate); if (state != nullptr) { auto it = state->types.find(&structInfo); - if (it != state->types.end()) { + if (it != state->types.end() && it->second.ctor != nullptr) { return it->second.ctor->Get(isolate); } } - StructTypeWrapper* wrapper = new StructTypeWrapper(structInfo); - Local ext = External::New(isolate, wrapper, v8::kExternalPointerTypeTagDefault); + Local ctorTemplate = + MetadataBuilder::GetOrCreateStructCtorTemplate(context, structInfo); Local structCtorFunc; - bool success = - v8::Function::New(context, StructConstructorCallback, ext).ToLocal(&structCtorFunc); + bool success = ctorTemplate->GetFunction(context).ToLocal(&structCtorFunc); tns::Assert(success, isolate); - tns::SetValue(isolate, structCtorFunc, wrapper); + StructTypeWrapper* typeWrapper = + state != nullptr ? state->types[&structInfo].typeWrapper : new StructTypeWrapper(structInfo); + tns::SetValue(isolate, structCtorFunc, typeWrapper); Local equalsFunc; success = v8::Function::New(context, StructEqualsCallback).ToLocal(&equalsFunc); @@ -250,56 +260,142 @@ NamedPropertyHandlerConfiguration config(MetadataBuilder::GlobalPropertyGetter, structCtorFunc->Set(context, tns::ToV8String(isolate, "equals"), equalsFunc).FromMaybe(false); tns::Assert(success, isolate); - // 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)); + state->types[&structInfo].ctor = + std::make_unique>(isolate, structCtorFunc); } return structCtorFunc; } -Local MetadataBuilder::GetOrCreateStructPrototype(Local context, - const StructInfo& structInfo) { +Local MetadataBuilder::GetOrCreateStructCtorTemplate( + Local context, const StructInfo& structInfo) { + Isolate* isolate = v8::Isolate::GetCurrent(); + StructTypeState* state = Caches::StateFor(isolate); + if (state == nullptr) { + // Teardown has begun: build without caching. Instances created in this + // window do not share a prototype with earlier ones; only finalizer-driven + // conversions can reach it. + return MetadataBuilder::CreateStructCtorTemplate(isolate, new StructTypeWrapper(structInfo)); + } + + auto it = state->types.find(&structInfo); + if (it != state->types.end() && it->second.ctorTemplate != nullptr) { + return it->second.ctorTemplate->Get(isolate); + } + + // Building the template allocates and may run a GC, so no reference into + // `types` is held across the build. + StructTypeWrapper* typeWrapper = new StructTypeWrapper(structInfo); + Local ctorTemplate = + MetadataBuilder::CreateStructCtorTemplate(isolate, typeWrapper); + StructTypeEntry& entry = state->types[&structInfo]; + if (entry.ctorTemplate != nullptr) { + delete typeWrapper; + return entry.ctorTemplate->Get(isolate); + } + entry.typeWrapper = typeWrapper; + entry.ctorTemplate = std::make_unique>(isolate, ctorTemplate); + return ctorTemplate; +} + +Local MetadataBuilder::CreateStructCtorTemplate(Isolate* isolate, + StructTypeWrapper* typeWrapper) { + const StructInfo& structInfo = typeWrapper->StructInfo(); + Local ext = External::New(isolate, typeWrapper, v8::kExternalPointerTypeTagDefault); + Local ctorTemplate = + FunctionTemplate::New(isolate, StructConstructorCallback, ext); + // Like interface constructors, a struct constructor's prototype can be + // extended but not replaced. + ctorTemplate->ReadOnlyPrototype(); + + Local instanceTemplate = ctorTemplate->InstanceTemplate(); + const std::vector& fields = structInfo.Fields(); + tns::Assert(fields.size() <= kStructFieldIndexMask, isolate); + int viewSlots = 0; + // V8 instantiates template native-data properties in reverse definition + // order; define them back to front so enumeration follows the C layout. + for (size_t i = fields.size(); i-- > 0;) { + const StructField& field = fields[i]; + BinaryTypeEncodingType type = field.Encoding()->type; + int viewSlot = 0; + if (type == BinaryTypeEncodingType::StructDeclarationReference || + type == BinaryTypeEncodingType::AnonymousStructEncoding) { + viewSlot = kStructFirstViewField + viewSlots++; + } + Local name; + bool success = v8::String::NewFromUtf8(isolate, field.Name().c_str(), + NewStringType::kInternalized, (int)field.Name().size()) + .ToLocal(&name); + tns::Assert(success, isolate); + int32_t data = static_cast(i) | (viewSlot << kStructViewSlotShift); + instanceTemplate->SetNativeDataProperty(name, StructFieldGetterCallback, + StructFieldSetterCallback, + v8::Int32::New(isolate, data), DontDelete); + } + instanceTemplate->SetInternalFieldCount(kStructFirstViewField + viewSlots); + + return ctorTemplate; +} + +StructWrapper* MetadataBuilder::AttachStructStorage(Isolate* isolate, Local instance, + const StructInfo& structInfo, void* data, + StructStorage storage) { + // Darwin's malloc returns 16-byte-aligned blocks, which simd fields (e.g. + // simd_float4) need; alignof(max_align_t) is only 8 on arm64. + constexpr size_t kAlign = 16; + constexpr size_t kHeaderSize = (sizeof(StructWrapper) + kAlign - 1) & ~(kAlign - 1); + + size_t byteSize = storage == StructStorage::kCopy ? structInfo.FFIType()->size : 0; + void* block = malloc(kHeaderSize + byteSize); + void* bytes = static_cast(block) + kHeaderSize; + if (storage == StructStorage::kCopy) { + if (data != nullptr) { + memcpy(bytes, data, byteSize); + } else { + memset(bytes, 0, byteSize); + } + } + StructWrapper* wrapper = + new (block) StructWrapper(structInfo, storage == StructStorage::kCopy ? bytes : data); + + std::unique_ptr backingStore = ArrayBuffer::NewBackingStore( + block, kHeaderSize + byteSize, + [](void* data, size_t, void*) { + static_cast(data)->~StructWrapper(); + free(data); + }, + nullptr); + instance->SetInternalField(StructWrapper::kStorageField, + ArrayBuffer::New(isolate, std::move(backingStore))); + tns::SetValue(isolate, instance, wrapper); + return wrapper; +} + +Local MetadataBuilder::NewStructInstance(Local context, + const StructInfo& structInfo, void* data, + StructStorage storage, Local parent) { Isolate* isolate = v8::Isolate::GetCurrent(); StructTypeState* state = Caches::StateFor(isolate); if (state != nullptr) { auto it = state->types.find(&structInfo); - if (it == state->types.end()) { + if (it == state->types.end() || it->second.ctor == nullptr) { + // `instance.constructor` must be the complete constructor (`equals`, + // type metadata) even before JS has named the struct type. MetadataBuilder::GetOrCreateStructCtorFunction(context, structInfo); - it = state->types.find(&structInfo); - } - if (it != state->types.end()) { - return it->second.prototype->Get(isolate); } } + Local ctorTemplate = + MetadataBuilder::GetOrCreateStructCtorTemplate(context, structInfo); + Local instance; + bool success = ctorTemplate->InstanceTemplate()->NewInstance(context).ToLocal(&instance); + tns::Assert(success, 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(); + MetadataBuilder::AttachStructStorage(isolate, instance, structInfo, data, storage); + if (!parent.IsEmpty()) { + instance->SetInternalField(kStructParentField, parent); } - return proto.As(); + return instance; } void MetadataBuilder::StructConstructorCallback(const FunctionCallbackInfo& info) { @@ -311,13 +407,15 @@ NamedPropertyHandlerConfiguration config(MetadataBuilder::GlobalPropertyGetter, const StructInfo& structInfo = typeWrapper->StructInfo(); - void* dest = nullptr; - + Local result; if (info.IsConstructCall()) { // A new structure is allocated Local initializer = info.Length() > 0 ? info[0] : Local(); - dest = malloc(structInfo.FFIType()->size); - Interop::InitializeStruct(context, dest, structInfo.Fields(), initializer); + // `this` was built from the struct type's instance template. + result = info.This(); + StructWrapper* wrapper = MetadataBuilder::AttachStructStorage(isolate, result, structInfo, + nullptr, StructStorage::kCopy); + Interop::InitializeStruct(context, wrapper->Data(), structInfo.Fields(), initializer); } else { // The structure is not used as constructor and in this case we assume a pointer is passed to // the function This pointer will be used as backing memory for the structure @@ -328,20 +426,13 @@ throw NativeScriptException( "A pointer instance must be passed to the structure initializer"); } + // The memory stays the pointer's; holding the pointer object keeps it + // alive as long as the struct object. PointerWrapper* pw = static_cast(wrapper); - dest = pw->Data(); + result = MetadataBuilder::NewStructInstance(context, structInfo, pw->Data(), + StructStorage::kAlias, info[0].As()); } - StructWrapper* wrapper = new StructWrapper(structInfo, dest, nullptr); - Local context = isolate->GetCurrentContext(); - 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); - cache->StructInstances.emplace(key, poResult); - tns::DeleteWrapperIfUnused(isolate, result, wrapper); - info.GetReturnValue().Set(result); } catch (NativeScriptException& ex) { ex.ReThrowToV8(isolate); @@ -989,90 +1080,61 @@ NamedPropertyHandlerConfiguration config(nullptr, MetadataBuilder::SwizzledInsta item->ResolveClass(), false); } -Intercepted MetadataBuilder::StructPropertyGetterCallback(Local property, - const PropertyCallbackInfo& info) { +void MetadataBuilder::StructFieldGetterCallback(Local property, + const PropertyCallbackInfo& info) { Isolate* isolate = info.GetIsolate(); Local thiz = info.Holder(); + int32_t data = info.Data().As()->Value(); + int viewSlot = data >> kStructViewSlotShift; - std::string propertyName = tns::ToString(isolate, property); - - if (propertyName == "") { - info.GetReturnValue().Set(thiz); - return Intercepted::kYes; + if (viewSlot != 0) { + Local cached = thiz->GetInternalField(viewSlot); + if (cached->IsValue() && cached.As()->IsObject()) { + info.GetReturnValue().Set(cached.As()); + return; + } } BaseDataWrapper* baseWrapper = tns::GetValueOrReport(isolate, thiz, "struct property get"); if (baseWrapper == nullptr) { info.GetReturnValue().SetUndefined(); - return Intercepted::kYes; + return; } tns::Assert(baseWrapper->Type() == WrapperType::Struct, isolate); StructWrapper* wrapper = static_cast(baseWrapper); - const StructInfo& structInfo = wrapper->StructInfo(); - - std::shared_ptr cache = Caches::Get(isolate); - 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; - } - - 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; - } - - const StructField& field = *it; - const TypeEncoding* fieldEncoding = field.Encoding(); - ptrdiff_t offset = field.Offset(); - void* buffer = wrapper->Data(); - BaseCall call((uint8_t*)buffer, offset); + const StructField& field = wrapper->StructInfo().Fields()[data & kStructFieldIndexMask]; + BaseCall call((uint8_t*)wrapper->Data(), field.Offset()); Local context = isolate->GetCurrentContext(); - Local result = - Interop::GetResult(context, fieldEncoding, &call, false, parentStruct, true); + Local result = Interop::GetResult(context, field.Encoding(), &call, false, + viewSlot != 0 ? thiz : Local(), true); + if (viewSlot != 0) { + thiz->SetInternalField(viewSlot, result); + } info.GetReturnValue().Set(result); - return Intercepted::kYes; } -Intercepted MetadataBuilder::StructPropertySetterCallback( - Local property, Local value, const PropertyCallbackInfo& info) { +void MetadataBuilder::StructFieldSetterCallback(Local property, Local value, + const PropertyCallbackInfo& info) { Isolate* isolate = info.GetIsolate(); - Local context = isolate->GetCurrentContext(); Local thiz = info.Holder(); - - std::string propertyName = tns::ToString(isolate, property); - - if (propertyName == "") { - return Intercepted::kYes; - } + int32_t data = info.Data().As()->Value(); BaseDataWrapper* baseWrapper = tns::GetValueOrReport(isolate, thiz, "struct property set"); if (baseWrapper == nullptr) { - return Intercepted::kYes; + return; } tns::Assert(baseWrapper->Type() == WrapperType::Struct, isolate); StructWrapper* wrapper = static_cast(baseWrapper); - const StructInfo& structInfo = wrapper->StructInfo(); - 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()) { - return Intercepted::kNo; + const StructField& field = wrapper->StructInfo().Fields()[data & kStructFieldIndexMask]; + try { + Interop::SetStructPropertyValue(isolate->GetCurrentContext(), wrapper, field, value); + } catch (NativeScriptException& ex) { + ex.ReThrowToV8(isolate); } - - Interop::SetStructPropertyValue(context, wrapper, *it, value); - return Intercepted::kYes; } void MetadataBuilder::DefineFunctionLengthProperty(Local context, diff --git a/NativeScript/runtime/ObjectManager.mm b/NativeScript/runtime/ObjectManager.mm index 43119ff9..904ab233 100644 --- a/NativeScript/runtime/ObjectManager.mm +++ b/NativeScript/runtime/ObjectManager.mm @@ -202,30 +202,9 @@ void DisposeHandle(v8::Isolate* isolate, std::shared_ptr cache = Caches::Get(isolate); switch (wrapper->Type()) { - case WrapperType::Struct: { - StructWrapper* structWrapper = static_cast(wrapper); - void* data = structWrapper->Data(); - - std::shared_ptr> poParentStruct = structWrapper->Parent(); - if (poParentStruct != nullptr) { - Local parentStruct = poParentStruct->Get(isolate); - BaseDataWrapper* parentWrapper = tns::GetValue(isolate, parentStruct); - if (parentWrapper != nullptr && parentWrapper->Type() == WrapperType::Struct) { - StructWrapper* parentStructWrapper = static_cast(parentWrapper); - parentStructWrapper->DecrementChildren(); - } - } else { - if (structWrapper->ChildCount() == 0) { - std::pair key = - std::make_pair(data, &structWrapper->StructInfo()); - cache->StructInstances.erase(key); - std::free(data); - } else { - return false; - } - } - break; - } + case WrapperType::Struct: + // Lives in the struct object's ArrayBuffer storage, which the GC frees. + return true; case WrapperType::ObjCObject: { ObjCDataWrapper* objCObjectWrapper = static_cast(wrapper); id target = objCObjectWrapper->Data(); diff --git a/NativeScript/runtime/Runtime.mm b/NativeScript/runtime/Runtime.mm index a358a46f..ccd8cc16 100644 --- a/NativeScript/runtime/Runtime.mm +++ b/NativeScript/runtime/Runtime.mm @@ -397,7 +397,7 @@ void DisposeIsolateWhenPossible(Isolate* isolate, int gateId) { IsolateGates::Open(cache->getGateId()); cache->isWorker = isWorker; cache->ObjectCtorInitializer = MetadataBuilder::GetOrCreateConstructorFunctionTemplate; - cache->StructPrototypeInitializer = MetadataBuilder::GetOrCreateStructPrototype; + cache->StructInstanceFactory = MetadataBuilder::NewStructInstance; Isolate::Scope isolate_scope(isolate); HandleScope handle_scope(isolate); @@ -481,8 +481,7 @@ void DisposeIsolateWhenPossible(Isolate* isolate, int gateId) { this->moduleInternal_ = std::make_unique(context); - ArgConverter::Init(context, MetadataBuilder::StructPropertyGetterCallback, - MetadataBuilder::StructPropertySetterCallback); + ArgConverter::Init(context); Interop::RegisterInteropTypes(context); ClassBuilder::RegisterBaseTypeScriptExtendsFunction( diff --git a/TestRunner/app/tests/GCFinalizerTests.js b/TestRunner/app/tests/GCFinalizerTests.js index d51ea954..4d77e7a5 100644 --- a/TestRunner/app/tests/GCFinalizerTests.js +++ b/TestRunner/app/tests/GCFinalizerTests.js @@ -63,7 +63,9 @@ describe("GC finalizer callbacks", function () { (function create() { for (var i = 0; i < N; i++) { var t = HuskParent.alloc().init(); - t.buddy = CGPointMake(3, 4); + // Structs keep their storage while reachable and never husk; + // a Reference still has a finalizer of its own. + t.buddy = new interop.Reference(interop.types.int32, 3); t.__mine = true; holder.addObject(t); // native-only retain → GcProtect } @@ -117,7 +119,7 @@ describe("GC finalizer callbacks", function () { // Default policy: report — the operations no-op. rect.origin = husk; expect(rect.origin.x).toBe(0); - expect(husk.x).toBeUndefined(); + expect(husk[0]).toBeUndefined(); // Throw policy: same touches become catchable ReferenceErrors. var runtime = require("ns:runtime"); @@ -126,7 +128,7 @@ describe("GC finalizer callbacks", function () { rect.origin = husk; }).toThrowError(/released/); expect(function () { - return husk.x; + return husk[0]; }).toThrowError(/released/); runtime.setConfig("releasedObjectPolicy", "report"); @@ -142,6 +144,44 @@ describe("GC finalizer callbacks", function () { }, 0); }); + it("keeps a struct view reachable only through a resurrected parent intact", function () { + var StructParent = (function (_super) { + __extends(StructParent, _super); + function StructParent() { + return (_super !== null && _super.apply(this, arguments)) || this; + } + return StructParent; + })(NSObject); + + var N = 16; + var holder = NSMutableArray.alloc().init(); + (function create() { + for (var i = 0; i < N; i++) { + var t = StructParent.alloc().init(); + // Only the view is held; its root is reachable through it alone. + t.size = CGRectMake(1, 2, 3, 4).size; + t.__mine = true; + holder.addObject(t); + } + })(); + + scrubStack(); + __collect(); + __collect(); + + for (var i = 0; i < N; i++) { + var t2 = holder.objectAtIndex(i); + if (t2.__mine !== true) { + continue; + } + expect(t2.size.width).toBe(3); + expect(t2.size.height).toBe(4); + t2.size.width = 5; + expect(t2.size.width).toBe(5); + } + holder.removeAllObjects(); + }); + // Overwrites the stack region that held the creation locals so // conservative stack scanning cannot keep dead wrappers alive. function scrubStack() { diff --git a/TestRunner/app/tests/Marshalling/RecordTests.js b/TestRunner/app/tests/Marshalling/RecordTests.js index 0b37dd61..aebcf093 100644 --- a/TestRunner/app/tests/Marshalling/RecordTests.js +++ b/TestRunner/app/tests/Marshalling/RecordTests.js @@ -81,6 +81,245 @@ describe(module.id, function () { expect(size.height).toBe(40); }); + it("copies a struct returned from native", function () { + var rect = getRectStruct(); + rect.size.width = 1; + expect(getRectStruct().size.width).toBe(30); + }); + + it("returns the same view for repeated nested reads", function () { + var rect = getRectStruct(); + expect(rect.size).toBe(rect.size); + expect(rect.origin).toBe(rect.origin); + expect(rect.size).not.toBe(getRectStruct().size); + }); + + it("writes through a held view in both directions", function () { + var rect = getRectStruct(); + var size = rect.size; + size.width = 5; + expect(rect.size.width).toBe(5); + + rect.size = new CGSize({ width: 7, height: 8 }); + expect(size.width).toBe(7); + expect(size.height).toBe(8); + expect(CGRect.equals(rect, { origin: { x: 10, y: 20 }, size: { width: 7, height: 8 } })).toBe(true); + }); + + it("writes through views nested more than one level deep", function () { + var record = TNSTestNativeCallbacks.recordsNestedAnonymousStruct({ x1: 1, y1: { x2: 2, y2: { x3: 3 } } }); + var inner = record.y1.y2; + inner.x3 = 30; + expect(record.y1.y2).toBe(inner); + expect(record.y1.y2.x3).toBe(30); + }); + + it("keeps a deep view's root alive after the root and middle view are dropped", function () { + var inner = null; + (() => { + var record = TNSTestNativeCallbacks.recordsNestedAnonymousStruct({ x1: 1, y1: { x2: 2, y2: { x3: 3 } } }); + inner = record.y1.y2; + })(); + gc(); + gc(); + + expect(inner.x3).toBe(3); + }); + + it("keeps struct storage alive as long as a struct object reaches it", function () { + var size = getRectStruct().size; + var pointer = interop.alloc(interop.sizeof(CGSize)); + var aliased = CGSize(pointer); + aliased.width = 9; + for (var i = 0; i < 20000; i++) { + sink = getRectStruct().size.width + new CGPoint({ x: i, y: i }).x; + } + gc(); + gc(); + + expect(size.width).toBe(30); + expect(size.height).toBe(40); + expect(aliased.width).toBe(9); + expect(CGSize(pointer).width).toBe(9); + }); + var sink = 0; + + it("zero-fills a struct constructed without an initializer", function () { + var record = new TNSStructWithArray(); + expect(record.x).toBe(0); + expect([record.arr[0], record.arr[1], record.arr[2], record.arr[3]]).toEqual([0, 0, 0, 0]); + }); + + it("collects roots together with their cached views", function () { + for (var i = 0; i < 1000; i++) { + var rect = getRectStruct(); + rect.origin.x = i; + rect.size.width = i; + } + gc(); + gc(); + expect(getRectStruct().size.width).toBe(30); + }); + + it("exposes fields as own enumerable properties", function () { + var point = new CGPoint({ x: 1, y: 2 }); + expect(Object.keys(point)).toEqual(["x", "y"]); + expect(JSON.stringify(point)).toBe('{"x":1,"y":2}'); + expect(Object.keys(getRectStruct())).toEqual(["origin", "size"]); + expect(delete point.x).toBe(false); + expect(point.x).toBe(1); + }); + + it("gives a struct from native its complete constructor", function () { + var record = getStructWith3Doubles(); + expect(typeof record.constructor.equals).toBe("function"); + expect(interop.sizeof(record.constructor)).toBe(24); + }); + + it("assigns struct fields from structs and plain objects", function () { + var target = TNSTestNativeCallbacks.recordsNestedAnonymousStruct({ x1: 1, y1: { x2: 2, y2: { x3: 3 } } }); + var source = TNSTestNativeCallbacks.recordsNestedAnonymousStruct({ x1: 4, y1: { x2: 5, y2: { x3: 6 } } }); + var inner = target.y1; + Object.assign(target, source); + expect(target.x1).toBe(4); + expect(inner.x2).toBe(5); + expect(inner.y2.x3).toBe(6); + + var rect = getRectStruct(); + rect.size = { width: 1, height: 2 }; + expect(rect.size.width).toBe(1); + expect(rect.size.height).toBe(2); + expect(function () { rect.size = 5; }).toThrowError(TypeError); + }); + + it("leaves the memory of a pointer-backed struct to the pointer", function () { + var pointer = interop.alloc(interop.sizeof(CGSize)); + (() => { + var size = CGSize(pointer); + size.width = 3; + })(); + gc(); + gc(); + var again = CGSize(pointer); + again.height = 4; + expect(again.width).toBe(3); + expect(again.height).toBe(4); + }); + + it("assigns pointer fields from pointers, references, structs and null", function () { + var memory = interop.alloc(4); + var simple = new TNSSimpleStruct({ x: 1, y: 2 }); + var next = new TNSStructWithPointers(); + var source = new TNSStructWithPointers(); + source.x = memory; + source.y = simple; + source.z = next; + + var target = new TNSStructWithPointers(); + Object.assign(target, source); + expect(interop.handleof(target.x).toNumber()).toBe(memory.toNumber()); + expect(interop.handleof(target.y).toNumber()).toBe(interop.handleof(simple).toNumber()); + expect(target.y.value.x).toBe(1); + expect(interop.handleof(target.z).toNumber()).toBe(interop.handleof(next).toNumber()); + expect(interop.handleof(source.x).toNumber()).toBe(memory.toNumber()); + + var unbound = new interop.Reference(interop.types.int32, 5); + target.x = unbound; + expect(target.x.value).toBe(5); + + target.x = null; + expect(target.x).toBe(null); + }); + + it("rejects values a pointer field cannot hold", function () { + var record = new TNSStructWithPointers(); + var memory = interop.alloc(4); + record.x = memory; + + expect(function () { record.x = "abc"; }).toThrowError(TypeError); + expect(function () { record.x = 5; }).toThrowError(TypeError); + expect(function () { record.x = {}; }).toThrowError(TypeError); + expect(function () { Object.assign(record, { x: "abc" }); }).toThrowError(TypeError); + expect(interop.handleof(record.x).toNumber()).toBe(memory.toNumber()); + + var buffer = new ArrayBuffer(4); + record.x = buffer; + expect(record.x).not.toBe(null); + + record.y = { x: 7, y: 8 }; + expect(record.y.value.x).toBe(7); + + var rect = getRectStruct(); + expect(function () { rect.size = new ArrayBuffer(16); }).toThrowError(TypeError); + }); + + it("rejects a struct of another type assigned to a struct field", function () { + var rect = getRectStruct(); + expect(function () { rect.size = new CGPoint({ x: 1, y: 2 }); }).toThrowError(TypeError); + expect(function () { rect.size = getRectStruct(); }).toThrowError(TypeError); + expect(rect.size.width).toBe(30); + expect(rect.size.height).toBe(40); + + rect.size = new CGSize({ width: 5, height: 6 }); + expect(rect.size.width).toBe(5); + }); + + function nativeCopy(rect) { + return NSValue.valueWithCGRect(rect).CGRectValue; + } + + [ + ["without traps", {}], + ["with Reflect traps", { + get: function (t, k, r) { return Reflect.get(t, k, r); }, + set: function (t, k, v, r) { return Reflect.set(t, k, v, r); } + }] + ].forEach(function (testCase) { + it("reads and writes struct fields through a Proxy " + testCase[0], function () { + var rect = getRectStruct(); + var proxy = new Proxy(rect, testCase[1]); + expect(proxy.origin.x).toBe(10); + expect(proxy.size.height).toBe(40); + + proxy.origin.x = 1; + proxy.size = { width: 2, height: 3 }; + var origin = new Proxy(rect.origin, testCase[1]); + origin.y = 4; + var direct = new Proxy(rect.size, testCase[1]); + direct.height = 5; + + expect(rect.origin.x).toBe(1); + expect(rect.origin.y).toBe(4); + expect(rect.size.width).toBe(2); + expect(rect.size.height).toBe(5); + var copy = nativeCopy(rect); + expect(copy.origin.x).toBe(1); + expect(copy.origin.y).toBe(4); + expect(copy.size.width).toBe(2); + expect(copy.size.height).toBe(5); + + // The fields stay bound to the buffer after a write through a Proxy. + proxy.size.width = 7; + rect.size.height = 8; + expect(nativeCopy(rect).size.width).toBe(7); + expect(nativeCopy(rect).size.height).toBe(8); + expect(Object.getOwnPropertyDescriptor(rect.size, "height").configurable).toBe(false); + }); + }); + + it("reaches methods on the struct prototype", function () { + CGSize.prototype.area = function () { + return this.width * this.height; + }; + try { + expect(getRectStruct().size.area()).toBe(1200); + expect(new CGSize({ width: 2, height: 3 }).area()).toBe(6); + } finally { + delete CGSize.prototype.area; + } + expect(typeof getRectStruct().hasOwnProperty).toBe("function"); + }); + it("RecordConstructorPointer", function () { (function () { var size = interop.sizeof(TNSNestedStruct); diff --git a/docs/knowledge/v8-resurrecting-finalizers.md b/docs/knowledge/v8-resurrecting-finalizers.md index c7326287..ece736d6 100644 --- a/docs/knowledge/v8-resurrecting-finalizers.md +++ b/docs/knowledge/v8-resurrecting-finalizers.md @@ -190,7 +190,7 @@ missing wrapper — default `"report"` no-ops the operation and fires the catchable JS `ReferenceError` — and every internal-field read that could otherwise see a neutered field goes through it — `Pointer.cpp`'s methods, `ExtVector.cpp` toString, `Interop::SetStructPropertyValue` (both struct cases), `MetadataBuilder::GetStructData` and -both struct property interceptors, the two `InteropTypes.mm` argument readers, and the +both struct field accessors, the two `InteropTypes.mm` argument readers, and the `Reference.cpp` accessors (`SetValueCallback`, `GetWrappedPointer`, `GetDataPair` → indexed get/set, toString). Regression spec: *"throws instead of crashing when touching a released object through a resurrected parent"* in `GCFinalizerTests.js`. @@ -199,7 +199,9 @@ Two facts worth knowing when reasoning about which objects can husk: - `Pointer`/`interop.alloc` objects never husk at runtime — `Caches::PointerInstances` holds them behind strong persistents, so their finalizers cannot fire. Struct instances - and `Reference`s are held only by their own weak finalizer handle and do husk. + never husk either: their wrapper and bytes live in an ArrayBuffer backing store held by + the struct object, so the GC frees them only once nothing (resurrected or not) reaches + it. `Reference`s are held only by their own weak finalizer handle and do husk. - Only the TypeScript `__extends` path installs the retain/release swizzles that drive `GcProtect()`; a class from plain `.extend()` is never protected, so its instances are disposed rather than resurrected when natively retained.