From ecefc67fc6c4109ab5a5a8c9c5294b9c891e3927 Mon Sep 17 00:00:00 2001 From: Eduardo Speroni Date: Fri, 13 Nov 2026 16:07:18 -0300 Subject: [PATCH 1/3] fix(runtime): cache bindings of protocol-qualified objects instead of rebuilding the class Wrapping an object whose declared type adds protocols to its class (id

, T

*) skipped the constructor cache and rebuilt the class's whole binding on every call. The rebuilt template, constructor and prototype were offered to caches that already held the class, so the inserts were no-ops and their Persistents leaked strong V8 handles (about 125 KB per object for NSObject). The rebuild also replaced the global constructor, after which `instanceof NSObject` was false for every existing object. Such objects now get a binding that inherits from the class's own binding and adds only the protocol members the class lacks, cached per class and protocol set. It is never published as the class's constructor or stored in the class caches, and its prototype's `constructor` is the class's constructor. --- NativeScript/runtime/MetadataBuilder.h | 8 ++ NativeScript/runtime/MetadataBuilder.mm | 116 ++++++++++++++++++- TestRunner/app/tests/ProtocolBindingTests.js | 67 +++++++++++ TestRunner/app/tests/index.js | 1 + 4 files changed, 186 insertions(+), 6 deletions(-) create mode 100644 TestRunner/app/tests/ProtocolBindingTests.js diff --git a/NativeScript/runtime/MetadataBuilder.h b/NativeScript/runtime/MetadataBuilder.h index d0660990..f87d7a6d 100644 --- a/NativeScript/runtime/MetadataBuilder.h +++ b/NativeScript/runtime/MetadataBuilder.h @@ -32,6 +32,14 @@ class MetadataBuilder { v8::Local parent); private: + // The binding of an object whose static type adds protocols to `meta`: a + // template inheriting from the class's own binding with the protocols' + // extra members, cached per class and protocol set. Never published as the + // class's constructor. + static v8::Local GetOrCreateProtocolBindingTemplate( + v8::Local context, const BaseClassMeta* meta, + KnownUnknownClassPair pair, + const std::vector& additionalProtocols); static v8::Local GetOrCreateConstructorFunctionTemplateInternal( v8::Local context, const BaseClassMeta* meta, diff --git a/NativeScript/runtime/MetadataBuilder.mm b/NativeScript/runtime/MetadataBuilder.mm index d45d980c..7a1aa52b 100644 --- a/NativeScript/runtime/MetadataBuilder.mm +++ b/NativeScript/runtime/MetadataBuilder.mm @@ -1,5 +1,7 @@ #include "MetadataBuilder.h" #include +#include +#include #include "ArgConverter.h" #include "Caches.h" #include "Constants.h" @@ -532,12 +534,15 @@ throw NativeScriptException( Local ctorFuncTemplate; auto cache = Caches::Get(isolate); - if (additionalProtocols.empty()) { - auto it = cache->CtorFuncTemplates.find(meta); - if (it != cache->CtorFuncTemplates.end()) { - ctorFuncTemplate = it->second->Get(isolate); - return ctorFuncTemplate; - } + if (!additionalProtocols.empty()) { + return MetadataBuilder::GetOrCreateProtocolBindingTemplate(context, meta, pair, + additionalProtocols); + } + + auto cached = cache->CtorFuncTemplates.find(meta); + if (cached != cache->CtorFuncTemplates.end()) { + ctorFuncTemplate = cached->second->Get(isolate); + return ctorFuncTemplate; } std::string className; @@ -662,6 +667,105 @@ NamedPropertyHandlerConfiguration config(nullptr, MetadataBuilder::SwizzledInsta return ctorFuncTemplate; } +namespace { + +struct ProtocolBindings { + std::map>, Global> + templates; +}; + +// The instance member names the class's own binding registers, so a +// protocol member the class already has keeps the class's definition. +void CollectInstanceMemberNames(const BaseClassMeta* meta, KnownUnknownClassPair pair, + bool ownMembers, + robin_hood::unordered_map& names) { + if (ownMembers) { + for (auto it = meta->instanceMethods->begin(); it != meta->instanceMethods->end(); it++) { + const MethodMeta* methodMeta = (*it).valuePtr(); + if (methodMeta->isAvailableInClasses(pair, false)) { + names.emplace(methodMeta->name(), 0); + } + } + for (auto it = meta->instanceProps->begin(); it != meta->instanceProps->end(); it++) { + const PropertyMeta* propMeta = (*it).valuePtr(); + if (propMeta->isAvailableInClasses(pair, false)) { + names.emplace(propMeta->name(), 0); + } + } + for (auto it = meta->protocols->begin(); it != meta->protocols->end(); it++) { + const Meta* protocolMeta = ArgConverter::GetMeta((*it).valuePtr()); + if (protocolMeta != nullptr && protocolMeta->type() == MetaType::ProtocolType) { + CollectInstanceMemberNames(static_cast(protocolMeta), pair, true, + names); + } + } + } + if (meta->type() != MetaType::Interface) { + return; + } + const char* baseName = static_cast(meta)->baseName(); + if (baseName == nullptr) { + return; + } + const Meta* baseMeta = ArgConverter::GetMeta(baseName); + if (baseMeta != nullptr && baseMeta->type() == MetaType::Interface) { + CollectInstanceMemberNames(static_cast(baseMeta), pair, + baseMeta->isAvailable(), names); + } +} + +} // namespace + +Local MetadataBuilder::GetOrCreateProtocolBindingTemplate( + Local context, const BaseClassMeta* meta, KnownUnknownClassPair pair, + const std::vector& additionalProtocols) { + Isolate* isolate = v8::Isolate::GetCurrent(); + std::vector protocols = additionalProtocols; + std::sort(protocols.begin(), protocols.end()); + protocols.erase(std::unique(protocols.begin(), protocols.end()), protocols.end()); + + ProtocolBindings* bindings = Caches::StateFor(isolate); + std::pair> key(meta, protocols); + if (bindings != nullptr) { + auto it = bindings->templates.find(key); + if (it != bindings->templates.end()) { + return it->second.Get(isolate); + } + } + + Local classTemplate = + MetadataBuilder::GetOrCreateConstructorFunctionTemplate(context, meta, pair); + robin_hood::unordered_map names; + CollectInstanceMemberNames(meta, pair, true, names); + + Local bindingTemplate = FunctionTemplate::New(isolate); + bindingTemplate->InstanceTemplate()->SetInternalFieldCount(1); + bindingTemplate->SetClassName(tns::ToV8String(isolate, meta->name())); + bindingTemplate->Inherit(classTemplate); + MetadataBuilder::RegisterAdditionalProtocols(context, bindingTemplate, pair, protocols, names); + + Local bindingFunc; + Local classFunc; + Local prototype; + bool success = bindingTemplate->GetFunction(context).ToLocal(&bindingFunc) && + classTemplate->GetFunction(context).ToLocal(&classFunc) && + bindingFunc->Get(context, tns::ToV8String(isolate, "prototype")) + .ToLocal(&prototype) && + prototype->IsObject(); + tns::Assert(success, isolate); + // Instances report the class's constructor, which is the one published + // globally and the one subclasses extend. + prototype.As() + ->DefineOwnProperty(context, tns::ToV8String(isolate, "constructor"), classFunc, + PropertyAttribute::DontEnum) + .FromMaybe(false); + + if (bindings != nullptr) { + bindings->templates.emplace(std::move(key), Global(isolate, bindingTemplate)); + } + return bindingTemplate; +} + void MetadataBuilder::ToStringFunctionCallback(const FunctionCallbackInfo& info) { Isolate* isolate = info.GetIsolate(); Local thiz = info.This(); diff --git a/TestRunner/app/tests/ProtocolBindingTests.js b/TestRunner/app/tests/ProtocolBindingTests.js new file mode 100644 index 00000000..14c45387 --- /dev/null +++ b/TestRunner/app/tests/ProtocolBindingTests.js @@ -0,0 +1,67 @@ +// Objects whose declared type adds protocols to their class (id

, T

*) +// get a binding with the protocols' members on top of the class's own. +describe("Protocol-qualified object bindings", function () { + it("reuses one binding per class and protocol set", function () { + var a = TNSPseudoDataType.getId(); + var b = TNSPseudoDataType.getId(); + expect(Object.getPrototypeOf(a)).toBe(Object.getPrototypeOf(b)); + + var c = TNSPseudoDataType.getType(); + var d = TNSPseudoDataType.getType(); + expect(Object.getPrototypeOf(c)).toBe(Object.getPrototypeOf(d)); + }); + + it("does not build new bindings for repeated objects", function () { + var prototype = Object.getPrototypeOf(TNSPseudoDataType.getId()); + var distinct = 0; + for (var i = 0; i < 500; i++) { + if (Object.getPrototypeOf(TNSPseudoDataType.getId()) !== prototype) { + distinct++; + } + } + expect(distinct).toBe(0); + }); + + it("leaves the global class constructors in place", function () { + var nsObject = NSObject; + var tnsType = TNSType; + TNSPseudoDataType.getId(); + TNSPseudoDataType.getType(); + expect(NSObject).toBe(nsObject); + expect(TNSType).toBe(tnsType); + expect(NSDate.date() instanceof NSObject).toBe(true); + expect(NSObject.prototype.isPrototypeOf(UIView.prototype)).toBe(true); + }); + + it("keeps such objects instances of their class", function () { + // getId() answers a private TNSType subclass, so its binding is TNSType's. + var id = TNSPseudoDataType.getId(); + var type = TNSPseudoDataType.getType(); + expect(id instanceof TNSType).toBe(true); + expect(id instanceof NSObject).toBe(true); + expect(id.constructor).toBe(TNSType); + expect(type instanceof TNSType).toBe(true); + expect(type instanceof NSObject).toBe(true); + expect(type.constructor).toBe(TNSType); + expect(NSObject.alloc().init().methodFromProto1).toBeUndefined(); + }); + + it("shares one binding across NSNotificationCenter observer tokens", function () { + var center = NSNotificationCenter.defaultCenter; + var nsObject = NSObject; + var make = function () { + return center.addObserverForNameObjectQueueUsingBlock("ProtocolBindingProbe", null, null, function () {}); + }; + var a = make(); + var b = make(); + try { + expect(Object.getPrototypeOf(a)).toBe(Object.getPrototypeOf(b)); + expect(a.constructor).toBe(b.constructor); + expect(NSObject).toBe(nsObject); + expect(a instanceof NSObject).toBe(true); + } finally { + center.removeObserver(a); + center.removeObserver(b); + } + }); +}); diff --git a/TestRunner/app/tests/index.js b/TestRunner/app/tests/index.js index 9c9ce44f..4338197d 100644 --- a/TestRunner/app/tests/index.js +++ b/TestRunner/app/tests/index.js @@ -128,6 +128,7 @@ require("./ObjCConstructors"); require("./MetadataTests"); // require("./ApiTests"); +require("./ProtocolBindingTests"); require("./ProxyReceiverTests"); require("./NsRuntimeTests"); require("./NsWorkerThreadsTests"); From b669fbaafd86c2f0f7432a6281ccb41d76649f7f Mon Sep 17 00:00:00 2001 From: Eduardo Speroni Date: Fri, 13 Nov 2026 16:35:18 -0300 Subject: [PATCH 2/3] fix(runtime): key protocol bindings by runtime class and keep class property accessors Whether an optional protocol member is bound depends on the object's runtime class, so bindings cached per class metadata let the first object's class decide the members for every later one. They are now cached per runtime class, in a map keyed by Class with no allocation on a hit, and protocol lists are matched in their declared order. Property names were seeded with no accessors, so every protocol property replaced the class's own, e.g. a readwrite class property redeclared readonly by a protocol became readonly. Seeds now carry the accessor counts RegisterInstanceProperties compares against. --- NativeScript/runtime/MetadataBuilder.mm | 69 ++++++++++++++------ TestFixtures/Api/TNSPseudoDataTypes.h | 22 +++++++ TestFixtures/Api/TNSPseudoDataTypes.m | 32 +++++++++ TestRunner/app/tests/ProtocolBindingTests.js | 16 +++++ 4 files changed, 118 insertions(+), 21 deletions(-) diff --git a/NativeScript/runtime/MetadataBuilder.mm b/NativeScript/runtime/MetadataBuilder.mm index 7a1aa52b..b0c398cb 100644 --- a/NativeScript/runtime/MetadataBuilder.mm +++ b/NativeScript/runtime/MetadataBuilder.mm @@ -1,7 +1,6 @@ #include "MetadataBuilder.h" #include #include -#include #include "ArgConverter.h" #include "Caches.h" #include "Constants.h" @@ -594,9 +593,6 @@ throw NativeScriptException( MetadataBuilder::RegisterInstanceMethods(context, ctorFuncTemplate, meta, pair, instanceMembers); MetadataBuilder::RegisterInstanceProtocols(context, ctorFuncTemplate, meta, meta->name(), pair, instanceMembers); - MetadataBuilder::RegisterAdditionalProtocols(context, ctorFuncTemplate, pair, additionalProtocols, - instanceMembers); - ctorFuncTemplate->PrototypeTemplate()->Set( tns::ToV8String(isolate, "toString"), FunctionTemplate::New(isolate, MetadataBuilder::ToStringFunctionCallback)); @@ -669,13 +665,20 @@ NamedPropertyHandlerConfiguration config(nullptr, MetadataBuilder::SwizzledInsta namespace { +// Keyed by the object's runtime class: optional protocol members are bound +// only when that class implements them. struct ProtocolBindings { - std::map>, Global> - templates; + struct Binding { + const BaseClassMeta* meta; + std::vector protocols; + Global bindingTemplate; + }; + robin_hood::unordered_map> byClass; }; -// The instance member names the class's own binding registers, so a -// protocol member the class already has keeps the class's definition. +// The instance members the class's own binding registers, with the property +// accessor counts RegisterInstanceProperties compares against, so a protocol +// member only replaces the class's definition where the old leaf rebuild did. void CollectInstanceMemberNames(const BaseClassMeta* meta, KnownUnknownClassPair pair, bool ownMembers, robin_hood::unordered_map& names) { @@ -688,9 +691,15 @@ void CollectInstanceMemberNames(const BaseClassMeta* meta, KnownUnknownClassPair } for (auto it = meta->instanceProps->begin(); it != meta->instanceProps->end(); it++) { const PropertyMeta* propMeta = (*it).valuePtr(); - if (propMeta->isAvailableInClasses(pair, false)) { - names.emplace(propMeta->name(), 0); + if (!propMeta->isAvailableInClasses(pair, false)) { + continue; + } + uint8_t accessors = (propMeta->hasGetter() ? 1 : 0) + (propMeta->hasSetter() ? 1 : 0); + if (accessors == 0) { + continue; } + uint8_t& seen = names[propMeta->name()]; + seen = std::max(seen, accessors); } for (auto it = meta->protocols->begin(); it != meta->protocols->end(); it++) { const Meta* protocolMeta = ArgConverter::GetMeta((*it).valuePtr()); @@ -720,29 +729,41 @@ void CollectInstanceMemberNames(const BaseClassMeta* meta, KnownUnknownClassPair Local context, const BaseClassMeta* meta, KnownUnknownClassPair pair, const std::vector& additionalProtocols) { Isolate* isolate = v8::Isolate::GetCurrent(); - std::vector protocols = additionalProtocols; - std::sort(protocols.begin(), protocols.end()); - protocols.erase(std::unique(protocols.begin(), protocols.end()), protocols.end()); + Class runtimeClass = pair.realClass(); ProtocolBindings* bindings = Caches::StateFor(isolate); - std::pair> key(meta, protocols); - if (bindings != nullptr) { - auto it = bindings->templates.find(key); - if (it != bindings->templates.end()) { - return it->second.Get(isolate); + auto findBinding = [&]() -> Local { + if (bindings != nullptr) { + auto it = bindings->byClass.find(runtimeClass); + if (it != bindings->byClass.end()) { + for (const ProtocolBindings::Binding& binding : it->second) { + if (binding.meta == meta && binding.protocols == additionalProtocols) { + return binding.bindingTemplate.Get(isolate); + } + } + } } + return Local(); + }; + Local cached = findBinding(); + if (!cached.IsEmpty()) { + return cached; } Local classTemplate = MetadataBuilder::GetOrCreateConstructorFunctionTemplate(context, meta, pair); + // Seeded from the metadata's class alone: a member only the runtime class + // implements may be missing from the class's binding, so it is left to the + // protocols to bind. robin_hood::unordered_map names; - CollectInstanceMemberNames(meta, pair, true, names); + CollectInstanceMemberNames(meta, KnownUnknownClassPair(pair.known), true, names); Local bindingTemplate = FunctionTemplate::New(isolate); bindingTemplate->InstanceTemplate()->SetInternalFieldCount(1); bindingTemplate->SetClassName(tns::ToV8String(isolate, meta->name())); bindingTemplate->Inherit(classTemplate); - MetadataBuilder::RegisterAdditionalProtocols(context, bindingTemplate, pair, protocols, names); + MetadataBuilder::RegisterAdditionalProtocols(context, bindingTemplate, pair, additionalProtocols, + names); Local bindingFunc; Local classFunc; @@ -760,8 +781,14 @@ void CollectInstanceMemberNames(const BaseClassMeta* meta, KnownUnknownClassPair PropertyAttribute::DontEnum) .FromMaybe(false); + // Building the class's binding can wrap objects of this class first. + cached = findBinding(); + if (!cached.IsEmpty()) { + return cached; + } if (bindings != nullptr) { - bindings->templates.emplace(std::move(key), Global(isolate, bindingTemplate)); + bindings->byClass[runtimeClass].push_back(ProtocolBindings::Binding{ + meta, additionalProtocols, Global(isolate, bindingTemplate)}); } return bindingTemplate; } diff --git a/TestFixtures/Api/TNSPseudoDataTypes.h b/TestFixtures/Api/TNSPseudoDataTypes.h index b275ec44..2ab3ee0c 100644 --- a/TestFixtures/Api/TNSPseudoDataTypes.h +++ b/TestFixtures/Api/TNSPseudoDataTypes.h @@ -14,6 +14,25 @@ @end +@protocol TNSOptionalMemberProtocol + +@optional +-(NSString*)optionalMember; + +@end + +@protocol TNSReadonlyValueProtocol + +@property (nonatomic, readonly) NSInteger value; + +@end + +@interface TNSReadwriteValue : NSObject + +@property (nonatomic) NSInteger value; + +@end + @interface TNSType : NSObject -(void)method; @@ -24,5 +43,8 @@ +(id)getId; +(TNSType*)getType; ++(id)getWithOptionalMember; ++(id)getWithoutOptionalMember; ++(id)getReadwriteValue; @end diff --git a/TestFixtures/Api/TNSPseudoDataTypes.m b/TestFixtures/Api/TNSPseudoDataTypes.m index e8c9dfd1..844c1e60 100644 --- a/TestFixtures/Api/TNSPseudoDataTypes.m +++ b/TestFixtures/Api/TNSPseudoDataTypes.m @@ -9,6 +9,26 @@ -(void)methodFromProto2:(NSString*)param; @end +@interface TNSWithOptionalMember : NSObject +@end + +@implementation TNSWithOptionalMember + +-(NSString*)optionalMember { + return @"optional member"; +} + +@end + +@interface TNSWithoutOptionalMember : NSObject +@end + +@implementation TNSWithoutOptionalMember +@end + +@implementation TNSReadwriteValue +@end + @implementation TNSType -(void)method { @@ -41,6 +61,18 @@ @implementation TNSPseudoDataType return internal; } ++(id)getWithOptionalMember { + return [[TNSWithOptionalMember alloc] init]; +} + ++(id)getWithoutOptionalMember { + return [[TNSWithoutOptionalMember alloc] init]; +} + ++(id)getReadwriteValue { + return [[TNSReadwriteValue alloc] init]; +} + +(TNSType*)getType { TNSPseudoDataTypeInternal* internal = [[TNSPseudoDataTypeInternal alloc] init]; internal.propertyFromProto1 = @"property from proto1"; diff --git a/TestRunner/app/tests/ProtocolBindingTests.js b/TestRunner/app/tests/ProtocolBindingTests.js index 14c45387..35f4492d 100644 --- a/TestRunner/app/tests/ProtocolBindingTests.js +++ b/TestRunner/app/tests/ProtocolBindingTests.js @@ -46,6 +46,22 @@ describe("Protocol-qualified object bindings", function () { expect(NSObject.alloc().init().methodFromProto1).toBeUndefined(); }); + it("binds an optional protocol member only for classes that implement it", function () { + var without = TNSPseudoDataType.getWithoutOptionalMember(); + var withMember = TNSPseudoDataType.getWithOptionalMember(); + var withoutAgain = TNSPseudoDataType.getWithoutOptionalMember(); + expect(without.optionalMember).toBeUndefined(); + expect(withMember.optionalMember()).toBe("optional member"); + expect(withoutAgain.optionalMember).toBeUndefined(); + expect(TNSPseudoDataType.getWithOptionalMember().optionalMember()).toBe("optional member"); + }); + + it("keeps the class's own property when a protocol redeclares it readonly", function () { + var object = TNSPseudoDataType.getReadwriteValue(); + object.value = 5; + expect(object.value).toBe(5); + }); + it("shares one binding across NSNotificationCenter observer tokens", function () { var center = NSNotificationCenter.defaultCenter; var nsObject = NSObject; From 4cd9c2771ecc16455e3099b0c1f3946064f43ea7 Mon Sep 17 00:00:00 2001 From: Eduardo Speroni Date: Sat, 14 Nov 2026 14:36:54 -0300 Subject: [PATCH 3/3] fix(runtime): seed protocol bindings only with members the class chain binds A protocol-qualified object's binding skips every protocol member the class chain's bindings already register. The skip list was computed by checking every level of the class chain against the object's own class, but each base class's binding is built for whichever class first reached it and registers a member only if that class responds to it. A member a base class declares but does not implement (an optional member of a protocol it adopts, or a category method without an implementation) and that only the subclass implements was therefore skipped by the protocol while missing from the base binding, and read as undefined. Check each level's members against that level's own class, which every class that could have built its binding descends from, so a skipped member is always present on the chain. --- NativeScript/runtime/MetadataBuilder.mm | 29 ++++++++++++-------- TestFixtures/Api/TNSPseudoDataTypes.h | 20 ++++++++++++++ TestFixtures/Api/TNSPseudoDataTypes.m | 15 ++++++++++ TestRunner/app/tests/ProtocolBindingTests.js | 9 ++++++ 4 files changed, 61 insertions(+), 12 deletions(-) diff --git a/NativeScript/runtime/MetadataBuilder.mm b/NativeScript/runtime/MetadataBuilder.mm index b0c398cb..eca54cce 100644 --- a/NativeScript/runtime/MetadataBuilder.mm +++ b/NativeScript/runtime/MetadataBuilder.mm @@ -676,12 +676,15 @@ NamedPropertyHandlerConfiguration config(nullptr, MetadataBuilder::SwizzledInsta robin_hood::unordered_map> byClass; }; -// The instance members the class's own binding registers, with the property -// accessor counts RegisterInstanceProperties compares against, so a protocol -// member only replaces the class's definition where the old leaf rebuild did. -void CollectInstanceMemberNames(const BaseClassMeta* meta, KnownUnknownClassPair pair, - bool ownMembers, +// The instance members the bindings of `meta`'s class chain are guaranteed to +// have, with the property accessor counts RegisterInstanceProperties compares +// against: a protocol property replaces the class's only where it adds an +// accessor, and a protocol method never replaces one the class chain has. +// Each level is checked against its own class, the one class that every class +// whose wrapping could have built that level's binding descends from. +void CollectInstanceMemberNames(const BaseClassMeta* meta, Class klass, bool ownMembers, robin_hood::unordered_map& names) { + KnownUnknownClassPair pair(klass); if (ownMembers) { for (auto it = meta->instanceMethods->begin(); it != meta->instanceMethods->end(); it++) { const MethodMeta* methodMeta = (*it).valuePtr(); @@ -704,7 +707,7 @@ void CollectInstanceMemberNames(const BaseClassMeta* meta, KnownUnknownClassPair for (auto it = meta->protocols->begin(); it != meta->protocols->end(); it++) { const Meta* protocolMeta = ArgConverter::GetMeta((*it).valuePtr()); if (protocolMeta != nullptr && protocolMeta->type() == MetaType::ProtocolType) { - CollectInstanceMemberNames(static_cast(protocolMeta), pair, true, + CollectInstanceMemberNames(static_cast(protocolMeta), klass, true, names); } } @@ -718,8 +721,11 @@ void CollectInstanceMemberNames(const BaseClassMeta* meta, KnownUnknownClassPair } const Meta* baseMeta = ArgConverter::GetMeta(baseName); if (baseMeta != nullptr && baseMeta->type() == MetaType::Interface) { - CollectInstanceMemberNames(static_cast(baseMeta), pair, - baseMeta->isAvailable(), names); + // A nil class would count every member as implemented. + Class baseClass = objc_getClass(baseMeta->name()); + CollectInstanceMemberNames(static_cast(baseMeta), + baseClass != nil ? baseClass : klass, baseMeta->isAvailable(), + names); } } @@ -752,11 +758,10 @@ void CollectInstanceMemberNames(const BaseClassMeta* meta, KnownUnknownClassPair Local classTemplate = MetadataBuilder::GetOrCreateConstructorFunctionTemplate(context, meta, pair); - // Seeded from the metadata's class alone: a member only the runtime class - // implements may be missing from the class's binding, so it is left to the - // protocols to bind. + // Seeded from the metadata's classes, never the runtime class: a member the + // class chain's bindings may lack is left to the protocols to bind. robin_hood::unordered_map names; - CollectInstanceMemberNames(meta, KnownUnknownClassPair(pair.known), true, names); + CollectInstanceMemberNames(meta, pair.known, true, names); Local bindingTemplate = FunctionTemplate::New(isolate); bindingTemplate->InstanceTemplate()->SetInternalFieldCount(1); diff --git a/TestFixtures/Api/TNSPseudoDataTypes.h b/TestFixtures/Api/TNSPseudoDataTypes.h index 2ab3ee0c..fef6f013 100644 --- a/TestFixtures/Api/TNSPseudoDataTypes.h +++ b/TestFixtures/Api/TNSPseudoDataTypes.h @@ -33,6 +33,25 @@ @end +@protocol TNSDeclaredMemberProtocol + +-(NSString*)declaredMember; + +@end + +@interface TNSDeclaredMemberBase : NSObject +@end + +// Declared for the base class, implemented only by TNSDeclaredMemberSubclass. +@interface TNSDeclaredMemberBase (TNSDeclaredOnly) + +-(NSString*)declaredMember; + +@end + +@interface TNSDeclaredMemberSubclass : TNSDeclaredMemberBase +@end + @interface TNSType : NSObject -(void)method; @@ -46,5 +65,6 @@ +(id)getWithOptionalMember; +(id)getWithoutOptionalMember; +(id)getReadwriteValue; ++(id)getDeclaredMemberSubclass; @end diff --git a/TestFixtures/Api/TNSPseudoDataTypes.m b/TestFixtures/Api/TNSPseudoDataTypes.m index 844c1e60..e8094af4 100644 --- a/TestFixtures/Api/TNSPseudoDataTypes.m +++ b/TestFixtures/Api/TNSPseudoDataTypes.m @@ -29,6 +29,17 @@ @implementation TNSWithoutOptionalMember @implementation TNSReadwriteValue @end +@implementation TNSDeclaredMemberBase +@end + +@implementation TNSDeclaredMemberSubclass + +-(NSString*)declaredMember { + return @"declared member"; +} + +@end + @implementation TNSType -(void)method { @@ -73,6 +84,10 @@ @implementation TNSPseudoDataType return [[TNSReadwriteValue alloc] init]; } ++(id)getDeclaredMemberSubclass { + return [[TNSDeclaredMemberSubclass alloc] init]; +} + +(TNSType*)getType { TNSPseudoDataTypeInternal* internal = [[TNSPseudoDataTypeInternal alloc] init]; internal.propertyFromProto1 = @"property from proto1"; diff --git a/TestRunner/app/tests/ProtocolBindingTests.js b/TestRunner/app/tests/ProtocolBindingTests.js index 35f4492d..b4eb1f03 100644 --- a/TestRunner/app/tests/ProtocolBindingTests.js +++ b/TestRunner/app/tests/ProtocolBindingTests.js @@ -56,6 +56,15 @@ describe("Protocol-qualified object bindings", function () { expect(TNSPseudoDataType.getWithOptionalMember().optionalMember()).toBe("optional member"); }); + it("binds a protocol member a base class declares but only the subclass implements", function () { + // Builds the base class's binding for a class that lacks the member. + var base = TNSDeclaredMemberBase.alloc().init(); + expect(base.declaredMember).toBeUndefined(); + + var object = TNSPseudoDataType.getDeclaredMemberSubclass(); + expect(object.declaredMember()).toBe("declared member"); + }); + it("keeps the class's own property when a protocol redeclares it readonly", function () { var object = TNSPseudoDataType.getReadwriteValue(); object.value = 5;