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..eca54cce 100644 --- a/NativeScript/runtime/MetadataBuilder.mm +++ b/NativeScript/runtime/MetadataBuilder.mm @@ -1,5 +1,6 @@ #include "MetadataBuilder.h" #include +#include #include "ArgConverter.h" #include "Caches.h" #include "Constants.h" @@ -532,12 +533,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; @@ -589,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)); @@ -662,6 +663,141 @@ NamedPropertyHandlerConfiguration config(nullptr, MetadataBuilder::SwizzledInsta return ctorFuncTemplate; } +namespace { + +// Keyed by the object's runtime class: optional protocol members are bound +// only when that class implements them. +struct ProtocolBindings { + struct Binding { + const BaseClassMeta* meta; + std::vector protocols; + Global bindingTemplate; + }; + robin_hood::unordered_map> byClass; +}; + +// 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(); + 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)) { + 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()); + if (protocolMeta != nullptr && protocolMeta->type() == MetaType::ProtocolType) { + CollectInstanceMemberNames(static_cast(protocolMeta), klass, 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) { + // 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); + } +} + +} // namespace + +Local MetadataBuilder::GetOrCreateProtocolBindingTemplate( + Local context, const BaseClassMeta* meta, KnownUnknownClassPair pair, + const std::vector& additionalProtocols) { + Isolate* isolate = v8::Isolate::GetCurrent(); + Class runtimeClass = pair.realClass(); + + ProtocolBindings* bindings = Caches::StateFor(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 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, 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, additionalProtocols, + 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); + + // Building the class's binding can wrap objects of this class first. + cached = findBinding(); + if (!cached.IsEmpty()) { + return cached; + } + if (bindings != nullptr) { + bindings->byClass[runtimeClass].push_back(ProtocolBindings::Binding{ + meta, additionalProtocols, Global(isolate, bindingTemplate)}); + } + return bindingTemplate; +} + void MetadataBuilder::ToStringFunctionCallback(const FunctionCallbackInfo& info) { Isolate* isolate = info.GetIsolate(); Local thiz = info.This(); diff --git a/TestFixtures/Api/TNSPseudoDataTypes.h b/TestFixtures/Api/TNSPseudoDataTypes.h index b275ec44..fef6f013 100644 --- a/TestFixtures/Api/TNSPseudoDataTypes.h +++ b/TestFixtures/Api/TNSPseudoDataTypes.h @@ -14,6 +14,44 @@ @end +@protocol TNSOptionalMemberProtocol + +@optional +-(NSString*)optionalMember; + +@end + +@protocol TNSReadonlyValueProtocol + +@property (nonatomic, readonly) NSInteger value; + +@end + +@interface TNSReadwriteValue : NSObject + +@property (nonatomic) NSInteger value; + +@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; @@ -24,5 +62,9 @@ +(id)getId; +(TNSType*)getType; ++(id)getWithOptionalMember; ++(id)getWithoutOptionalMember; ++(id)getReadwriteValue; ++(id)getDeclaredMemberSubclass; @end diff --git a/TestFixtures/Api/TNSPseudoDataTypes.m b/TestFixtures/Api/TNSPseudoDataTypes.m index e8c9dfd1..e8094af4 100644 --- a/TestFixtures/Api/TNSPseudoDataTypes.m +++ b/TestFixtures/Api/TNSPseudoDataTypes.m @@ -9,6 +9,37 @@ -(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 TNSDeclaredMemberBase +@end + +@implementation TNSDeclaredMemberSubclass + +-(NSString*)declaredMember { + return @"declared member"; +} + +@end + @implementation TNSType -(void)method { @@ -41,6 +72,22 @@ @implementation TNSPseudoDataType return internal; } ++(id)getWithOptionalMember { + return [[TNSWithOptionalMember alloc] init]; +} + ++(id)getWithoutOptionalMember { + return [[TNSWithoutOptionalMember alloc] init]; +} + ++(id)getReadwriteValue { + 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 new file mode 100644 index 00000000..b4eb1f03 --- /dev/null +++ b/TestRunner/app/tests/ProtocolBindingTests.js @@ -0,0 +1,92 @@ +// 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("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("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; + expect(object.value).toBe(5); + }); + + 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");